読者です 読者をやめる 読者になる 読者になる

北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

Perlでモナドを学ぶ - Maybeモナド編

Maybeモナドの実装です。

簡単な解説

Maybeモナドの関手のT_objectは、渡された集合に対して、すべてをリファレンス化した集合にundefを加えたものに移します。こうすることによって、元々持っていた値を全て表現すると同時に新しいundefと言う値を手に入れられます。undefがHaskellのNothingとなります。

実装

リファレンスをとる処理がうざかったので、ヘルパー関数にまとめました。こいつの名前をjustにしたので、Haskellっぽくなりました*1

sub just {
	my $value = shift;
	return \$value;
}

my $list_monad = Monad->new(
	T_arrow => sub {
		my $arrow = shift;  # A -> B
		return sub {
			my $tx = shift; # TA -> TB
			defined $tx or return undef;
			return just $arrow->($$tx);
		};
	},
	eta => sub {
		return just shift;
	},
	mu => sub {
		my $ttx = shift;
		defined $ttx or return undef;
		return $$ttx;
	},
);

使い道

途中で失敗する可能性のある処理を繋げるのに便利です。まともに書くと長くなるので、do_ と言うショートカットを準備します。こいつは関数をflat (A->TA を TA -> TAに) にして、逆順で合成してくれます。(flat g) . (flat f) = do_ f, g のイメージです。

sub do_ {
	my @f = @_;
	my $composite;
	while( my $f = shift @f ){
		$f = $maybe_monad->flat->( $f );
		if( $composite ){
			$composite = comp $f, $composite;
		}else{
			$composite = $f;
		}
	}
	return $composite;
}

do_ があれば、こんな感じで書けます。

sub get_number {
	shift =~ qr{(<span.+?>.+?</span>)} or return undef;
	return just $1;
}

sub cut_tag{
	my $html = shift;
	$html =~ s|<[^>]+>||g;
	return $html;
}

sub parse_number {
	shift =~ qr{^(\d+)/(\d+)$} or return undef;
	return just [$1, $2];
}

sub div{
	my ($n1, $n2) = @{ $_[0] };
	return undef if $n2 == 0;
	return just $n1 / $n2;
}

my $parse_rate = do_ \&get_number,
                     ( comp $maybe_monad->eta, \&cut_tag ),
                     \&parse_number,
                     \&div;

print Dumper $parse_rate->(just <<__HTML__);
<span id="number">3/10</span>
__HTML__

print Dumper $parse_rate->(just <<__HTML__);
HOGEHOGE
__HTML__

print Dumper $parse_rate->(just <<__HTML__);
<span id="number">not a number</span>
__HTML__

print Dumper $parse_rate->(just <<__HTML__);
<span id="number">100/0</span>
__HTML__

__END__
【結果】
$VAR1 = \'0.3';
$VAR1 = undef;
$VAR1 = undef;
$VAR1 = undef;

HTMLのパースや数値のパース、割り算と3カ所失敗所があるにもかかわらず、条件分岐せずにそのままつなげることができてしまいます。もうちょっとシンタックスシュガーを加えれば、すごく便利な機能になりそうです。なお、cut_tagだけは関手で移される前の世界の関数なので、etaを使ってMaybeモナドの世界に引きずりこんでます。

感想

Listモナドの時のテストは、関手を施した先の値(リスト or just)を使う関数と値を書き換えるだけで、実はそのまま使えてしまいます(diff参照)。それなのに、Listモナドと全然振る舞い方が違うのが、モナドの表現力の高さを物語ってます。

33c40
<       my $v = [ {key1 => 1, key2 => 2},  {key1 => 3, key2 => 4}, ];
---
>       my $v = just 123;
49c56
<       my $v = ['abc', 'defg', 'hijklm'];
---
>       my $v = just "abc";
78c85
<       my $v = [[ 'perl', 'python', 'haskell' ]];
---
>       my $v = just just 'maybe';
95c102
<       my $v = [ [ [1,2] ], [ [3, 4], [5, 6] ] ];
---
>       my $v = just just just [1,2,3];
112c119
<       my $v = [1, 2, 3];
---
>       my $v = just {key => 100};
127c134
<       my $v = [2,3,4];
---
>       my $v = just "what";
137c144
<       my $f = sub { [ ( 0 ) x $_[0] ] };
---
>       my $f = sub { $_[0] != 0 ? just 100 / $_[0] : undef };
150,151c157,158
<       my $f = sub { [ split / /, shift ] };
<       my $g = sub { [ split /,/, shift ] };
---
>       my $f = sub { $_[0] > 0 ? just 100 / $_[0] : undef };
>       my $g = sub { just 50 + $_[0] };
155c162
<       my $v = ["ABC DEF", "GH, IJK", "LM N,OP"];
---
>       my $v = just 10;

*1:sub nothing{ undef } とかするともっとHaskellっぽくなるけど、あえてやってない。