純粋関数型JavaScriptのつくりかた

  • 122
    いいね
  • 7
    コメント
この記事は最終更新日から1年以上が経過しています。

前回筆者が書いた記事が長いって言われたので、今回は簡潔にいきます。この記事との関連は薄いので、前回の記事を読んでなくても大丈夫です。

さて、言語全体が参照透明な(簡単にいえば副作用のない)式で構成される言語を純粋関数型プログラミング言語と言いますが、プログラミング言語から副作用のある式をすべて除去し、その代わりにアクションとかIOモナドという仕掛けを追加すると、その言語を純粋関数型に変えることができます。このあいだふとした思いつきでJavaScriptを純粋関数型にしてみたんですが、そのままストレージの奥で腐らせるのはもったいないのでAurorScriptと名づけて記事にして飾っておきます。アクションの仕掛け全体は10行くらいで書けるので簡単です。

純粋関数型を理解するには、自分でアクションのような仕組みを作ってみるのがとてもいい勉強になります。だって「副作用のない式のみで副作用を表現する」とか説明しても「……はぁ?副作用あるの?ないの?」とかっていう反応になるの当たり前じゃないですか。数学屋さん好みの説明に、世界全体の状態を表す型 $\Omega$ があるとき、アクション $ {\rm IO} \ a$ とは ${\rm IO} \ a : \Omega \rightarrow (a, \Omega)$ であるどうのこうのみたいな感じの説明もありますが、気持ち的には確かにそんな感じでも、ホントに世界全体を記述できるわけないじゃないですか。いい加減にしてください!筆者が10行でアクションを実装して見せて、アクションが具体的にどういう仕組みなのかお見せします(言うまでもありませんが、これは数ある実装方法のひとつでしかありません。まともなコンパイラはちゃんと効率のいいコードを吐きます)。筆者は世の中の「関数型言語とかわけわからん」という人の味方ですからね。

フレームワーク

まずは 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

見ての通りの関数で、短いので特に説明は不要かと思います(なお、今回は簡潔にいきたいのでいきなりアロー関数式使いまくりです)。次に、なくてもいいんですがあると便利な wrap という関数を定義しておきます。

var wrap = f=>a=>_=>f(a)                  // wrap :: (a -> b) -> (a -> IO b)

こんなたった1行の関数をいちいち説明していたら、お前の説明は長い長いとまた言われるでしょうし、次に進みましょう。

アクション

先ほどの3つの関数はアクションを組み合わせて運用するためのフレームワークにすぎないので、実際に副作用を表現するにはその副作用に対応したアクションという値を定義する必要があります。ここでは標準出力と標準入力に対応するアクションを用意することにし、標準出力はブラウザのコンソールに書くことにします。標準出力と標準入力に対応するアクションを用意すれば、その言語で参照透明性を保ったまま標準出力と標準入力を扱うことができるようになります。

console.logのような1引数の関数はwrapに渡すだけでアクションを返す関数にすることができます。

var put = wrap(console.log.bind(console)) // put :: a -> IO ()

次に標準入力です。ブラウザ環境のJavaScriptには標準入力のようなものはありませんので、XHRでGETして入力の代わりにします。次の関数getはURLとして文字列を与えると、そのURLのデータをXHRで取ってくるというアクションを返します。これも対応する関数をwrapに渡すだけです。

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は参照透明な関数です。

こんな感じで残りの関数も調べていくと、それぞれすべて参照透明な関数であることが確認できます。参照透明な関数をどのように組み合わせてもコードは参照透明なままです。したがってAurorScriptで書かれたコード全体は参照透明なのです。execは気にしないでください。いいから気にしないで。長い話が苦手な人たちのフラストレーションがすでに限界寸前でピキピキ音を立てているのが聞こえます。無駄話をしている余裕はありませんから次に行きましょう。

使ってみよう

あとは、mainに設定されたアクションを起動するようなランタイムを書き加えて完成です。よいしょっと。

Object.defineProperty(window, "main", { set: exec });

え?exec?本当に何もかも副作用がなかったら、プログラムは何もすることができません。清浄なる世界を取り戻すためには、誰かが汚れ仕事を引き受けなければなりません。たった一度だけ、execを呼び出すという最初で最後の禁忌を冒します。

さて、これまで書いたコードをランタイムライブラリとしてまとめて、orionhubで参照できるようにしてみましたこの記事の最後に付録として載せておきました(どうでもいいですが、参照先のコードにはこの purebind がモナドになっているという簡単な証明もついています。証明を見てオエーッってなりたい人はどうぞ)。次のコードは指定した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/ のソースがすべて大文字になって出力されていれば成功です(アロー関数式使いまくりなのでFirefoxでしか動きません。Chromeで動かしたかったら全部function式に書き換えてください)。

