Haskell
関数型言語
関数型プログラミング

参照透過性と副作用についての提言

More than 3 years have passed since last update.


背景、あるいは事実

[1]http://www.cs.indiana.edu/~sabry/papers/purelyFunctional.ps

および

[2]http://www.cs.nott.ac.uk/~pszgmh/faq.html#purity

が端的で分かりやすいかと思います。(副作用の定義はまた諸論あるかもしれませんが。)

・標語的に「関数を評価した時の<値>が変わらない」という性質を持つ関数は、純粋関数である、という。純粋、というのは初等数学の関数の意味での関数との対比から。

・これをきちんと述べる為に、「ある(関数型)言語Lが純粋関数型である」ということを、値呼び出し、名前呼び出し、必要呼び出しの評価結果が同値であること、として定義する。ここで関数型言語というのは、ざっくり型付きλ計算ができればよい。

・名前呼び出しと必要呼び出しの同値性によって、一度評価された結果の一意性が保証されることになり、この純粋性から直ちに「純粋関数型言語では、任意の関数は副作用によってその評価結果が変わることがない」ことが分かる。ここで「副作用によって」と言っているのは、要するに関数の引数以外の何かで、ということ。ただし、ここでは「関数の評価に影響を与えない外界」への影響に関しては何も言及しない。

・Haskellにおける入出力は、そのコードを「実行」するまで評価される事が無い。IOに対しては関数適用(β簡約)が行われない、ということ。その意味で、Haskellの入出力は、Haskell内の評価に影響を与えるような副作用を持たない。「Haskell内の評価に影響を与える」と言っているのは、「関数の引数以外の何かで関数の評価結果が変わる」という意味での副作用、ということ。

これらは全て、一般的にも了解が取れるであろう事実(のつもり)です。


暫定提言

・(関数型)言語全体で<値呼び出し、名前呼び出し、必要呼び出しいずれで評価しても同じ結果が保証される>という性質はpurity(純粋性)と言われている(これは事実。敢えて言えば「破壊的更新が無い」という要素の有無はあるものの、大きな用語の混乱はないと思われる)。

・これと類似のことを、個々の関数について表したい場合、その関数を定義している一連のコード(文脈)Cを全て含めて<関数fは文脈Cの中で純粋である>、あるいは文脈Cが純粋な関数だけで構成されるとき<文脈Cは純粋である>という言い方ができるのではないか。若干まどろっこしい言い方なのは、関数fがその内部で定義されていない何かを使用している場合、その何かの定義によって評価結果が変わり得る為。

注意事項として、[1]で関数(項)の同値類を定義していて、この項は当然自由変数を含んでも当然よいのだが、文脈不変性を持っている訳ではない。f,gが任意の文脈で同じ評価結果eval(f)=eval(g)になる(文脈によらず同一の評価結果ということではない)、ということを以って同値類としている。

・ただし、関数fが自由変数を持たない場合は、[1]のProp.2.8~Lemma.2.11あたりを参考にして、fの文脈不変性を以ってfが純粋であるということを定義できる。のではないかと思うが、まだきちんと考えていない。(言語が任意のときに、fを切り取って純粋と言いきる事ができるのか、的な意味で)

この意味で純粋なfの性質(fの値の文脈不変性)が、いわゆる参照透過の「素人的な」説明でしばしば見受けられるものだと思われるが、とりあえず参照透過という言葉は封印する。C(f)がCによらず一定になる性質のことは、(仮にそういう性質があったとすれば)文脈不変性と呼ぶ。(嘘にはならないので)

・ある関数の文脈不変性と、評価戦略に対する同値性は別の概念だが、それぞれに参照透過という概念が付いて回っているので、いっそ封印する。

