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

セカイノカタチ

世界のカタチを探求するブログ。関数型言語に興味があり、HaskellやScalaを勉強中。最近はカメラの話題も多め

マーブルワーズ

プログラムを純粋に書けって言われたけど、どこまで純粋に書けばいいの?

今、関数型言語の流行により、プログラムを純粋に書くことが求められています*1

そういわれて、「わかった、副作用のないプログラムを書くぞ!」と勇んで書き始めるとすぐにわかりますが、これがなかなか簡単なものではありません。

純粋な関数を定義するのは簡単ですが、純粋な関数というのは連鎖します。状態を保持することができないため、aを渡してa+1を返す関数があった場合、戻ってきたa+1を呼び出し側も保持できません。そのため、どうするかというと、呼び出し元に返します。そしてその呼び出し元も・・・。

|scala| def f(x:Int) = x + 1 def g(x:Int) = f(x) + 1 def h(x:Int) = g(x) + 1 ... ||<

classも状態を持たないので、状態を変更したければ、状態が変更されたインスタンスを新たに生成することになります。

しかし、変更されたインスタンスを受け取っても保持できないため、さらに変更されたインスタンスを返さなければなりません。さらにそれを受け取るインスタンス・・・。

|scala| case class A(num:Int) case class B(a:A) case class C(b:B) def aInc(a: A) = A(a.num + 1) def bInc(b: B) = aInc(b.a) def cInc(c: C) = bInc(c.b) val c = cInc(C(B(A(1)))) // cも状態として保持できない・・・ ... ||<

関数型言語を学び始めた当初、このことに非常に頭を悩ませました。

究極的には、純粋なプログラムは外界と通信するすべを持ちません。副作用を持たないからです。 これでは、とても実用に足るパラダイムとは言えないではないか・・・。と、途方に暮れたものです。

** 純粋な領域と不純な領域を分ける

もし、あなたが関数型言語を学び始めたばかりで、同じように悩んでいるのなら、こんな風に考えると気持ちが楽になりますよ。っていうコツがあります。

「プログラムを純粋に保つ」という言い方は、間違いでなないですが舌足らずです。

正確に言うと「プログラムの純粋な領域と不純な領域を分離する」となります。

プログラムというものは、本質的に副作用を伴うものなので、これは避けて通れません。しかし、何の制限もなく自由に副作用を発生させるプログラムは、多くの問題を発生させます*2

そこで、「ここは純粋ですよー」という領域と、「ここはちょっとカオスってるカモ(///)」という領域を乙女チックに分けると、それらの問題を発生しにくくすることが出来るという考えが生まれました。

ただ分けると言っても漠然としすぎていますので、とある指標に従って分離していきます。

どのように分けるかというと、外界との繋がり、つまりIOを一端として、そこから連なるプログラム領域のうち、IOが避けられない部分を「副作用が発生する危険領域」として隔離するのです。

こうすることで、IOが必要ない本質的に純粋な領域を安全に保つことが出来るというわけです。適当に図を描くとこんな感じのイメージです。

この領域を分けると言う仕事を言語的にサポートしたのが、Haskellで言うところのIOモナドの役割となります。

プログラミングが持つ普遍的な戦略として、「分離した領域のうち、純粋な領域をなるべく広く保つ」ということが言えると思います。そのためには、まずは分離。ということで、このような仕組みが取り入れられているのだと思います。

逆に、悪い例でいうと、純粋な領域と不純な領域が混ざってしまうと良くないですし、そもそも純粋を意識せず、全領域が不純な状態というのもマズいわけです。 Scalaなんかだと、やろうと思えば、いつでもIOを混ぜ込めたりするわけで、その辺のところを意思の力でなんとかするわけです(おい)。

まあ、ざっくり言ってしまうととても簡単な事ですが、実際やれと言われると結構難しいんですよね。これが。

*1:純粋って何?って言うことに関してはここでは書きませんし、僕も良くわかりません(汗)。ここでは、「単に副作用の無い関数を組み合わせてプログラムを書くこと」ぐらいに捉えておいてください

*2:どんな問題かはここでは書きません