Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

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

ListモナドMaybeモナドに続いて、今日はIOモナドのお勉強をしてみます。

簡単な解説

IOモナドは実質的にStateモナドなので、こちらとかこちらとかを読んで下さい。

ただし、状態は「世界」となります。世界によって値は異なり、新しい世界(副作用が施された世界)を返します。

実装

Stateモナドと同じ定義をしました。

my $io_monad = Monad->new(
	T_arrow => sub {
		my $arrow = shift;  #  A ->  B
		return sub {        # TA -> TB
			my $tx = shift; # TA
			return sub {    # TB
				my $world = shift;
				my ($value, $new_world) = $tx->($world);
				return $arrow->($value), $new_world;
			};
		};
	},
	eta => sub {
		my $value = shift; #  A
		return sub {       # TA
			my $world = shift;
			return $value, $world;
		};
	},
	mu => sub {
		my $ttx = shift; # TTA
		return sub {     #  TA
			my $world = shift;
			my ($tx, $new_world) = $ttx->($world);
			return $tx->($new_world);
		};
	},
);

IOモナドを使うための関数

副作用を持つ関数を、通常の値を受け取ってIOモナドの値*1を返す関数として定義します。

1. print文

undef を表す IOモナド値 を返す関数として定義します。このIOモナド値は、世界を「printが施された状態」に変化させます。ただし、RealWorld値をどう実装すべきかわからなかったので、世界を名前(WORLD、等)として実装しました。副作用を施すと新しい世界になるので、この名前をWORLD' とかとします。*2

sub io_print{  # A -> T A
	my $x = shift;
	return sub {
		my $world = shift;
		print Dumper $x;
		return undef, $world . q(');
	};
}
2. 引数を読む

コマンドライン引数は世界の状態によって変わるのでIOモナド値で表されます。この値は世界に依存しますが、参照しても世界は変更されません。

なお、引数$xはdo表記を楽にするためのダミーです。

sub io_args{  # A -> T A
	my $x = shift;
	return sub {
		my $world = shift;
		return \@ARGV, $world;
	};
}
3. ファイルを読む

2. と同様です。*3

sub io_readfile{
	my $x = shift;
	return sub {
		my $world = shift;
		open my $fh, '<', $x;
		my $val = do {local $/; <$fh>;};
		close $fh;
		return $val, $world;
	};
}

使い方

準備ができましたので、使ってみましょう。引数に渡されたファイルを読み、そのファイルに含まれるバイト数を数えてみます。

まず、第一引数を取り出すための関数とバイト数を数えるための関数を用意します。どちらも普通の関数です。

sub fst{
	my $ref = shift;
	return $ref->[0];
}

sub size{
	my $str = shift;
	return length $str;
}

次に、前回のdo表記で処理を作ります。これは全てIOモナドの世界に持ち上げられ、 T A -> T B と言う関数になります。

my $task = do_
	\&io_args,                       # 引数を得る
	( comp $io_monad->eta, \&fst ),  # 最初の引数を使う
	\&io_readfile,                   # ファイルを読む
	( comp $io_monad->eta, \&size ), # サイズを数える
	\&io_print,                      # 出力する
;

この関数を評価して値を得ます。これが、Haskellのmainであり、アプリケーションはひとつのIOモナド値として表されます。IOモナド値は環境によって値が決まり、環境を変化させる値なので、我々がアプリケーションに持っているイメージと一致しますね。

なお、最初に渡す値はundefを表現するIOモナド値である eta->(undef) としました。

my $main = $task->( $io_monad->eta->(undef) );

では、このIOモナド値は今我々が居る環境ではどういう値として具現化されるのか、評価してみましょう。最初に書いたように今回は世界を名前として扱ってますので、'WORLD'と言う値を今の我々の世界につけたものとして、これを渡してみます。

my ($value, $world) = $main->('WORLD');
print "value: ", $value, "\n";
print "world: ", $world, "\n";

この結果は、

% perl io_monad.pl io_monad.pl
$VAR1 = 1650;
Use of uninitialized value in print at io_monad.pl line 104.
value: 
world: WORLD'

となりました。渡したファイルのサイズは、1650 byteでした。

実はアプリケーションに対して我々が期待するのは、その値ではなく世界の変化です。結果は、画面に表示されて我々の目に入ってきます。つまり、画面の更新がアプリケーションに求めているものです。これを反映するように、値としてはundef値、そして、【ファイルサイズが表示された新しい「WORLD'」と言う世界】が返ってきました。

感想

IOモナドも、こうして実装してみると決して黒魔術じゃないってことがわかりますね! IOモナドがなぜ副作用を自然に表現できるのかってのはまだよく理解できてないのですが、

  • 実際に評価されるIOモナドはmainに渡った一つのみ
  • mainにバインドさせるには、 >>= で合成して一つの値にまとめなければいけない
  • IOモナド値 の演算は可換じゃない (x y ≠ y x)


等が関係しているのではないでしょうか?

*1:T_object を施した対象に含まれる値。

*2:haskellとかでどう実装されているかは不勉強でわかりません。ごめんなさい。

*3:ファイルシステムを利用するので変更されると実装すべきかもしれないが、今回はそうしてない。