・プログラミング言語における「副作用」は、評価に影響を与えるか否か、という意味で使う。実行系では、(

封印する理由をもう少し噛み砕くと、

・文脈不変性と参照透過の関連に関しては上に書いた通りで、封印する。

・評価結果の同値性をかなりナイーブに見ると、評価の仕方という<<文脈>>に依存せず関数の評価結果(あまり正確ではないが、その言語の中で、他の関数の入力になり得る対象)が変わらないという意味で、比較的「本来の意味」に近い意味で参照透過と言えるが、このナイーブな見方は色々不毛な議論になりがちなので、封印する。

そういうわけで、「正しい」参照透過性や「間違った」参照透過性は、どれも使用を回避して、純粋性(+副作用がないこと)、もしくは文脈不変性という言葉を使う。

副作用は、評価に影響を与えるか否か、という意味で使う。←これは一般的な感性からは中々受け入れられないような。。。

これで、大分穏便な主張になったような。


前提

本稿の目的は、よりすっきりした内容を目指して、解釈・考え方・パラダイム・用語 etc.の整理を行うことにあります。その目的に対する筆者のスタンスは、間違いや不勉強は謹んで訂正させて頂きつつ、ゆるゆると邁進するというものです。

色々と考えてはいますが、基本的には筆者の思うところ、であって、これが絶対的な事実であるとかそういうことを主張するのが目的ではありません。また、他の考え方や意見を否定するものでもありません。基本的には、個人としてこう考えるとすっきりするかな、という落としどころが見つかればいいな、という程度のスタンスです。もしコメント頂ければ、筆者の理解力や時間を超えないレベルで応答します。という勝手な前置きの上で。

※ちなみに、Qiitaにも、分析哲学からの出自も含むような考察や歴史に関する言及がありますね。

以下、過去の議論です。


参照透過性と副作用は異なる概念であって、分離すべき

端的には、これが結論であり主張のすべてなのですが、具体的な内容を以下で説明します。


参照透過性とは

式f(x)=【fを定義する文字列】(引数)の評価とは、f(x)という式に対して、<値>を対応させることを言う。ただし、この対応は有限の時間では終わらないこともある。

たとえば、fという関数が、「整数の引数xを2倍した値を返す」と定義されている場合には、f(5)という式に対しては、<値>として10が定まる。

式f(x)=【fを定義する文字列】(引数)を評価した<値>(f(x)の評価を行う過程で評価が行われる<値>も含む)が、fを定義する文字列と引数によって一意に定まるとき、f(x)は参照透過である、という。

※本当は、【fを定義する文字列】の解釈の方法と抱き合わせで参照透過か否かが定まるので、正確にはfを定義する文字列、引数、文脈、というべきではあります。ここでは、文脈が明らかである、ということを仮定していると考えて進めます。

※ここで「文脈」として曖昧にしていたもののうち、少なくとも定義に内包される自由変数を始末する文言は必要と分かったので、自由変数を始末する為に「f(x)の評価を行う過程で評価が行われる<値>も含む」という定義を付け加えました。

ご指摘ありがとうございます。

これは、標語的によく言われる「式の値が引数によって一意に決まること」をちょびっと丁寧に言いなおした、ということになります。

値をわざわざ<値>と書いているのは、ここで言いたい<値>というのは、形式言語とそれが指し示す値(語弊を承知で乱暴に言い換えれば、例えば日本といったときにそれが日本の国の概念なのか、日本列島という物質のことなのか、日本人なのか、とか)のマッピングを意図しているのではなくて、純粋に評価を行った結果という意味での<値>という意味である、ということです。

ヒルベルトの公理的幾何学とビールジョッキ・机・椅子の話がイメージできれば、話は早いと思います。

これらの関係と同様で、それらの言葉が持つであろう「言葉の値」ということとは別の次元の話として、単にプログラミング言語の中の規則によって、ある式を評価したときの結果が、その式の内部のものだけで本質的に言い尽くされているのか、それともその式の外部のものにも依存しているのか、ということが参照透過性という概念です。と、少なくとも私は思いたい。理由は後述します。

有限時間で処理が終わらない可能性についての仮定は、いろいろと理由はありますが、とりあえず副作用で話をするファイル入出力関数への対応のため、と思っておきます。


副作用とは

一般には、プログラミング言語は、式を評価する際に、式の値を確定させるだけではなく、その他の処理も行います。

式f(x)=【fを定義する文字列】(引数)を評価する際に、(物理的なイメージではメモリやディスクドライブのような)何らかの状態に影響を与えるとき、これは副作用を持つという。また、その影響のことを副作用という。

おそらく、プログラマにとって最もイメージしやすい副作用はファイル入出力です。

今の定義に従えば、「参照透過だが副作用を持つ」という事象は、あり得ます。

例えば、fという関数を、ファイルパスを受け取って、ファイルパスのファイルの内容をすべて読み込み、ファイル名の末尾にcという文字をつけてコピーし、その結果として1を返す関数とします。また、ファイル処理が途中でエラーを起こした場合、この関数は結果を返さない(無限ループと同じ状態と思う)とします。

そうすると、fは上の定義では参照透過で(値を返すとすれば1)、かつ副作用を持ちます。

※ただし、この副作用の定義だと、<値>の評価に影響の無い副作用も副作用に含んでしまうので、この定義はあまり適切でない気がしています。結局、評価する際の<値>を変えないならば、言語仕様的な意味での副作用は存在しない、という考え方が適切ではないか、と。

そう考えると、「参照透過だが副作用を持つ」ということは(きっと)無いのではないか、と。


どうしてこういう定義を考えたいのか

Haskellを普通に使われている方にとっては、結構自明なことかもしれませんが。

IOモナドは、外部への文字列等の出力や入力を持つという意味で、副作用を持ちます。

→「Haskellから観測できない」という意味で、実は、「Haskellの中ではIOモナドも副作用を持たない」と考えるのが正しい気がしてきました。経緯はコメントを追ってください。(長くてすみません。まとめ直すかもしれません。)

一方で、Haskellは「純粋関数型言語」という言い方もされます。

これがどういう意味なのかというと、上の意味であらゆる関数は参照透過であることを保証される(副作用が無いとは言っていない!)ということだと思っています。

これが、よく標語的に「モナドは別世界に隔離するためのもの」「入出力を入出力の閉じた世界に送る」「IOを伴うものにはIOを伴うというシールを貼る」「Maybeは失敗など<そうでない可能性>があるものに<そうでない可能性>のシールを貼る」みたいな感じで説明されている内容を表しています。

これは、結局、関数というものが「値」を返すものであるということを思い返せばすっきりしていて、

関数が返す値=関数を評価した<値>は、いわゆる初等数学であつかう関数と同じく、引数が同じときは常に同じである

ということだと思うのです。

この考え方、然るべきところでは既に一般常識なのかなあとも思うのですが、私自身が無知なもので、とりあえず提言という書き方で書かせて頂きました。いかがでしょうか。

そういう言葉の使い方は適切でないとか、理屈として破綻しているとか、もし何かあれば、ぜひとも教えてください。