[Perl] 以 Monad 來達成 safe navigation
作者:gugod 發佈於: ,更新於: #perl #monad在 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 三個方法內發生了任何例外事件,程式還是能正確地被中止。並不會有太多副作用。在本文內提到的三種改寫辦法裡面,算是既通用,又不太囉唆的一種。