参照透過性ふいたwww

不完全にしておよそ正しくないプログラミング言語小史
1990 ? サイモン・ペイトン・ジョーンズ、ポール・フダック、フィリップ・ワドラー、デミ・ムーアの夫、ならびに動物の倫理的扱いを求める人々によって構成される委員会により、遅延評価を行う純粋な関数型言語Haskellが作られる。副作用の制御に使われるモナドの複雑さのため、Haskellには抵抗を持つ人々がいる。ワドラーは批判を和らげるために、こう語っている。「モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?」

「何か問題でも」と言われた場合は、自分の問題にして解いてみることにしている。

というわけでIOモナドをJSとPerlで書いた上、モナドはどんな問題を解決しているのか解説してみることにする。

例題

具体的には、これをやってみることにした。

#!/usr/bin/runhugs
import System.Time
-- pi = 4 * atan2 1 1
main = do
  t <-getClockTime
  putStrLn $ show t
  putStrLn $ show pi

これをdo記法なしで書くとこうなる。

#!/usr/bin/runhugs
import System.Time
main = getClockTime >>= (putStrLn . show) >> (putStrLn $ show pi)

JavaScript

で、JSの場合。モナドは単なる関数オブジェクトで、>>=に相当するbindメソッドと>>に相当するpushメソッドを持っている。

#!/usr/bin/js
function monad(f){
  f.bind = function(g){ return g(this()) };
  f.push = function(g){ return this.bind(monad(function(){ return g })) };
  return f;
}

var getClockTime = monad(function(){ return (new Date) + '' });
var putStrLn = function(s){ return monad(function(){ console.log(s) }) };
var pi = Math.atan2(1,1)*4;

var main = getClockTime.bind(putStrLn).push(putStrLn(pi));
main();


Perl

Perlではそれに加えて>>=>>を演算子として実装してみた。かなりHaskellっぽくなるが、結合法則まではoverloadできないので括弧を使う羽目になる。

#!/usr/bin/perl
use strict;
use warnings;

package Monad::IO;

sub new {
    my $class = shift;
    bless shift, $class;
}

sub bind {
    my ( $m0, $fm1 ) = @_;
    $fm1->( $m0->() );
}

sub push {
    my ( $m0, $m1 ) = @_;
    shift->bind( __PACKAGE__->new( sub { $m1 } ) );
}

use overload
  '>>='    => sub { shift->bind(@_) },
  '>>'     => sub { shift->push(@_) },
  fallback => 1,
  ;

# export の代わり
sub main::monad(&) { Monad::IO->new(shift) }

package main;

my $getClockTime = monad { localtime() . '' };
my $putStrLn = sub {
    my $str = shift;
    monad { print "$str\n" };
};

sub pi { atan2( 1, 1 ) * 4 }
# >>= も >> も perl では右結合なので括弧
my $main = ( $getClockTime >>= $putStrLn ) >> $putStrLn->( pi() );

$main->();

なお、Perlに関しては、ガチで「単なる自己関手の圏におけるモノイド対象」なやつも

にあるのであわせてご覧いただきたい。

で、なんでこんなもんが必要なの?

全ては「参照透過性を守るため」と言い切っていいだろう。「単なる自己関手の圏におけるモノイド対象」だけで頭痛が痛いのに参照透過性とは、と文句もいいたくなるがこちらの方はそれほど難しくない。要するに、「関数に同じものを入れたら、同じ結果が必ず返ってくる事」のことだ。数学の「函数」がこれだ。cosにπを入れたら、だれがいつどこでやっても常に-1だ。

ところがほとんどの言語における関数==function==機能はこれが成り立つとは限らないし、これが成り立ってしまったら困ってしまう。たとえば「今何時?」を知りたい時にはJSならnew Date、perlならtime()ないしlocaltime()とやるが、時刻が変われば結果も変わる以上、当然参照透過ではない。

こんな時、どうしたらよいのだろう。

逆に考えるんだ。「関数に時刻を調べさせる」のではなく、「時刻で何かする関数を用意して、その引数に「何をするか」を決めた関数を食わせればいいんじゃないか?」。引数としての関数は定数。その結果であるところの返り値も関数という定数。入力と出力の対応が一対一になっていれば、途中で何が起きてもいいじゃないか。

ここで、ghciなりで>>=の型を見てみる。

Prelude> :type (>>=)
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

第一引数がモナドa、第二引数がaを受け取ってモナドbを返す関数、そして返り値がモナドb。aやbは確かに外に出てこない。aやbに参照非透過な値が入る事はことはあっても、入力や出力はあくまでモナドaでありモナドb。同じモナドを食ったら同じモナドを吐くのであれば確かに全体として見れば>>=は参照透過になる。

参照非透過な関数も、その関数そのものは定数、すなわち参照透過。

そしてJSのbindの実装は

f.bind = function(g){ return g(this()) };

となっている。確かにgには「裸の値」が渡るのだけど、しかしgの方ではその結果はきちんとモナドにくるみ直して返している。

var putStrLn = function(s){ return monad(function(){ print(s) }) };

モナドはモナカ。

「単なる自己関手の圏におけるモノイド対象」よりは覚えやすいと思う。

それじゃ、あんこをモナカにならぬ、値をモナドにくるむ時はどうしたらよいか。

上の例ではmonad()という名前の関数がその作業を行う。make_monadでもmonadizeでもいいだろう。

が、Haskellでは、それになんとreturnという名前がついている。これははっきり言ってひどい。"Real World Haskell"もそういっている。私も二重の意味でひどいと感じる。一つは他の言語のreturnと意味がまるで違うこと。そしてもう一つは、「一度モナドで包んだものは、外には絶対に出すなよ」という戒律と全く逆の意味に取れること。returnはノーリターンなのだ。

じゃあ参照透過性が保証されると何がいいんだろう。

長くなったので別の機会に。

Dan the Blogger Combinator