最近發現 GNU Stow 這個軟體,其主要提供的功能是以 symlink 來做到軟體安裝。其自我介紹文中第一段話為:
GNU Stow is a symlink farm manager which takes distinct packages of software and/or data located in separate directories on the filesystem, and makes them appear to be installed in the same place.
意思就是說:你可以用 GNU Stow 把本來裝在四處的資料或程式,以 symlink,讓它們看來好像是全裝在同一個目錄下。
乍看之下好像怪怪的沒有什麼用處(為何不一開始就裝在一起?)但轉念一想:這不就是 homebrew 的做法嗎?
看來 GNU Stow 所做的工作,就類似於 brew link
這個階段,把本來安奘在 A 處的各個檔案,逐一在 B 處建立相對應的 symlink。由於 B 處都是 symlink,要移除也相對簡單。並且,能以 symlink 內容作為某種安裝證明,不怕失誤刪到了不在管轄範圍內的物件。
這做法對於管理 $HOME
底下的 dotfile 十分有用。由於 dotfile 都是必須要放在 $HOME
底下指定位置的檔案,如果要以版本控制系統來管理,那就還要另外有一套方式去把那些檔案自版本控制系統中拷貝出來放在 $HOME
底下,如果還想要有簡單的同步或刪除機制的話,用 GNU Stow / symlink 似乎就很適合了。
以下用幾個簡單的範例來示範一下。首先是建一個 dotfiles/ 目錄,並在底下依照軟體名建立出若干子目錄。子目錄底下則是放置該軟體的 dotfile。並且,使其中 dotfile 路徑有保留住 $HOME
之後的部分。例如,如果某 abc 軟體的 dotfile 為 $HOME/.config/abc/abc.conf
,那就把其 $HOME
的部分換為 dotfiles/abc/
。也就是要建出 dotfiles/abc/.config/abc/abc.conf
。
如果是從既有的檔案移管,準備起來大致上像這樣:
mkdir -p dotfiles/zsh
cp ~/.zshrc ~/.zshenv dotfiles/zsh/
mkdir -p dotfiles/emacs/.emacs.d
cp ~/.emacs.d/init.el dotfiles/emacs/.emacs.d
mkdir -p dotfiles/sway/.config/sway/
cp ~/.config/sway/* dotfiles/sway/.config/sway/
mkdir -p dotfiles/wezterm/.config/wezterm/
cp ~/.config/wezterm/* dotfiles/wezterm/.config/wezterm/
將 dotfiles/
準備完畢之後,就執行 stow
cd dotfiles
stow --target=$HOME zsh/
stow --target=$HOME emacs/
stow --target=$HOME sway/
stow --target=$HOME wezterm/
這麼一做完之後,原本 $HOME
底下的 dotfile 就會被 stow 改成是連到 dotfiles/
目錄底下某個檔案的 symlink 了。
而 dotfiles/
目錄本身就可以再透過 git init
等步驟來納入版本控制系統中,並且,其內容是依照各個軟體名分類放好了的。就算實際在 $HOME
底下會出現在很多子目錄,但在版本控制系統中是很一致的。
特別是在刪除的時候。有時候試用軟體要寫幾個 dotfile,一陣子沒用後,把軟體刪除了,但 dotfile 卻沒有一起清。久而久之就忘了那些在 $HOME
底下的 .abcrc
、.xyz.conf
倒底是有用還是已經沒用的了。變得想清理又會怕弄錯。
透過本文所述的管理方式,如果哪天想把軟體 abc 的 dotfile 全丟了,也不必再花心力去找出它的 dotfile 是 .abcrc
、.abc.conf
、config/abc/abc.conf
還是什麼其他冷門的命名慣例,只要以 stow 指令:
stow --target=$HOME --delete abc/
就可以把當初安裝上的所有檔案一次清光了。
如果發現不小心刪錯了,再重跑一次 stow
就可以救回來。
GNU Stow 的機制簡單又合理。看來應該能讓管理個人的 dotfile 的過程變得更有條理一些。
這一次的 YAPC Japan 是辦在廣島國際會議中心,位於和平紀念公園內,離原爆圓頂館很近。雖然離市中心區有一小段距離,但走起來並不算遠。
比起去年的 YAPC::Kyoto,這次的活動規模似乎擴大了。贊助商數竟然超過 50 家,而且有八間是廣島當地的中小企業。這還真是令人驚訝。
不過這次的議程,在內容方面與 Perl 直接相關的已經減少到一半左右了,可能算是有些偏離主體?但儘管如此,技術方面的主題仍然挺吸引人,並且技術知識的共享方面是共通的。似乎可以不必計較。
贊助商的攤位方面,有十來間到場設攤宣傳的廠商。另我印象比較深刻的是 chocoZAP 這個主打用零碎時間來運動的輕度健身的品牌,竟然也是贊助商(其公司為 RIZAP)。而且也在現場招募。從宣傳單上看來,伺服器端是 Ruby,iOS App 為 Swift,Android 為 Kotlin 這樣的組合吧。宣傳單的背面則是拉人加入健身房會員的廣告了(附折扣碼)(笑)。
另一間另我印象深刻的是 Findy。其主產品是協助軟體團隊能理解開發現況的視覺化輔助工具。跟我在碩士班期間做的題目很像。能給一個團隊一些簡明的圖表來迅速看出目前專案開發的狀態,瓶頸之有無,人員協作的連結度等等。應該比較偏向是給管理階級的人員看的圖資工具。
議程本體方面,總共分成三軌,自然也是等日後看有沒有錄影可以補完。在此先大致上摘要了一些我現場聽到覺得有意思的內容。
〈VISAカードの裏側と “手が掛かる” 決済システムの育て方 by 三谷〉。講者是在 VISA 發卡公司員工。這演講算是業界甘苦談吧。難度核心部份在於要能夠「寬鬆而有彈性地」去處理由 VISA API 呼過來的各種不太合理的內容。講者宣稱在聽完後,大家之後必定會在拿到刷卡對帳單時好好看一下在那張小單據印上的各欄資訊。這是真的。
〈rakulangで実装する! RubyVM by 八雲アナグラ〉。講者探索了如何以 rakulang 製作一個簡單的 Ruby VM,並分享了其摘要。嚴格說起來他做的程式不算是 Ruby VM,因為不能吃 bytecode,只能吃文字格式的組語。但要做出 YARV bytecode 似乎不那麼容易,所以就退而求其次了。是深入淺出,內容豐富的演講。
〈PerlでつくるフルスクラッチWebAuthn/パスキー認証 by macopy〉。講者用 live coding 形式說明了 WebAuthn 協定。概要清晰,有把 WebAuthn 中幾個重點都解釋出來。live coding 最後也成功了。但看來有 80% 左右的程式碼是 Copilot 寫的啊。 (結果 ok 就好)
Keynote 由「とほほのWWW入門」這個網站的作者「杜甫々」來登台。看來是位人人皆知的名人,這網站上所有文件似乎都是他自己編寫的,種類眾多,從軟體技術細節到所得稅法都有著墨。真是不得由衷讓人欽佩:怎麼會有人這麼會寫文件?
伴隨研討會的舉行,同時這次由面白法人主催了個 perlgolf 活動。很久沒玩了,還是頂懷念這種看似胡搞,但卻又能確實達成目的且節節變難的重構過程啊。同時也有種:「這才是 Perl 研討會啊!」的一種爽快感。雖然實務上不會有要把程式碼節節縮短的需要,但對同一個問題反覆進行重構、不斷得出不同風格的解法,不也是一種解決問題所必備的基礎能力嗎?
最近注意到有三個新版連發:
但是內容都一樣,看來是專門為了處理以下兩個 CVE 而發的新版本:
CVE-2023-47038 的影響範圍是自 5.30.0 (釋出日為 2019/05/22) 到 5.38.0 (釋出日為 2023/07/02)。這範圍不小,得來升級一下。
在 Fedora Linux 上,要裝設 Sway 的方式是裝 Sway Desktop 這一組套件:
dnf group install 'Sway Desktop'
這麼做完之後,在 GDM 登入畫面內某個不起眼的選單內底就可以選擇使用 Sway。
但只是這樣的話,有一些需要在 sway 啟動前就設定好的環境變數似乎就無法可設。主要是這些:
MOZ_ENABLE_WAYLAND=1
INPUT_METHOD=fcitx
GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
稍微找了一下,發現透過 gdm 開的 sway,似乎是叫做 start-sway
# pgrep -lfa gdm
1208 /usr/sbin/gdm
2011 gdm-session-worker [pam/gdm-password]
2151 /usr/libexec/gdm-wayland-session --register-session start-sway
在 /usr/share/wayland-sessions/sway.desktop
中也可看到這行:
Exec=start-sway
在 /etc
搜尋 start-sway
這個字串的話,可看到 /usr/bin/start-sway
這檔名出現在 /etc/sway/environment
內。
# sudo rg start-sway /etc/
/etc/sway/environment
2:# from /usr/bin/start-sway script for all users of the system.
/usr/bin/start-sway
這個檔案是個 shell script,讀其程式碼後發現它的主要作用是去讀取 /etc/sway/environment
及 ${HOME}/.config/sway/environment
的內容,吃進這兩檔案內所設好的變數之後,再啟動 sway。
看來正好能滿足我的需要。於是將一干變數塞入 ${HOME}/.config/sway/environment
後,登出、重新登入。就管用了。
我猜 start-sway
這 shell scrett 是 Fedora Linux 方面對設定環境變數這需求所實做出的一個變通方法。
目前使用 Sway 主要的問題之一,是在只支援 Wayland 的幾個應用程式視窗內進行打字時,雖可看到輸入的「預編輯」有正確地內嵌在應用程式中,但是選字窗則是完全消失,毫無蹤影。
不少通 Wayland 協定的終端機類程式,如 foot、WezTerm 等等,皆是能打字,但選字窗看不到。反而,那些需透過 XWayland 才能執行的程式,像是 lxterminal 跟 Emacs 就能打字,能看到選字窗:
依 @wengxt 在 Fcitx5 專案中所留之言,不見選字窗,是因為 Sway 卻乏幾項關於 input_panel 協定之實作,而讓 fcitx5 無法得知該將選字窗繪製在何處。
依照 fcitx5 wiki Using_Fcitx_5_on_Wayland#Sway 這一頁中簡短的說明看來,似乎已經有一解 Sway PR 7226 待併入 Sway 之中。雖然仍需等待,但總之,各類程式都有替代方案,因此都算可用。
https://www.fastmail.help/hc/en-us/articles/4406536368911-Masked-Email
基本上是把多個自動生成的 email 地址對應自己的信箱的轉寄服務。這自動生成的 email 地址通常看來就像是串不好唸的咒文。
賣點就是匿名性。如果有什麼網站資料被竊盜的問題發生時,可以立刻把流出去的那個 email 地址給撤銷掉,讓它變成無效的。(從這點看來,自動生成的演算法必 須要類似 UUID 那樣,既不太容易列舉、又絕對不會生出重複的字串。)
原則上是每個站都給不同的 email 地址會比較好管理。
像在 Usenet group 這種公開場所,基本上必須在 From 欄位內給出自己的 email 給所有人看。顯然也很適合來使用這種 email 地址。
現在有一些 Usenet group 上的人會故意在 From 內的 Email 地址處提供像 `john@ex{amp]le.com' 這種不合規格的字串,也算是個方法。由於 hostname 部分 解不出來,至少能防到一些爬資料的機器人。但有智慧的物種則有機會看穿這種 email 地址的用意而在必要時手動修正一下。只是或許有被 NNTP 伺服器擋掉,或 是造成處理失誤等可能性。
另外想到有 https://simplelogin.io/ 這個服務。基本上是一樣的,但 simplelogin 只有提供轉寄,而 fastmail 主要是提供 email 信箱。
還有 https://duckduckgo.com/email/ Duckduckgo 那裡也有提供類似的,他們叫做 "Private Duck Address"。似乎可以一直產生新的。但是必須先去申請一個 @duck.com 的 email 地址下來,然後這個 email 地址也只是轉寄到現有信箱。
proton mail 那邊好像還沒有這做到這種地步,不知是否有機會跟進。
ibus mozc 的預設輸入模式叫做 "Direct Input",也就是「不是日文輸入」的意思。依照預設熱鍵設定看來,似乎是為了能讓人使用鍵盤上的「變換」鍵來快速於「英數」與「平假名」兩種輸入模式間切換。但對於使用多種輸入法的人來說,變成要在切換到 mozc 後還要再多按一次變換鍵才能開始打平假名,反而礙事。
要讓 ibus mozc 的預設模式為平假名的辦法,是去修改 .config/mozc/ibus_config.textproto
,讓 active_on_launch
為 True
:
active_on_launch: True
改完後重新開機。
順帶附上整個檔內容如下,以供參考:
# `ibus write-cache; ibus restart` might be necessary to apply changes.
engines {
name : "mozc-jp"
longname : "Mozc"
layout : "default"
layout_variant : ""
layout_option : ""
rank : 80
symbol : "あ"
}
engines {
name : "mozc-on"
longname : "Mozc:あ"
layout : "default"
layout_variant : ""
layout_option : ""
rank : 99
symbol : "あ"
composition_mode : HIRAGANA
}
engines {
name : "mozc-off"
longname : "Mozc:A_"
layout : "default"
layout_variant : ""
layout_option : ""
rank : 99
symbol : "A"
composition_mode : DIRECT
}
active_on_launch: True
perl-5.38.0 變更說明: https://metacpan.org/release/RJBS/perl-5.38.0/changes
摘要:
class
, field
, method
三個定義類別的關鍵字。(實驗性機能。須以 use feature 'class';
打開。)
PERL_RAND_SEED
環境變數為程式開跑時的亂數種子值。相當於去呼叫 srand()
。
package
或 class
的 .pm
檔案) 最後補上 true
'
分隔符號。
這應該是這是兩三年來著力最多的部份吧。比起多數提供在 CPAN 上的 OO 框架,perlclass 相對來說很好懂。有三個新的關鍵字與其帶來的新語意,但是幾乎沒有新的語法規則,至少目前沒有。
參考:perlclass 文件。
三個新關鍵字是:class
、method
、field
。class
用來定義類別。method
用來定義 instance method,field
用來宣告 instance variable。
以 class
定義出來的類別會自動以 new
為其 constructor 函式,並且,所有 instance method 體內皆能以 $self
表示目前物件。class 體內的 instance method 的 signature 則不可包含 $self
。
而 class
體內的 sub
可用來定義 class method, class
體內的 my
則可用來宣告 private class variable。 class
體內的 our
則可用來宣告 public class variable。
在同一 class
體內以 my
、our
、field
宣告出來的諸變數是共用同一個名稱空間的。名稱有衝突的話,只有最後一個變數會被留下。
在同一 class
體內以 sub
與 method
宣告出來的各函式也是共用同一個名稱空間的。名稱有衝突的話,只有最後一個函式會被留下。
以 field
宣告出來的變數為 instance variable,只能被用於 instance method 體內。
以 method
定義出來的函式為 instance method,體內可使用 instance variable,同時亦會自動有 $self
可用來代表目前物件。要呼叫其他同一 class 內的 class method 的寫法與普通函式呼叫相同。要呼叫 instance method 時則是需要對 $self
呼叫。
當做範例,以下是一個描述了五段變速電風扇的簡易類別。
use v5.38;
use feature 'class';
use builtin 'true', 'false';
class Fan {
use List::Util qw(min max);
my $__id = 1;
my $RPM_MAX = 600;
my $RPM_MIN = 100;
my $RPM_STEP = 100;
sub vendor { "Cool Company" }
sub speeds { ($RPM_MAX - $RPM_MIN ) / $RPM_STEP }
field $id = $__id++;
field $rpm = 3600;
field $on = false;
method id { $id }
method speed { $rpm / $RPM_STEP }
method turn_on { $on = true }
method turn_off { $on = false }
method faster { $rpm = min($rpm + $RPM_STEP, $RPM_MAX) }
method slower { $rpm = max($rpm - $RPM_STEP, $RPM_MIN) }
method TO_JSON {
+{
# instance method / variable
id => $self->id(),
speed => $self->speed(),
on => $on,
# class method / variable
vendor => vendor(),
speeds => speeds(),
rpm_max => $RPM_MAX,
rpm_min => $RPM_MIN,
}
}
}
前述以 sub
定義出來的 class method,其實也可以被當成是 instance method 來呼叫。例如 Fan
類別的 vendor
,有兩種呼叫方式:
my $fan = Fan->new;
say $fan->vendor(); # (1)
say Fan->vendor(); # (2)
(1) 與 (2) 兩處對應到的函式是同一個。
以 class
所附的 new
函式所做出來物件,字串形式上會有個 OBJECT 字樣:
my $fan = Fan->new;
say "$fan";
#=> Fan=OBJECT(0x19477b0);
如果是以常見的 blessed HashRef 做法 (即:bless {}, 'Fan'
) 所造出來的物件,其字串形式是這樣:
Fan=HASH(0x1b3a5e8)
由此可窺知在 perl 內部多了一種以 OBJECT 為表記的值。
true
以往都要在 .pm
檔案最後補個 1;
,然後還得向人說明這行看似無作用的程式碼的用途。以後在開頭加上 use v5.38;
後就不必在檔尾補 1
了。
# Foo.pm
use v5.38;
package Foo {
sub foo() { "Bar" }
}
附帶一提,就算在最後在 Foo.pm 最後故意加上 0;
,也不會讓 use Foo;
失敗。
在 reddit.com/r/perl 板上看到 pmz 問了這樣子的一個問題:
XML::Twig, get value (or not) without dying
Currently I do:
if (defined $elt->first_child('addr')->first_child('postalCode')) { $patient{patient_postal_code} = $elt->first_child('addr')->first_child('postalCode')->text ; }because if I don't check for "defined" and the resulting value is null , it dies.
原文連結: https://www.reddit.com/r/perl/comments/1492sc1/xmltwig_get_value_or_not_without_dying/
這一方面是在針對 XML::Twig 模組的用法提問,另一方面是在問:當 first_child('addr')
傳回 undef
時,表示在目前 XML 文件樹中沒有 addr
元素,而後o方的連環呼叫 first_child('postalCode')
之處就會讓程式執行中斷,因為原本預期是物件的地方,也有可能是 undef
。那有沒有能讓連環呼叫不會使程式失敗,並且在遇到 undef
時,讓整串算式的結果成為 undef
的辦法?
如果把題目重新描述得較抽象一些的話,就是:假設有一類別,其下有方法 a()
、b()
、c()
。這三個方法可能會傳回 undef
或同一類別的物件。今有一物件 $o
屬此類別,考慮此連環呼叫式:
$res = $o->a()->b()->c();
此式在 a()
或 b()
傳回 undef
時,就會讓程式中斷,並會出現像這樣的錯誤訊息:
Can't call method "c" on an undefined value
亦即:無法對 undef
(undefined value) 呼叫 "c" 這個方法。這是當然的,因為 undef
並非物件。
試問:有何手段能改寫此式,能避免前述錯誤之發生,使 $res
在 a()
、b()
、c()
任一者傳回 undef
時就算得 undef
,且在全無 undef
情況下,又能正確算得 c()
的傳回值?
在一些語言中,這可輕易得透過使用 safe-navigation operator 或 optional chaining operator 來解決。例如 javascript 或 kotlin 中的 ?.
:
res = o.a()?.b()?.c();
又例如 raku 語中的 .?
:
$res = $o.a().?b().?c();
但直到目前 perl 5.38 為止,在 perl 語言中尚未出現同等作用的算符。
有個直觀的改寫方式是像這樣:
$res_a = $o->a();
$res_b = $res_a && $res_a->b();
$res = $res_b && $res_b->c();
但在讓程式變長、用了多個臨時變數而讓程式碼變得稍微不好讀之外,這改寫法還不通用,碰到類似結構、名稱不同的式子就得手動從頭改寫。實在算不上是好辦法。
此外,有個超間單的改寫方法:
$res = eval { $o->a()->b()->c() };
但是,這個寫法的副作用,就是所有例外事件通通都會被忽略,雖然簡單好改,但其副作用很強。不見得在各種情境都適合。
在此提供一個以 Monad Design Pattern 來達成得的一個解法。
改寫方法如下:
$res = SafeNav->wrap($o) ->a()->b()->c() ->unwrap();
其中 SafeNav
的定義為:
use v5.36;
package SafeNav {
sub wrap ($class, $o) { bless \$o, $class }
sub unwrap ($self) { $$self }
sub AUTOLOAD {
our $AUTOLOAD;
my $method = substr $AUTOLOAD, 2 + rindex($AUTOLOAD, '::');
my ($self, @args) = @_;
# [a]
(defined $$self) ?
__PACKAGE__->wrap( $$self -> $method(@args) ) : # [a.1]
$self; # [a.2]
}
sub DESTROY {}
};
這類別基本上是把所有純量值都包裝成物件。SafeNav
類別定義有 AUTOLOAD
這個特殊用途的函式,使其物件可接受所有方法呼叫。就算包著的值是 undef
方法呼叫也都不會失敗,而是會進入 AUTOLOAD
函式中。
於是在 [a] 處就可進行核心處理:若自己包著的值不是 undef
,就對其呼叫原方法([a.1] 處)。倘若自己包著的值已經是 undef
了,那繼續躺平做自己(?)就可以了([a.2] 處)。
多虧有 AUTOLOAD
機制,原連環呼叫 ->a()->b()->c()
的部份是原樣保留著的。在此並列、比較一下:
$res = $o ->a()->b()->c();
$res = SafeNav->wrap($o) ->a()->b()->c() ->unwrap();
前後的 wrap()
與 unwrap()
算是明確界定了 SafeNav
類別在此式中的作用範圍。過了 unwrap()
之後如果還繼續串著更多方法呼叫,就一定不是被包在 SafeNav
裡面的。
這麼一來,會被「妥善忽略」的,就只有在碰到 undef
這個值的時候。若在 a、b、c 三個方法內發生了任何例外事件,程式還是能正確地被中止。並不會有太多副作用。在本文內提到的三種改寫辦法裡面,算是既通用,又不太囉唆的一種。
Recently on reddit.com/r/perl, pmz posted a question like this:
XML::Twig, get value (or not) without dying
Currently I do:
if (defined $elt->first_child('addr')->first_child('postalCode')) { $patient{patient_postal_code} = $elt->first_child('addr')->first_child('postalCode')->text ; }because if I don't check for "defined" and the resulting value is null , it dies.
Link to the original post: https://www.reddit.com/r/perl/comments/1492sc1/xmltwig_get_value_or_not_without_dying/
While on one hand the question is about how to use XML::Twig, on the other hand the obvious inconvenience here is when first_child('addr')
returns undef
, which means there are no <addr>
element underneath, the following call of first_child('postalCode')
would make the programm die. Generally speaking: in a chain of calls we expect objects to be present in all positions, but sometimes there are undef
. Given that, is there a way to avoid the program from dying and let the entire call chain return undef
if undef
is encountered in the calling chain ?
To formalize the question a bit more generically: assume a class with instance methods a()
, b()
, and c()
. These methods may return an instance of same class, or ocassionally, undef
. Consider the following chain of calls originally from $o
:
$res = $o->a()->b()->c();
In case any of a()
, b()
, or c()
returns undef
, the program dies with messages like this:
Can't call method "c" on an undefined value
Which suggests b()
returns undef
and since undef
is not an object, we cannot call methods on it.
Now, could we rewrite the same program to prevent the abovementioned error from happening, while making $res
be undef
if any of a()
, b()
, c()
returns undef
, or otherwise, the return value of c()
?
In some other programming languages, such purpose could be satisfied by using the safe-navigation operator, such as ?.
in javascript or kotlin:
res = o.a()?.b()?.c();
Or in raku, .?
$res = $o.a().?b().?c();
However, we haven't seen anything similar up until perl 5.38 just yet.
A rather intuitive way to rewrite would be something like this:
$res_a = $o->a();
$res_b = $res_a && $res_a->b();
$res = $res_b && $res_b->c();
However, besides making the program much longer and less easier to grasp, the rewrite is not generic. It'll be different for similar statements with different method names. Not a good strategy.
Meanwhile, here's a super simple and generic way:
$res = eval { $o->a()->b()->c() };
However, with the powerful side-effect of eval
, all exceptions would be ignored while we are only interested in ignoring undefined values. That is a lot more than what we want. Even though it looks simple, it is probably not applicable.
Here is a solution with Monad design pattern.
The rewritten version looks like this:
$res = SafeNav->wrap($o) ->a()->b()->c() ->unwrap();
The SafeNav
is defined as the folowing.
use v5.36;
package SafeNav {
sub wrap ($class, $o) { bless \$o, $class }
sub unwrap ($self) { $$self }
sub AUTOLOAD {
our $AUTOLOAD;
my $method = substr $AUTOLOAD, 2 + rindex($AUTOLOAD, '::');
my ($self, @args) = @_;
# [a]
(defined $$self) ?
__PACKAGE__->wrap( $$self -> $method(@args) ) : # [a.1]
$self; # [a.2]
}
sub DESTROY {}
};
SafeNav
is a class that wraps all scalar values and equips with AUTOLOAD
for responding to all method calls.
Inside AUTOLOAD
there is the core part of our logic in [a]: If we are not wrapping an undef
value, we call the original method on it, then re-wrap the return value ([a.1]). Or if we are wrapping an undef
, we ignore the method call and just lay down and keep being ourselves ([a.2]).
Thanks to the mechanism of AUTOLOAD
, the original form of ->a()->b()->c()
is kept exactly the same after the rewrite. Let's put both versions side-by-side for a quick comparison:
$res = $o ->a()->b()->c();
$res = SafeNav->wrap($o) ->a()->b()->c() ->unwrap();
The wrap()
at the front, together with unwrap()
at the back, form a clear boundary in which SafeNav
is effective. Method calls after unwrap()
are not not guarded by SafeNav
.
With that, we properly ignore undef
values, nothing more. If other kinds of exceptions are thrown from method a, b, c, the program would correctly abort. In the 3 proposed ways to rewrite the program in this article, the SafeNav
monad is both generic and not adding too much verbosity to the original program.
最近開始看到 Perl Toolchain Summit 的結果了。稍微讀了一下,以此篇文章綜合摘要一下今年在 Perl Toolchain 那裡的幾項決定(Lyon Amendment):
如果有經常性在升級 perl 的話應該是不會因此受到甚麼影響。主要會被影響到的應該是一些因故無法升級的老系統,最糟有可能會變成不能使用 cpan 指令來安裝模組、或是有某些重要的模組在某版之後變成無法編譯、要使用的話就必須固定使用某個舊版。在老系統上安裝新模組的需求一般來說通常倒是安全性更新。考慮到作業系統本身也需要被更新,讓 Perl / CPAN 相關工具以類似作業系統更新的步調去「與時俱進」似乎還算是合理的。
註:
參考:
(注意: http 網址。其實用 https 也可以連,但是站長沒有把 cert domain 弄對。看來是同一台機器上架了多個網站,然後就「順便」多了一個錯誤的 https 進入點。)
一陣子之前在 HN 上紅了一陣子的這個網站是個搜尋引擎兼 web proxy。作者要在爲了要讓他蒐集的三十年前的老電腦也可以看各網頁而做了這個特殊的 web proxy。
實際上看來是把搜尋丟去 duckduckgo,並且把搜尋結果中的每個網址都換成他的 read.php,而 read.php 中實做 Readability 演算法,把原網頁中能內容極度精簡化再送回給 browser。 也因此有可能會碰到無法處理的網頁。但,在 Readability 演算法管用時的瀏覽體驗非常的好。
因此,給記憶體比較不足的機器(如 Raspberry Pi)來配合 w3m / lynx / eww 等純文字瀏覽器來用也是非常合的。
簡單筆記一下。以 Debian 爲例。
在安裝 DBD::mysql
時需要 mysql_config
這個指令,但如果要使用 MariaDB Client 而不能安裝 MySQL Client 時,也可以讓 DBD::mysql
去使用 mariadb_config
,使其改與 libmariadb 連結。由於 MariaDB Client 也可以用來與 MySQL Server 溝通,在不想額外加入 mysql 套件的時候也可以使用這招來省點事。
首先需要裝好開發與編譯用的函式庫:
sudo apt-get install libmariadb-dev
然後是在安裝 DBD::mysql
時,透過指定環境變數的方式,讓其在編譯過程去使用 MariaDB 的編譯設定:
以 cpm 指令來裝
DBD_MYSQL_CONFIG=/usr/bin/mariadb_config cpm install -g DBD::mysql
以 cpanm 指令來裝
DBD_MYSQL_CONFIG=/usr/bin/mariadb_config cpanm DBD::mysql
以 cpan 指令來裝
DBD_MYSQL_CONFIG=/usr/bin/mariadb_config cpan -i DBD::mysql
以上。
https://metacpan.org/release/GUGOD/PerlX-ScopeFunction-0.03
好幾年前我擅自開始把 PerlX:: 這個 namespace 當成是「Perl 語言的延伸」的 意義在使用,並且做了一些有的沒的新語法。最近這星期做了 PerlX::ScopeFunction。
這個模組提供兩個新的語法關鍵字: let 與 with。
先看範例,就能大致上掌握這兩個關鍵字的用法:
use v5.36;
use List::Util qw( sum0 );
use List::MoreUtils qw( part minmax );
use PerlX::ScopeFunction qw(let with);
my @input = (3,1,3,3,7);
let ( ($min,$max) = minmax(@input); $mean = sum0(@input)/@input ) {
say "$min <= $mean <= $max";
}
with ( part { $_ % 2 } @input ) {
my ($evens, $odds) = @_;
say "There are " . scalar(@$evens) . " even numbers: " . join(" ", @$evens);
say "There are " . scalar(@$odds) . " odd numbers: " . join(" ", @$odds);
}
這兩個關鍵字提供了一種可以將幾個指定變數的語義範圍縮限起來的方式。前述範 例中 let 語句所造出的 $min、$max、$mean 三個變數的語義範圍是在後方的區塊 之內。with 語句則是能將一個算式取值,並把結果裝入 @_ ,使其能在後方的區塊內使 用。
這兩種語法結構都是從其他語言借過來的,在 Perl5 目前的語法中,最類似的是
sub closure 與 do
:
sub {
my ($evens, $odds) = @_;
...
}->(part { $_ % 2 } @input);
do {
my ($min,$max) = minmax(@input);
my $mean = sum0(@input)/@input;
...
};
而 PerlX::ScopeFunction 所做的就是提供一些新關鍵字,讓這種特殊片語寫起來 更加明瞭一些,也利於解讀。
Steam Deck 的桌面模式基本上就是個 KDE Plasma 界面,拿來做不少工作很是夠用。
本文描述的幾項重點爲:
work
。
work
身份而安裝 distrobox
並開設虛擬機。
基本上直接使用 Steam OS 的「桌面模式」。在不做大幅度修改的前提之下,就可以完成不少工作事項。
在切換到桌面模式之後,右搖桿可當滑鼠來使用。R2 對應到滑鼠左鍵,L2 對應到滑鼠右鍵(注意:左右相反)。外出時接個好打的鍵盤,就可以進行不少文字工作。只是,一直低頭看小螢幕實在傷頸酸肩,最好還是準備個能將機器墊高一些的支架或站台,
這 Steam OS 提供的桌面模式有以下幾點不那麼容易克服的限制:
deck
這個帳號,並且不必輸入密碼就可以登入。
deck
帳號是 wheel
管理員群組的一員,而且 無法 爲其設定登入密碼。
由於我還是會把 Steam Deck 拿來玩遊戲,所以基本上不會去試着突破以上這幾個限制,也不會嘗試去重新安裝成慣用的 Linux。附帶一提, 有人成功在 Steam Deck 上裝了 Windows,姑且算是個選項。
在不破壞前述幾項限制的前提之下,基本上能有的選項是:
以軟體開發爲目的話,大致上會需要
flathub 上的軟體能提供文字編輯器,但要安裝其他三項都不那麼容易。因此必須使用虛擬機。
目前爲止試過能較快上手的是 distrobox -- 這基本上是依賴 podman,以及現有的 container 生態系統去快速做出新的 Linux 環境。
使用 podman 的主要原因它有提供特殊版本。讓 podman 本身可以裝在 $HOME
底下,所有執行時需要的資料等等也都放在 $HOME
底下。對於 Steam Deck 這種特殊的系統而言是十分有利的。只要能裝在 $HOME
底下,就可以活過系統更新,不必重新安裝。
以 distrobox 所準備出來的虛擬機有個不錯的特性:可以將任意目錄指定爲虛擬機內的家目錄,並且以 bind mount 方式讓內外的檔案系統直接互通。也就是說,在桌面環境下其啓動的文字編輯器也可以直接去修改 distrobox 內部的家目錄內容,只讓編譯與測試在虛擬機內完成。這需要對指令有一定的熟練度,似乎對 IDE 不太友善。但至少是個不錯的辦法。真的需要 IDE 的話,也可在虛擬機內全套裝好。
建立工作用帳號的主要目的是便於區分家目錄底下的內容。讓 deck
帳號底下的內容全都是與遊戲相關的,並讓工作用帳號底下的內容全都是與工作相關的。
雖然根目錄是唯讀的,但是仍然可以透過指令或 KDE Plasma GUI 來建立其他使用者帳號,並且也能夠活過系統更新。只是,自遊戲模式切換到桌面模式時,固定是登入為 deck
帳號,無法 爲其設定登入密碼,也無法切換成其他使用者。只能開終端機出來用 su
變身成其他帳號。但這樣也就夠了。
假設工作用帳號爲 work
,那便以 deck
身份執行以下幾行指令來建立這個新帳號:
useradd -U -m work
建好之後可用 sudo
或 su
來變身:
sudo su - work
變身成爲 work
之後,首先是安裝 distrobox。依照 distrobox README.md 內的安裝說明,執行以下指令可將 distrobox 安裝到 ~/.local
底下:
curl -s https://raw.githubusercontent.com/89luca89/distrobox/main/install | sh -s -- --prefix ~/.local
安裝好後 distrobox
這個執行檔就會被放在 ~/.local/bin
底下。
再來是安裝 podman
-- 同樣也是要安裝到 ~/.local
底下去。依照 distrbox 文件 ,使用他們提供的 install-podman
腳本就可以:
curl -s https://raw.githubusercontent.com/89luca89/distrobox/main/extras/install-podman | sh -s -- --prefix ~/.local
如此一來 podman
全體會被放在 ~/.local/podman/
,而 podman
這個執行檔則是會被放在 ~/.local/podman/bin
。
最後是調整 PATH
變數內以便日後執行。慣例是在 ~/.profile
裡加入類似以下這一行的指令:
export PATH=~/.local/bin:~/.local/podman/bin:$PATH
最後就是實際上建出工作用虛擬機。假設要使用 alpine:latest 這個 container image,並將虛擬機命名爲 workstation
,且將虛擬機內的家目錄指定爲 ~/distrobox/workstation/home
的話,就是這行指令:
distrobox create --image alpine:latest --name workstation --home ~/distrobox/workstation/home
如果不指定的話,預設家目錄就是跟 $HOME
一樣。但這樣一來虛擬機內所有程式的設定檔路徑都會跟機主 (Steam Deck) 的路徑相同。如果要開設多個虛擬機,可能會遇到設定檔或暫存檔互相衝突的問題。建議是給每個虛擬機自己一個專用的家目錄。機主家目錄 $HOME
會以 bind mount 的方式直接與虛擬機內互通。所以也不至於造成太多不便。
建好之後就可以進入虛擬機內逛逛了:
distrobox enter workstation
目前 Steam Deck 桌面環境是 Xorg,也就是說,其實也能加入一些調整,讓 distrobox 虛擬機內的 GUI 程式(如 Firefox)直接顯示在桌面環境上。
桌面環境:需要以 xhost
指令去讓跑在其他地方的 X client 可以在本機開視窗出來。以我的設定方式,所謂的「其他地方」是本機上的其他使用者。包括由虛擬機內開出來的 GUI 程式。
假設工作用帳號是 work
,那便在 /home/deck/.bash_profile
內加入以下幾行指令:
xhost +SI:localuser:work
alias work='sudo --preserve-env=DISPLAY su -w DISPLAY - work
依照這兩個月的使用經驗,DISPLAY
有時是 :0
有時是 :2
,不是很確定爲何改變。但總之只要讓所有地方的 DISPLAY
都有正確地設定好就好。方法就是在以 sudo
變身時把目前的 DISPLAY
保留下來。
如此一來便能夠在 WezTerm 內以 work
爲切換到工作用帳號的指令。切換過去後,進入虛擬機:
distrobox enter workstation
並在虛擬機內直接執行 GUI 程式指令,例如 Firefox:
firefox &
過幾秒鐘後便可以看到 Firefox 視窗開出來了。
附帶一提,由此方法開出來的 GUI 視窗也是可以配合桌面環境上的 Fcitx 輸入法框架來輸入漢字。如果有程式對輸入法切換熱鍵沒反應,那就檢查看看 LANG
環境變數是否有設定爲 zh_TW.UTF-8
。有些程式不一定需要 LANG
爲 zh_TW.UTF-8
,只要是某個有效的值(有對應到存在於系統上的 locale
資料),設定爲 en_US.UTF-8
或許也可以。
Steam Deck 桌面模式就是 KDE Plasma 圖形化介面。但出廠附上的輸入法設定是 ibus 框架的,其內附的漢語拼音等輸入法對我個人而言不太合用。
於是,參考 BrLi 的方法 後裝了 fcitx 框架,並且修改了相關設定,讓進入桌面模式後,就能直接起動 fcitx 而且不會起動 ibus。重點步驟如下:
~/.config/plasma-workspace/env/input.sh
這個檔案,貼入如後內容。
讓 fcitx5 自動起動的方式是把其起動腳本複製一份到 ~/.config/autostart
這個路徑底下去:
cp /home/.steamos/offload/var/lib/flatpak/exports/share/applications/org.fcitx.Fcitx5.desktop ~/.config/autostart/
而 ~/.config/plasma-workspace/env/input.sh
內容為:
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
export XIM=fcitx
如此一來在進入桌面模式後,於右下角工具列上會出現一個 Fcitx 的圖示。會顯示目前輸入法的狀態,並且可以透過點擊此圖示來設定各項細節。
核心的輸入法方面,為了配合手邊的 40% 鍵盤,我目前主要是使用 RIME 中的朙月拼音來滿足輸入漢字的需求。但想必也能接上其他種類的輸入法。
目前在購買遊戲 DLC (主要是原聲帶)後似乎無法於 Steam Deck 遊戲模式界面底下播放,但可以在進入桌面模式後,開桌面模式的 Steam 程式、找到遊戲 DLC 頁面,然後使用其內建的播放器來播放。
不過、在 Steam 程式界面內逛索,在找到 DLC 在的位置就會令 GUI 路癡迷路了。如果能夠順利找到 DLC 頁面,下載完畢之後可以按下「瀏覽本機檔案」按鈕,將音樂檔案複製到別的地方,給慣用的 VLC 播放器去播放。
稍微調查了一下後發現如果是遊戲原聲帶,其實下載位置必定是在以下兩個路徑底下:
我的 SD 卡的路徑名爲 /run/media/mmcblk0p1,不確定這個名稱是否固定,或許換張卡就會改變。
初步看來是這兩個路徑底下還會依照 DLC 名稱分資料夾出來。來自不同 DLC 的音樂檔案不會混合在一起。
接下來就是開 VLC ,並把那兩個路徑直接加到 Media Library 中。VLC 會自行掃描整個路徑底下、可以自動找到所有音樂檔案。也可以省下在 Steam GUI 中迷路的時間。
一陣子之前看到的。 cpan-outdated 這個指令,能列出所有可升級的模組。
它的輸出格式看來是 CPAN Mirror 底下的路徑。可以直接餵給 cpanm 去安裝。
# cpan-outdated | head
P/PL/PLICEASE/Alien-Build-2.76.tar.gz
P/PL/PLICEASE/Alien-Libxml2-0.19.tar.gz
T/TO/TOKUHIROM/Amon2-6.16.tar.gz
R/RJ/RJBS/App-Cmd-0.335.tar.gz
O/OA/OALDERS/App-perlimports-0.000049.tar.gz
R/RJ/RJBS/App-Uni-9.006.tar.gz
P/PJ/PJACKLAM/bignum-0.66.tar.gz
L/LE/LEEJO/CGI-4.55.tar.gz
G/GA/GARU/Clone-0.46.tar.gz
P/PM/PMQS/Compress-Raw-Bzip2-2.201.tar.gz
此外既有的 cpan
指令也能做到類似的事:執行 cpan -O
後,會列出各模組的目前版號以及最新版的版號。
# cpan -O | head -15
Reading '/home/gugod/.cpan/Metadata'
Database was generated on Thu, 05 Jan 2023 00:54:02 GMT
Module Name Local CPAN
-------------------------------------------------------------------------
CPAN: Module::CoreList loaded ok (v5.20220520)
Acme::CPANAuthors::Austrian 1.1318 1.1318
Alien::Base 2.5100 2.7600
Alien::Base::PkgConfig 2.5100 2.7600
Alien::Base::Wrapper 2.5100 2.7600
Alien::Build 2.5100 2.7600
Alien::Build::CommandSequence 2.5100 2.7600
Alien::Build::Interpolate 2.5100 2.7600
Alien::Build::Interpolate::Default 2.5100 2.7600
Alien::Build::Log 2.5100 2.7600
Alien::Build::Log::Abbreviate 2.5100 2.7600
但看來 cpan -O
列出的是 package 而 cpan-outdated
列出的是 distribution。
不過也可執行 cpan-outdated -p
,讓它改列 package 名。
如果只是要無條件全部升到最新版的話,可以執行 cpan -U
或是 cpan-outdated | cpanm
。
2022 這一年度的變化還真是不少。
放假與觀光:稍微做了些簡單的旅行。多是一天就結束的,但也有幾次比較長期的。相對與去年與前年是多了幾回,但對於 COVID-19 的盛行還是放心不下,依然是小心翼翼地在外出。也由於一直沒有長期旅行而累積了不少休假日,而決定以「每個星期都請一天假」的方式來消化那些假日。讓自己連續四、五星期都是週休三日。並且把休假日放在週一或週五。放在週一的好處是可以去電影院,並且在完全沒有人潮的狀態下看電影,然後在同樣是沒有人潮的狀態下逛街。其實也是個消解壓力的不錯的方式。
舊物清理:在回到父母家時順便把閒置著的房間努力地大掃除了兩遍。除了掃去厚厚的灰塵以外,也把很多東西脫手了。一來是把不少擺到變成電子廢棄物的舊線材與舊器材等全部翻出來送去資源回收,二來是把一些還可以用的拿去給二手商店代售。賣幾多錢都無所謂,既然是還能用的東西,與其直接丟棄,不如讓給下一個使用者來用看看。有好幾項是十歲以上的舊蘋果電腦產品,拿去 Apple Store 回收是連店員也沒看過,變成好像我在給 Apple Store 店員展示 Apple 產品的狀態。
拜見甘露水:撥空前去欣賞了「甘露水」以及那代的藝術家們的作品。也拜訪翻修完成了的嘉義市立美術館欣賞陳澄波作品,以及那棟名建築。雖然僅是不太深入地看看,但似乎也能讓腦袋受到刺激與啓發。雖然這只是一年 365 日之中僅僅兩個半天的事情,但都是令人印象深刻的展覽。定期逛逛美術館,主動去看看那些美麗的靜態螢幕保護程式(?),似乎也有助於把長期佔在腦內的那些雜物給沖走,將腦袋中的「桌面圖示」一次清光。
主力個人工作電腦的改變:在歷經三次修理之後,自 2013 年左右用到 2021 年年底的 Macbook Air 總算是壞到不能更壞了。這次連開機過程都進不去,電源啓動後播放出 Sosumi 音效後就一直停在黑底白蘋果畫面,成爲只能顯示蘋果商標的機器了。努力嘗試救援後發現還能開進 Target disk mode 讓我把資料盡數救出,算是幸運。自然,這本筆電也是送去給 Apple Store 回收了。自回收之後,一直還沒有決定是否要再買筆電而就這麼過了大半年,一直都是以一台 Raspberry Pi 4 當做個人用工作機。以寫文章與簡單的開發而言還算夠用。雖然在瀏覽網頁這件事上勝不過 Android 手機(爲何這年頭網頁用起來如此的重歪歪?),但如果儘量不去開瀏覽器「閒逛」,倒也能完成不少事情。遲遲沒有再買筆電的原因之一是沒有需求:在外出時間減到最低的狀態下,根本不會像以前那樣拿著筆電去咖啡館使用。原因之二是我預期要把 SteamDeck + Cutie Pi 拿來當成工作機組合來用。只是不巧筆電提早壞去了。在年底收到 SteamDeck 後花了不少時間在弄懂如何在上面設置一套適合自己的工作環境。雖然 SteamDeck 有提供桌面模式,但有不少眉眉角角的地方得要注意。好在自己早已習慣於純文字工作環境,就算不把 SteamDeck 桌面模式徹底改掉,開個可以打指令的終端機出來其實也就夠用。但不得不讚歎一下:Valve 還真是把 SteamDeack 各處弄得妥妥善善的啊。
隨著前述工作機器上的變化,我決定開始重新定義一下自己與電腦之間的關係。由於公司政策改爲鼓勵遠端工作已經兩年,似乎也很有可能在 COVID19 盛行「宣告」結束之後繼續維持讓多數員工遠端作業,可以想見在家時間一直都會很長。偶爾會發生無意識地把電腦打開來,卻沒有要先想好要完成甚麼工作,變成常常有坐在電腦前面 30 分鐘,卻好像只是在檢查郵件、Slack、GitHub 通知,把四處出現的紅點通知給消除了,而實質上卻是只讀進而不產出,好像甚麼都沒有完成的這種狀況。由於這直接影響到了工作效率,必須想辦法改善纔行。自己想到的方法,是讓自己在開電腦時必定要完成一些事項,並且在完成後立刻把電腦關機。不是進入睡眠模式,而是關閉電源。在開電腦之前,先把此刻要完成的事項寫在面前的筆記本上,並且時刻意識到動作步驟。例如,先想好要改 BUG-123 要開哪幾個檔案,大致上要改那幾個函式,要補充那些文件之類等之類的這類動作。然後開電腦後就依照剛剛想好的步驟照做。這其實頂麻煩的,而且效果並不立竿見影(還是很容易分心),但似乎無意間增強了在腦內寫程式的能力。一定程度上這也直接影響到筆記這件事。既然開電腦的時間減少了,手寫筆記的次數就增加了。但我反正本來就是以簡單的子彈筆記的形式用紙筆在做工作追蹤與記錄的,頂多就只是筆記本消耗速度變快了一些,並不是一件很不習慣的事。
刻意增加看書時間:無論是紙本書或是電子書。由於住處附近不遠處就有圖書館,因此就每兩週去借個幾本書來翻。但這方面是完全沒有替自己設定「業績」。就算是借回來放在桌上放到最後一頁都沒翻,也不要讓自己覺得愧疚。可能是由於有這樣的心態建設,反而讓每本書都有進度。雖然實質上每本書都沒有 100% 讀完,有不少只細讀了 5% 左右,但是至少都有快速翻過,有把握住大綱。這種不求精實度的懶散做法似乎不錯,至少讓我能夠開始花比以前更多的時間在讀書這件事情上,而有個起頭後,如果真的因那起頭的 5% 的內容而對書本省下的 95% 產生了興趣,那就自然而然會多讀幾頁。內容類型不限,去圖書館當天想看甚麼就借甚麼。一定程度上也有助於將自己的舒適圈擴得更大。這刻意看書的習慣,也有助於減少 SNS 的無意識的使用。如果讓看書、滑 SNS、開電腦等日常的做爲都變成是有意識的行動,那麼自然就沒有時間會在無意間消失了。
隨著電腦使用習慣的改變而多出來的那麼一點點時間,拿來稍微翻幾頁書,也算是不錯的。如果順便有撿到一些名言佳句那就令人高興,但就算沒有,也不會太在意。這似乎有點像是在看 YouTube 影片的時候以略看縮圖的方式挑片段看。先快速瀏覽,抓個大意,如果有碰到似乎有點意思的片段,再放慢速度。
認養小貓兩隻:兩隻都是從幼貓開始養。看著小貓從快速的在三個月時間內從幼小體型長成接近成貓體型,實在是不得不讓人再次覺得:人體成長期真是沒有效率啊。同時也覺得:幼貓用手來抓東西的動作真是十分靈活,只要是碰觸得到的到物件大概都會被當成足球(手球?)撥來撥去。或許真的可以期待貓科動物演化出對向指,讓人類在進化這件事情上開始退休。
重新認識住家週邊環境:既然不打算遠行,也不必通勤上班。那就把通勤時間拿來在住家周圍散步。並且偶爾以步行走三、四公里到稍微有點距離的地方探險一下。即便只是週末專門步行到某間餐廳去喫一頓就回家,把原本只要 15 分鐘車程的事花一個下午的時間來做,讓過程大於結果。挑各種不同的小路走,觀察路上的變化,是一件頂有意思的事。說不定還無意讓玩 geoguesser 的能力提升了一些。
部落格文章的舊去新來:把部落格站上一些舊到變得很好笑的文章給清除了,也把近年來對 Raku 語的學習整理了一下,以「Raku 如何如何」這種形式做了一些文章。有部分只是把過去以 Perl 6 爲題的文章重新編譯過而已。在內容方面只能算是自己的學習筆記,說不上完整。不得不說 Raku 語真的是一門很容易學的語言,各種表示式很一致,幾乎沒有甚麼怪怪的地方,算式形式雖然多,但語法規則卻不多,只要遵循幾條規則就可以看懂大部分的程式碼,雖然跟其他語言命名慣例未必一致,內建的各類別語方法的名稱也很好記。學習 Raku 之外,偶爾也稍微學習一些其他新舊程式語言與一些數學知識。
林林總總地列舉了不少,光是坐在桌前就可以回憶出這麼多重要變化,可見 2022 年真的是很重要。(雖然每一年都是一樣重要的啦。)
幾年之前我開始把例假日的列舉做成模組、發佈到 CPAN 上面去。也就是名爲 Date::Holidays::TW 這個模組。這個模組基本上是帶有簡單 API 的資料集,能讓人查詢某年某月某日是否爲國定假日。所參考的資料來源有兩項:
一是行政院人事行政總處的「政府行政機關辦公日曆」,也就是在這個網址裡可以找到的某個頁面: https://www.dgpa.gov.tw/informationlist?uid=30
二是在政府開放資料平台上所找到、名爲「中華民國政府行政機關辦公日曆表」的這個資料集: https://data.gov.tw/dataset/14718 。這資料集的格式是 CSV 格式,因此主要是以此做爲資料源,再以辦公日曆來對答案。
而維護這模組主要的難處在於中華民國政府所定義的假日並不容易以計算的方式得知。因此無法製作單一一套演算法來一勞永逸。必須每年花點時間將資料匯入。而這之所以不容易計算,乃是因爲其規則有部分曖昧成分。
以下將中華民國一百一十二年政府行政機關辦公日曆表所節錄的放假與補假規則稍微摘要後列出:
資料內容主要是計算出規則 b 內各項紀念日及節日所對應到的西曆日期。除了有三項爲農曆日期之外,其中「清明節」本身的計算需要將一農曆年等分,或是使用各數學家推出的近似公式解。
只是,前述假日規則之外還有個「補班」規則,也就是有幾個原本爲放假日的星期六有可能會被變爲上班日。這補班規則是列在 政府機關調整上班日期處理要點 的第五條:
五、因應連續假期所為之上班日調整,除特殊情形者外,以提前於前一週之星期六補行上班為原則。
也就是說,以前述規則 a、 c、 d 所做出來的補假日或放假日(通常是星期一或星期五)所形成的連續假期,會讓其之前一週的星期六有可能會變成上班日,或稱「補班日」,但也有可能不會。在規則上以「特殊情形」這個用詞留下了一些曖昧空間。
再者,由於出現上班日,使其可能會成爲假日規則中的「前一個上班日」與「次一個上班日」所指涉的目標。也就是說這幾條規則的套用次序會直接影響到結果。而任何人獨自實做出來的程式碼,對於前述幾條規則的解釋與規則套用次序的理解,都有可能不同於行政院人事行政總處的解釋。
比方說以 2023 年假日的幾個補班例子來看,若以 中華民國一百一十二年政府行政機關辦公日曆表圖檔 爲準,可看到 6/17 星期六爲上班日,而其次星期的 6/23 星期五爲假日,因爲 6/22 爲端午節,是假日。這部分就符合補班規則裡「提前於前一週之星期六補行上班」這半句的描述。
但若看到 3/25 星期六,也是上班日,而且其次星期一 (3/27) 到星期五 (3/31) 則全是上班日,反而是八天後的 4/3 星期一爲放假日。可知這一日的補班,並非是「於前一週之星期六」,而是「於前二週之星期六」了。可另於 2/18 及 9/23 看到類似狀況。或許這可算是某種「特殊情形」。
由 2023 年的這幾個例子來推敲,似乎可得知實際的規則如下:如果某星期一被調爲放假日,則其八天前的星期六應被調爲上班日。而若某星期五被調爲放假日,則其六天前的星期六應被調爲上班日。
但若看到一月份:1/7 星期六爲上班日,而與其對應的放假日應爲 1/27 星期五。1/27 被調整爲放假日,但其六天前的週六爲農曆除夕,本爲假日、不應該被調爲上班日。因此,或許與 1/27 對應的補班日爲 1/14 星期六。但是我的推算與行政院人事行政總處不同。顯然這就是規則中所謂的「特殊情形」了。
所以爲了求模組的正確性,就不能只依靠計算,還必須定時把公佈的假日表複製一份到 Date::Holidays::TW
這模組裡面去才可以。看來做月曆或手帳的業者也是無法在公佈之前自行計算出正確的補班日,每年都要手動進行這一例行公事才行。
附帶一提,似乎從沒看過這表內出現任何由原住民族委員會所定義的假日。
先參考這影片: https://youtu.be/g8xXrhjqOZM
在這 2017 年 The Perl Conference Amsterdam 的演講中,於 19:04 處,Damian Conway 開始描述這個重複出現的 code pattern (Raku 語。當時仍名為 Perl 6):
@tape[$head] = .<write> if defined .<write>;
$state = .<state> if defined .<state>;
這個 .<write>
語法是從名爲 $_
的 HashMap 把對應到 "write"
的值取出來的意思。
這兩行則是在意圖要描述「在某個值為不為未定義時,才將某個值存到某變數中」。這個代表「某個值」的算式必須在同一行陳述裡面出現兩次。是重複出現的。
接着他以定義新算符 ?=
的手法,將其重構為:
sub infix:< ?= > ($lval is rw, $rval) { $lval = $rval if defined $rval; }
@tape[$head] ?= .<write>;
$state ?= .<state>;
關於這個 ?=
算符,雖然這符號的外觀有點像是 js / kotlin 中的 safe-navigation ?.
,但兩相比較起來,?.
的測試對象在其左方,而 ?=
的測試對象是在其右方,也就是算式的閱讀方向正好相反。有點像是主語位置變到句尾去的感覺。但對於已經習慣 ?.
語意的人而言可能要稍微花時間適應一下。
一般來說賦值算符算式的方向都是從右到左。計算右邊的算式所得到的值,會被存到左邊的變數內。但例如 TI-BASIC 這程式語言,其賦值算式是從要左至右讀的:
42 → A
A + 1 → B
在 Raku 語,可自行定義新算符來實做出這種方向是自左至右的算算符。例如以下的 assign-to
:
sub infix:< assign-to > ($lval, $rval is rw ){ $rval = $lval }
42 assign-to $a;
say $a; #=> 42
那麼基於類似概念而定義出來的 ?assign-to
算符,在閱讀方向上就與 safe-navigation 相同了
sub infix:< ?assign-to > ($lval, $rval is rw ) { $rval = $lval if defined $lval }
$a ?assign-to $b;
如果要去把「$a ?assign-to $b
」這一句給唸出來的話,就類似:
$a 的內容如果不是未定義的話,就存到 $b 裡面
或許對於已經讀慣 safe-navigation 的人而言,是一種比較親切的 code pattern。
所謂訛態,指的是程式執行期間所發生的「某種已知的錯誤」。例如:http client 函式在連接到指定網站時,發現網站的名稱無法解爲 IP 地址。名稱解析失敗是一種在程式設計期間就可以預想到會發生的錯誤,因此這種錯誤應該要被妥善處理。既然要被處理,那就表示與其相關的所有資訊都應當被暫時保存在記憶體檔中,才能被傳遞到其他地方去處理。
訛態處理手法不外乎:重試三遍、或顯示一些訊息在 UI 上、或直接無視。而如何將訛態從其發生地點傳遞到其處理地點去,基本上有兩派:一派是用程式語言提供的 Exception,另一派則是將訛態視爲函式回傳值的一部分,也就是所謂的 Status Return。
在 Exception 與 status returns 的這個題目上有不少好文章可以讀。跟 Emacs vs vi 一樣是個長年爭議題。各有優劣,但沒什麼固定答案。
以下是幾則舊文的結論摘要。:
=> https://www.joelonsoftware.com/2003/10/13/13/
由y Joel Spolsky 提出:
Status return 較好,因爲:
=> https://nedbatchelder.com/text/exceptions-vs-status.html
由 Ned Batchelder 提出:
Exception 較好,因爲:
=> https://www.perl.com/pub/2002/11/14/exception.html
由 Arun Udaya Shankar 提出:
Exception 較好,因爲:
這兩個新世代語言內,都沒有 Exception,而是採用 status return。
只是,語言中內建的 panic 函式能帶著錯誤訊息立即跳離目前函式。這函式的用法就跟丟出 Exception 是一樣的:行到 panic
處,無條件離開。只是 caller 端的收到訛態後的處理方式與處理 Exception 不同。
上一層的 caller 必須要寫幾行算式去來應對,要不就是明確表示要無視,要不就是要明確地寫出處理辦法。也無法在某個更上層的函式內做一個「錯誤處理中心」自動處理目前 call stack 層層下好發生的錯誤。
或許由不少人認爲這套辦法也可以算是一種「輕量的」Exception,但就處理機制而言,更像是 status return。
一定意義上或許這表示 status return 較好。(或是:沒有 Exception 也沒差。)
Perl 語言內要丟出 Exception 是以 die
這個內建函式。這個函式本身不會有傳回值,而是在執行時會讓以下這幾件事情依序發生:
$@
這個全域變數中。
$SIG{__DIE__}
處理函式存在的話,使其執行。
eval
領域」(可能在 call stack 中好幾層上面),使其傳回 undef
。
eval
領域,perl 預設的處理方式是將 $@
輸出至 STDERR
也就是說 Perl 語言內的 Exception 的處理,能由很上層的某個函式全數包辦。程式中也不必明確地將 Exception 重複再以 die
傳遞給上層,因爲這會自動發生。
一定意義上最最「省事」的做法,就是在進入點內寫個 eval { }
區塊,把所有算式都包入,並且,在所有其他函式內都不寫 eval
。這麼一來,在各處發生的 Exception,都可被最上層的 eval 領域捕捉到,並且統一處理。雖然這招有很多缺點,不一定四處都通用。但如果程式碼任務範圍內不容許任何 Exception 被忽略,發生錯誤也不必重試,那這個做法確實可以讓任務本體程式變得十分簡潔。因爲所有處理訛態的程式碼都被寫在別的地方了。
Perl 語言內要丟出 Exception 是以 die
這個內建函式。透過 Try::Tiny
提供的語法,可以簡單地做出一個轉換器:
use Try::Tiny;
sub error_or_result (&) {
my ($f) = @_;
try {
undef, $f->()
} catch {
$_
}
}
用法:
my ($err, @ret) = error_or_result { foo() };
如果 foo()
執行途中以 die
傳 Exception 出來,那麼 $err
內會裝有錯誤內容,而 @ret
則會是空的。如果沒有錯誤發生,那麼 $err
會是 undef
,而 @ret
會裝有 foo()
的傳回值。
也就是它會把 Exception 轉換成 status returns。如果你用了一些函式庫是以 Exception 來將訛態丟回來,但慣例上專案程式碼內是使用 status return,那麼將訛態的使用轉換成一貫的慣例,或許有助於讓同組組員都能夠快速讀懂。
尚有一派做法是把訛態對應到某種空值。最常見的使用時機是在呼叫外部 API 時。如果外部 API 所帶進來的資料不是必需的,是「有顯示可能有用,但不顯示出來也沒有關的」的這種狀況,那麼當這外部呼叫出錯時,與其將訛態顯示給使用者看,不如將其視爲「成功地取得了零筆資料回來」。也就是直接當成是跟「沒有資料」是一樣的狀態來處理。
當然,實務上我們會希望外部呼叫出錯時,會要在記錄檔裡面留下一些資訊,讓日後能夠分析。
這個題目真的就是個優缺取捨題:若因 A 的優點而選了 A,同時也要將 A 的缺點全數概括承受下來。
如果把程式的任務範圍限制在 Web app 來討論的話,以個人觀察及參與所得到的經驗而言,選用 Exception 的好處前三名大致上爲:
goto
一樣,但由於標的都只限制在 30x、40x、50x 這十幾種不同的狀態,其實反而讓語意更加清晰好懂。
同時,缺點的前三名爲:
只是我必須在此說明:所有我參與過的 Web app 專案都中都用上了 Exception,也就是說我沒有實際上只用 status returns 就完成一整套 Web app 的經驗。或許這幾點觀察都算是有點偏心了。但若真要問我偏向那一邊,我又不是很確定。
顯然,在語言有支援 Exception 機制的工作環境中工作久了,必然就會碰到需要將兩者「混搭」的狀況。或許,能夠讓程式設計師自由轉換的設計,纔是夠便利的。
雖然維基百科上說 Fizz Buzz 是會出現在程式設計師的面試考題,但現在應該沒有甚麼公司真的還在用這個題目來做篩選了吧 😎
姑且還是先把題目定義一下:
請提供一個函式,能將數字 1 至 100 逐一印出,但若碰到 3 的倍數則改印出 Fizz
,若碰到 5 的倍數則改印 Buzz
,碰到同時爲 3 與 5 的倍數時則改印 Fizz Buzz
。
直觀解之一就是依序檢查數字是否爲 15、5、3 的倍數,然後做將數字換成不同的字串。檢查順序會影響結果,15 要先檢查,但 3 和 5 兩者的檢查順序則不影響結果:
for 1..100 -> $n {
# [3]
say do given ($n) {
when $_ %% 15 { "Fizz Buzz" }
when $_ %% 3 { "Fizz" }
when $_ %% 5 { "Buzz" }
default { "$_" }
}
}
在此提供一個重構模式。那就是把「3 的倍數」、「5 的倍數」視爲兩種型別,而「15 的倍數」則可以由前兩者合成.
# [1]
subset Fizzer of Int where { $_ %% 3 }
subset Buzzer of Int where { $_ %% 5 }
# [2]
subset FizzBuzzer of Fizzer where Buzzer;
for 1..100 -> $n {
say do given ($n) {
when FizzBuzzer { "Fizz Buzz" }
when Fizzer { "Fizz" }
when Buzzer { "Buzz" }
default { "$_" }
}
}
[1] 處是定義兩個 Int
的子集合,Fizzer
爲 3 的倍數,Buzzer
爲 5 的倍數。
[2] 處定義的 FizzBuzzer
是「Fizzer
的子集合中,同時又是 Buzzer
者」。也就是同時爲 3 與 5 的倍數的 Int
。
[3] 處的 given...when
的這段,則是改用方才定義出的三種新的 Int
子集合來改寫。順序一樣是要注意的。
或者,可以不必使用 FizzBuzzer
,而是將 when FizzBuzzer
的改爲 when Fizzer & Buzzer
:
for 1..100 -> $n {
say do given ($n) {
when Fizzer & Buzzer { "Fizz Buzz" }
when Fizzer { "Fizz" }
when Buzzer { "Buzz" }
default { "$_" }
}
}
此處的 &
爲 Junction 算符。表示「前後兩方的條件都要滿足」之意。when Fizzer & Buzzer
就相當於 when ($_ ~~ Fizzer && $_ ~~ Buzzer)
,也就是 $_
必須滿足 Fizzer
的條件,同時必須滿足 Buzzer
的條件。
顯然以解 Fizz Buzz 這個練習題來說,任何重構都算是多餘的工程技術了。除了練習以外,沒有別的目的。這種自定型別用在檢查使用者輸入的時候非常的合用。尤其當條件稍微複雜一些的時候,若能將判別式拆解成「幾個集合的交集」這種形式,顯然能在讓程式碼更加容易閱讀。