なにが起きてるの?

  1. get("http://fiddle.jshell.net/") を呼び出すと function(){ var xhr = new XMLHttpRequest(); ... } みたいな関数が返ってきます。この関数は引数がない関数で、呼び出されるとXHRを発行してデータを取ってきます。が、まだその時ではありません。じっと時を待ちます。この実装では、アクションとはこういう引数がなくて実行の時を今か今かと待ち構えている関数のことです。
  2. x=>put(x.toUpperCase()) は文字列 x を渡されると toUpperCase で大文字にして put に渡す関数です。でもまだその時ではありません。put に渡す文字列が来るまでじっと待ちます。雌伏のときです。
  3. bind(...)(...) で、1と2で作られた関数オブジェクトがbindに渡されます。bindはこういうアクションと関数を結びつけた、新しいアクションを返します。しかしあくまで結びつけるだけで、何もしません。ひたすら時を待ちます。
  4. mainに3で作られたアクションが代入されます。AurorScriptのコードはここまでです。ここまで何の副作用もありません。これが言語が参照透明である、純粋関数型言語である、ということです。AurorScriptのコードで可能なのは、このmainというアクションの値を求めることだけなのです。
  5. しかしmainに渡されたアクションはそのままAurorScriptのランタイムに渡され、AurorScriptランタイムはexecを使ってこのアクションを起動します。時が来た!結び付けられたアクションが順番に起動し始めます。
  6. bind内部でまずget("http://fiddle.jshell.net/")のアクションが起動されXHRを発行、完了するとbindは結果の文字列をx=>put(x.toUpperCase())に渡します。
  7. x=>put(x.toUpperCase()) の x にさっきの文字列が渡され、toUpperCase で大文字になり、put に渡されます。
  8. このput("文字列") はアクションですが、これもbind内部で起動されます。console.logが呼び出され、文字列が出力されます。

とても詳しい解説

pure, bind, execはそれぞれHaskellのIOモナドの pure, >>=, unsafePerformIOに相当します。

あと、PureScriptにおけるアクションの実装方法がまさにこの手法を使っています。

マイ純粋関数型言語を作ろう

この記事と同様に10数行のコードを書くだけで、あなたの好きな言語を浄化して純粋関数型プログラミング言語にすることができます。これまでの副作用で薄汚れた生活を卒業し、ぜひ純粋な言語で清潔なコーディングライフを送りましょう。勉強以外の役には立ちませんが。

付録

なんかOrionHubさんの調子が安定していないので、aurorscript.jsの全文をコピペしておきます(本当はgithubとかにおいたほうがいいけどアレがアレなので)。以下のコードのライセンスはMIT Licenseとかでいいです。好きにしてください。

"use strict";

// -- stdlib --
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
var wrap = f=>a=>_=>f(a)                  // wrap :: (a -> b) -> (a -> IO b)
var put = wrap(console.log.bind(console)) // put :: a -> IO ()
var get = wrap(url=>{                     // get :: string -> IO string
    var xhr = new XMLHttpRequest()
    xhr.open("get", url, false)
    xhr.send()
    return xhr.responseText
})

// -- runtime --
Object.defineProperty(window, "main", { set: exec });


/*

A proof that (pure,bind) is a Monad:

law1:  (return x) >>= f == f x

  bind(pure(x))(f)
= (m=>f=>_=>f(m())())(pure(x))(f)
= (f=>_=>f(pure(x)())())(f)
= _=>f(pure(x)())()
= f((pure)(x)())
= f((a=>_=>a)(x)())
= f((_=>x)())
= f(x)

bind(pure(x))(f) = f(x)


law2:   m >>= return == m

  bind(m)(pure)
= (m=>f=>_=>f(m())())(m)(pure)
= (   f=>_=>f   (m())())   (pure)
= (      _=>pure(m())())
= (      _=>(a=>_=>a)(m())())
= (      _=>(_=>(m())) ())
= (      _=>((m())) )
= _=>(m())

bind(m)(pure)() = m()
bind(m)(pure) = m


law3:   (m >>= f) >>= g == m >>= (\x -> f x >>= g)

  bind(bind(m)(f))(g)
= bind(   bind                  (m)  (f)   )(g)
= bind(   (n=>h=>_=>h(n())())   (m)  (f)   )(g)
= bind(   (   h=>_=>h(m())())        (f)   )(g)
= bind    (      _=>f(m())())               (g)
= bind                       (_=>f(m())())   (g)
= (n=>h=>_=>h      (n())())  (_=>f(m())())   (g)
= (n=>h=>_=>h( n              ())())  (_=>f(m())())   (g)
= (   h=>_=>h( (_=>f(m())())  ())())                  (g)
= (      _=>g( (_=>f(m())())  ())())     
= (      _=>g(     f(m())()     )())     
= (_=>g(f(m())())())     

  bind(m)(x => bind(f(x))(g))
= bind(m)(x => (n=>h=>_=>h(n())()) (f(x))   (g))  
= bind(m)(x => (n=>h=>_=>h(n      ())()) (f(x))   (g))  
= bind(m)(x => (   h=>_=>h((f(x)) ())())          (g))  
= bind(m)(x => (   h=>_=>h((f(x)) ())())          (g))  
= bind(m)(x => (      _=>g((f(x)) ())())             )  
= bind(m)(x => (_=>g((f(x))())()))
= (n=>h=>_=>h(n())())   (m)   (x => (_=>g((f(x))())()))
= (   h=>_=>h(m())())         (x => (_=>g((f(x))())()))
= (   h=>_=>h                          (m())())  (x => (_=>g((f(x))())()))
= (      _=>(x => (_=>g((f(x))())()))  (m())())  
= (      _=>(x => (_=>g((f(x         ))())()))  (m())())  
= (      _=>(     (_=>g((f((m())     ))())()))       ())  
= (_=>((_=>g((f((m())))())()))())
= (_=>(_=>g(f(m())())())())
= (_=>g(f(m())())())

bind(bind(m)(f))(g) = bind(m)(x => bind(f(x))(g))
 */