言語全体が参照透明な式1で構成される言語を純粋関数型プログラミング言語(Purely Functional Programming Language)と言いますが、プログラミング言語から副作用のある式をすべて除去し、その代わりにアクションとかIOモナドと呼ばれる仕掛けを追加すると、その言語を純粋関数型に変えることができます。このあいだふとした思いつきでJavaScriptを純粋関数型にしてみたんですが、そのままストレージの奥で腐らせるのはもったいないのでAurorScriptと名づけて記事にして飾っておきます。アクションの仕掛け全体は10行くらいで書けるので簡単です。
純粋関数型を理解するには、自分でアクションのような仕組みを作ってみるのがとてもいい勉強になります。だって、「副作用のない式のみで副作用を表現する」とか説明しても「……はぁ? けっきょく副作用あるの? ないの?」とかっていう反応になるの当たり前じゃないですか。筆者が10行で『アクション』を実装して見せて、アクションが具体的にどういう仕組みなのかお見せします2。
フレームワーク
まずは pure
, bind
, exec
という3つのシンプルな関数を実装します。
var pure = a=>_=>a // pure :: a -> IO a
var bind = m=>f=>_=>f(m())() // bind :: IO a -> (a -> IO b) -> IO b
var exec = m=>m() // exec :: IO a -> a
見ての通りの関数で、短いので特に説明は不要かと思います3。次に、なくてもいいんですがあると便利な wrap
という関数を定義しておきます。
var wrap = f=>a=>_=>f(a) // wrap :: (a -> b) -> (a -> IO b)
こんなたった1行の関数をいちいち説明していたら、お前の説明は長いとまた言われるでしょうし4、次に進みましょう。
アクション
先ほどの3つの関数はアクションを組み合わせて運用するためのフレームワークにすぎないので、実際に副作用を表現するにはその副作用に対応したアクションという値を定義する必要があります。ここでは標準出力と標準入力に対応するアクションを用意することにし、標準出力はブラウザのコンソールに書くことにします。標準出力と標準入力に対応するアクションを用意すれば、その言語で参照透明性を保ったまま標準出力と標準入力を扱うことができるようになります。
console.log
のような1引数の関数はwrap
に渡すだけでアクションを返す関数にすることができます5。
var put = wrap(console.log.bind(console)) // put :: a -> IO ()
次に標準入力です。ブラウザ環境のJavaScriptには標準入力のようなものはありませんので、XHRでGETして入力の代わりにします。次の関数get
はURLとして文字列を与えると、そのURLのデータをXHRで取ってくるというアクションを返します。これも対応する関数をwrap
に渡すだけです6。
var get = wrap(url=>{ // get :: string -> IO string
var xhr = new XMLHttpRequest()
xhr.open("get", url, false)
xhr.send()
return xhr.responseText
})
補足
AurorScriptの大部分はJavaScriptと同じですが、いくつかの追加のルールがあります。ちょっと長いですが純粋関数型にするために必要なので我慢して下さい。コンパイラはないので、コンパイラの気持ちになりきって以下のルールを守ってコーディングします。
- 再代入の禁止: 変数やオブジェクトのプロパティへの再代入は禁止です。
- 変数の巻き上げなし: 変数は宣言する以前は参照することができません。
- 参照透明でないすべての関数の禁止: 使用することができる関数およびプロパティは、組み込みの関数
pure
,bind
,get
,put
に加えて、JavaScriptの関数のうち副作用のないプロパティと関数に限られます。 - 静的型付け: AurorScriptは厳格に静的型付けされた型推論付きのプログラム言語です。型注釈がないように見えるかもしれませんが、心の目を鍛えると見えるようになってきます。筆者は見えます。まだ型注釈が見えない人のためにコメントにてHaskellスタイルで型注釈を書いておいたので参考にしてください。関数じゃない型の値を関数として呼び出す式は型エラーです。例を挙げると、たとえば
put(0)
は関数ではなくIO ()
という型を持つアクションなので、put(0)(0)
という式は型エラーです。実体はFunction
オブジェクトだったとしても型エラーです。 - エントリポイント: プログラムのエントリポイントに相当するものとして、
main
という名前のグローバルな変数にアクションを設定するとそれが実行されます - 禁忌:
exec
は闇の魔術なので使用は厳禁です。使うなよ?絶対使うなよ?
本当にこれ参照透明?
本当に参照透明な言語になっているか確認しておきましょう。JavaScriptに元から存在する副作用のある操作はすべて禁止されていますから、あとは先ほど定義したいくつかの関数が参照透明であるかどうか確認すれば充分です。
たとえば put
は関数ですが、put("hoge")
というように呼び出しても、
put("hoge") = wrap(console.log.bind(console))("hoge")
= (f=>a=>_=>f(a))(console.log.bind(console))("hoge")
= (a=>_=>console.log.bind(console)(a))("hoge")
= _=>console.log.bind(console)("hoge")
ですから、put("hoge")
という式を評価しても"Hoge"
という文字が出力されるわけではなく、単に関数(それがアクションです)が返り値として返ってくるだけで、何の副作用もありません。したがってput
は参照透明な関数です。
こんな感じで残りの関数も調べていくと、それぞれすべて参照透明な関数であることが確認できます。参照透明な関数をどのように組み合わせてもコードは参照透明なままです。したがってAurorScriptで書かれたコード全体は参照透明なのです。
exec
は気にしないでください。いいから気にしないで。長い話が苦手な人たちのフラストレーションがすでに限界寸前でピキピキ音を立てているのが聞こえます。無駄話をしている余裕はありませんから、次に行きましょう。
使ってみよう
あとは、main
に設定されたアクションを起動するようなランタイムを書き加えて完成です。よいしょっと。
Object.defineProperty(window, "main", { set: exec });
……え? exec
? あれだけ使うなと言っておいて、exec
を使うのかって? ……本当に何もかも副作用がなかったら、プログラムは何もすることができません。清浄なる世界を取り戻すためには、誰かが汚れ仕事を引き受けなければなりません。たった一度だけ、exec
を呼び出すという最初で最後の禁忌を冒します7。
次のコードは指定したURLのデータをXHRで取ってきてそれをすべて大文字に変えコンソールに出力する、まったく参照透明な式のみからなるプログラムです。
main = bind(get("http://fiddle.jshell.net/"))(x=>put(x.toUpperCase()))
簡単に説明しておきます。
-
get
は引数に与えられたURLにXHRすることを示すアクションを返す関数で、get("http://fiddle.jshell.net/")
という式はhttp://fiddle.jshell.net/
のデータを取ってくることを示すアクションです。 -
put
は与えられた文字列を出力するようなアクションを返す関数で、x=>put(x.toUpperCase())
という式はx
を引数として「x
を大文字にして出力するようなアクション」を返す関数です。 -
bind
はアクションと関数を結びつけた別のアクションを返す関数で、bind(get("http://fiddle.jshell.net/"))(x=>put(x.toUpperCase()))
という式全体はやはりアクションです。
jsFiddleで実際に動くのが見れます。コンソールに http://fiddle.jshell.net/ のソースがすべて大文字になって出力されていれば成功です。
とても詳しい解説
pure
, bind
, exec
はそれぞれHaskellのIOモナドの pure
, >>=
, unsafePerformIO
に相当します。
あと、PureScriptにおけるアクションの実装方法がまさにこの手法を使っています。
さいごに
この記事と同様に10数行のコードを書くだけで、あなたの好きな言語を浄化して純粋関数型プログラミング言語にすることができます。良かったらぜひマイ純粋関数型言語を作ってみてください。これまでの副作用で薄汚れた生活を卒業し、ぜひ純粋な言語で清潔なコーディングライフを送りましょう。勉強以外の役には立ちませんが。
付録
aurorscript.jsの全ソースコードをgistに貼り付けておきました8。このコードのライセンスはMIT Licenseとかでいいです。好きにしてください。
いや……おいコラちょっと待て。アロー関数式がごちゃごちゃ組み合わされててさっぱりわからんぞ。なにが起きてるんや?
-
get("http://fiddle.jshell.net/")
を呼び出すとfunction(){ var xhr = new XMLHttpRequest(); ... }
みたいな関数が返ってきます。この関数は引数がない関数で、呼び出されるとXHRを発行してデータを取ってきます。が、まだその時ではありません。じっと時を待ちます。この実装では、アクションとはこういう引数がなくて実行の時を今か今かと待ち構えている関数のことです。 -
x=>put(x.toUpperCase())
は文字列x
を渡されるとtoUpperCase
で大文字にしてput
に渡す関数です。でもまだその時ではありません。put
に渡す文字列が来るまでじっと待ちます。雌伏のときです。 -
bind(...)(...)
で、1と2で作られた関数オブジェクトがbind
に渡されます。bind
はこういうアクションと関数を結びつけた、新しいアクションを返します。しかしあくまで結びつけるだけで、何もしません。ひたすら時を待ちます。 -
main
に3で作られたアクションが代入されます。AurorScriptのコードはここまでです。ここまで何の副作用もありません。これが言語が参照透明である、純粋関数型言語である、ということです。AurorScriptのコードで可能なのは、このmain
というアクションの値を求めることだけなのです。 - しかし
main
に渡されたアクションはそのままAurorScriptのランタイムに渡され、AurorScriptランタイムはexec
を使ってこのアクションを起動します。時が来た!結び付けられたアクションが順番に起動し始めます。 -
bind
内部でまずget("http://fiddle.jshell.net/")
のアクションが起動されXHRを発行、完了するとbind
は結果の文字列をx=>put(x.toUpperCase())
に渡します。 -
x=>put(x.toUpperCase())
のx
にさっきの文字列が渡され、toUpperCase
で大文字になり、put
に渡されます。 - この
put("文字列")
はアクションですが、これもbind
内部で起動されます。console.log
が呼び出され、文字列が出力されます。
-
厳密には違いますが、あえてざっくりといえば、『副作用のない式』のことだと思ってしまってもいいでしょう。 ↩
-
これは数ある実装方法のひとつでしかありません。まともなコンパイラはちゃんと効率のいいコードを吐きます。 ↩
-
この記事を最初に公開したときは、まだJavaScriptでのアロー関数式が一般的ではなかったので、アロー関数式使いまくりでごめんなさい、Firefoxでしか動きません、みたいな注釈がありました。現代ではもう基本中の基本として定着してますよね。隔世の感があります。 ↩
-
この記事のことです。長すぎるわ!アホか!みたいにたくさん言われましたので、この『純粋関数型JavaScriptのつくりかた』の記事の方ではなるべく駆け足でざっくり説明するスタイルを試しています。 ↩
-
this
を失わないようにbind
をする必要はあります ↩ -
今どき XHRかよ!という声が聞こえてきそうですが、非同期処理を交えるとまた少し説明がややこしくなるので、あえて同期処理を使っています。
fetch
を使った実装は読者への課題とします。 ↩ -
最初に一度だけ決まりきった処理を行うので、Haskellのような、最初から純粋な式のみが前提の言語では、通常このような記述をする必要はないようになっています。 ↩
-
どうでもいいですが、参照先のコードにはこの
pure
とbind
がモナドになっているという簡単な証明もついています。証明を見てオエーッってなりたい人はどうぞ。 ↩