LoginSignup
107
89

More than 5 years have passed since last update.

仮想DOMを使うのに純粋関数型言語が最適である理由

Last updated at Posted at 2016-02-01

『純粋』関数型プログラミング言語といえば関数型プログラミング言語全体のなかでも殊更ラジカルな言語として知られていますが、『すべて純粋』という言語には、『だいたい純粋』という言語にはない利点があります。このテキストは、実感を得やすい具体的な場面を元に『純粋関数型』の利点を紹介していくシリーズです。第一回は言語を純粋にしてモナドで抽象化すると、非同期処理のコールバック地獄をPromise/Generators以上にシンプルかつ優れた方法で解決できるよという話でした。今回は、言語が純粋なら仮想DOMを使うときに純粋性も何も知らなくていいし、レンダリング関数の純粋性が自動的に保証されてとにかく簡単だよ、という話です。

ケース1: 『別に純粋でない』言語~『だいたい純粋な』言語の場合

みなさん仮想DOMは使ってますでしょうか。筆者はもう新しいおもちゃを与えられたイヌのようにハフハフしながら遊んでいて、この間記事に書いたコレも中身はMatt-Esch/virtual-domでレンダリングされています。ところで、仮想DOM実装でもっともポピュラーだと思われるReactのドキュメントでは、ドキュメントをレンダリングする関数renderの実装の仕方について、次のような注意書きがあります。

render()関数は純粋(pure)であるべきです。 これはコンポーネントのstateを変更せず、それぞれの実行時には同じ結果が返り、 DOMからの読み込みをしない、DOMへの書き込みをしない、 もしくはブラウザとの相互作用による処理(例えば、setTimeout)を行わないという意味です。 もしブラウザとの相互作用の処理が必要な場合は、 代わりにcomponentDidMount()内、または別のライフサイクルのメソッド内で行って下さい。 render()を純粋に保つことで、サーバーでの描画(レンダリング)がより実用的になり、 コンポーネントの処理を推察しやすいものにします。

なるほど、renderは『純粋』でなければならないということだそうです。また、setStateのところに次のような注意書きが書かれています。

注意:

決して、setState()呼び出し後に変更されたとして、直接this.stateを変更しないでください。 this.stateは不変(immutable)であるかのように扱って下さい。 setState()は即座にthis.stateを変更しませんが、stateの遷移をペンディング(保留)にします。 このメソッド呼び出しの後にthis.stateにアクセスすると、既存の値が返る可能性があります。

ふむふむ、this.stateは不変オブジェクトであるかのように扱えということだそうです。データ型の不変性はプログラムの純粋性に含まれます。また、Flux実装でポピュラーなReduxのドキュメントには次のような記述もあります。

Reducers calculate a new state given the previous state and an action. They must be pure functions—functions that return the exact same output for given inputs. They should also be free of side-effects. This is what enables exciting features like hot reloading and time travel.

Reducerは与えられた直前の状態とアクションから求まる新しい状態を計算します。Reducerは純粋な関数、つまり入力に対してまったく同じ出力を返すような関数でなくてはなりません。これらは副作用も持つべきではありません。こうすることでホットリロードやタイムトラベルのようなイカした機能が可能になります。

Reduxにも部分によって同じような「純粋であるべき」という要求があるようです。それで、『純粋な関数』っていったいどういう関数を指しているんでしょうか?幾つかやってはいけない操作があるらしいのはわかります。たとえば、DOMを直接変更したり参照したりしてはいけないようです。それで、やっていい操作とダメな操作っていうのは、どうやって見分けたらいいのでしょうか。疑問はいろいろあります。

  • コンポーネントの状態は変更しちゃいけないらしいけど、それ以外の状態は参照していいの?たとえばグローバル変数を用意して書き換えたりしてもいいの?ローカル変数の書き換えはいいの?
  • 『ブラウザとの相互作用』っていってるけど、ブラウザのオブジェクトとは関係ないMath.randoomとかDate.nowとかは呼んでいいの?
  • 別のライブラリの関数は呼んでいいのでしょうか?jQueryで要素を選択したりしても大丈夫でしょうか?Reduxには"Do not put API calls into reducers."って書いてあるけど、ちょっとだけならいいよね?ダメ?
  • ホットリロードやタイムトラベルに興味が無いなら、別に純粋にしなくても大丈夫なの?サーバーサイドレンダリングを使わない場合はrenderの純粋性は関係ないの?コンポーネントの内部処理をすでによく理解しているなら、純粋性は気にしなくて構わないの?
  • 純粋性は必須条件なの?ただのプラクティスなの?面倒臭かったら破ってもいいの?どこまで忠実に守るべき?
  • 「同じ入力には同じ出力を返す」といわれても、そうなっていることを確かめるにはどうしたらいいのでしょうか?テストでも書けばいいのでしょうか?同じテストを2度実行して結果が同じなら純粋だとか言ってもいいのでしょうか?(ダメです)

それに、やってはいけない操作をもれなく理解したとして、その「やってはいけないこと」をやっていないことを確かめるにはどうしたらいいのでしょうか?たとえ正確に理解できたとしても、人間ですからうっかりやってはいけない操作をやってしまうミスをすることはあります。言語によっては、そのようなうっかりミスを自動的に防ぐ機能があります。たとえばJavaならprivate修飾子をつけることでクラス外やパッケージ外からのうっかりアクセスを禁止できますが、同じように「純粋」であるためにやってはいけないことをやっていないということを、自動的に確かめる方法はあるのでしょうか?

this.stateは「不変なオブジェクトのように扱え」ということですが、Javaのような言語ならfinalとかreadonlyとかつけまくればいいかもしれませんが、JavaScriptには不変性を保証する良い方法がありません。それとも、Object.freezeとかをかましても大丈夫なのでしょうか?不変っていっても、その「不変」オブジェクトが参照する別のオブジェクトも再帰的に不変だと扱うべきでしょうか?それはどうやって実施すればいいのでしょうか?通常のオブジェクトの使用は諦めて、状態はすべてimmutable.jsが提供するデータ構造に置き換えるべきでしょうか?

JavaScriptでは、純粋性とは何ぞやということを正しく理解し、ライブラリのどの部分で純粋性を満たさなくてはならないかドキュメントをよく読んで理解し、その純粋性を壊さないように注意深くコードを書かなくてはなりません。また、うっかり純粋性を破壊するコードを紛れ込ませているとも限らないので、どうやったらいいのか筆者は知りませんが、とにかくコードの純粋性が保たれていることをちゃんと自力で確認しなくてはなりません。とても簡単だとはいえない要求だと思いますが、そういうものなので仕方ありません。気合でなんとかしましょう。

要するに、一言で「純粋に」と言われても、そもそも純粋性とは何なのかよくわからないし、仮に何が純粋なのか区別できるようになったとしても、既存の関数のどれが純粋でどれが純粋でないのか事前に誰かが分類してくれているわけではないので自力で区別していかなくてはならないし、実装できたとしてもその関数が本当に純粋になっているのか確かめる良い方法がないということです。

ケース2: 『すべて純粋』な言語の場合

仮想DOMまわりの純粋性ついて、静的型付けの『純粋』関数型プログラミング言語ではどのように扱うのかというと、何もしなくても型システムがすべて自動的に純粋性を保証してくれますので、特に気にしなくて大丈夫です。





……で、純粋な言語を使えば、純粋性云々なんて一切考えなくていいし、そもそも純粋性とは何なのかすら知らなくていいし、純粋性はコンパイラが自動的に検証して100%完全に守られ、そして別にそれに伴うデメリットもない、何もかもが解決してめでたしめでたしということでまあ話は終わりなのですが、ピンと来ない、あるいは信じられないというひとがたぶん多いと思うので、もう少しだけ詳しく説明します。

すべては型が教えてくれる

純粋な言語であれば、純粋性は自動的に保証されます。ドキュメントに純粋にしろ云々というような注意書きはありませんし、ライブラリを使う側も何が純粋かなんていちいちそんなことを考える必要はありません。考えなければならないのは、単に関数に適切な型の引数を与えることだけです。

例えば、純粋関数型プログラミング言語であるPureScriptだと、現在の時刻を取得するにはData.Dateモジュールのnowを『実行する』だけです。次のようにすると、nowが実行されて結果が変数tに代入されます。それからshow関数で数値を文字列に変換してlogに渡せば、現在の時刻が出力されます。

main = do
    t <- now
    log (show t)

さて、一方でPureScriptの仮想DOMライブラリのひとつpurescript-halogenでは、レンダリング関数は次のような感じで定義されます。レンダリング関数renderは状態stateを受け取って、仮想DOM木を作って返します。ここでは単にdiv要素の中に固定された文字列"Hello, World"を表示しているだけです。

render state = div [] [text "Hello, World"]

これらを組み合わせて現在の時刻を表示させるWebアプリケーションをつくろうと次のようなコードを書いてみると、再描画するたびに現在の時刻が表示されるかと思いきや、あっさりコンパイルエラーです。

render state = div [] [text (show now)]    -- コンパイルエラー

これはつまり、仮想DOMのレンダリング関数の純粋性を保つために、nowのような操作はここでは直接実行できないようにライブラリ側で制約しているということです。といっても、何か特別な機能が働いて純粋でない操作を検出したとかそういうことではなく、単にtext関数の引数の型が正しくないというだけです。PureScriptならレンダリング関数の純粋性がどうのこうのという面倒な説明を頭に入れる必要はないし、その制約に違反していればコンパイラが自動的にその箇所を教えてくれます。APIドキュメントでは、関数の型の形で、どの操作が許されるか、どの操作が許されないかが明確にわかるようになっています。

作用を含むRenderの定義

レンダリング関数ができるのは現在の状態だけに従って仮想DOMをレンダリングすることだけで、それ以外の作用は一切起こせないように定義されていますが、作用を扱えるようにこの定義を変えるのも簡単です。purescript-halogenではレンダリング関数は次のような型を持っています。

type Render s f = s -> HTML Void (f Unit)

これは直前の状態sを受け取ってHTML Void (f Unit)というデータを返す関数であることを示しています。Halogenでは上のように定義されているわけですが、Renderの定義としては、次のように定義する方法もあったかもしれません。

type Render s f eff = s -> Eff eff (HTML Void (f Unit))

違いは、先ほどの関数の返り値の型HTML Void (f Unit)Eff eff aという特別な型で包まれていることです。こうすると、「副作用のある操作も可能なRender」に相当する定義になり、レンダリングするたびに何か作用を起こしたり、レンダリングするたびに異なる結果を返したりということが許されるようになります。このように、作用を許すかどうかを定義で書き分けるのも簡単なのです。もちろん、実際の仮想DOMではレンダリング関数はいつ何回呼ばれるかわからないし、アプリケーションの状態だけに従ってレンダリングされるべきで、つまりRenderは純粋な関数であるべきなので、purescript-halogenでは前者のような定義が選択されています。

不変なデータ型

Reactではthis.stateは不変なデータとして扱えということになっていましたが、純粋関数型言語ではすべてのデータが不変であり、別に通常のデータとの区別はありません。それどころか、そもそもすべての構文が不変のデータ型を扱うためにできているため、immutable.jsのようなライブラリは必要ありませんし、そのようなライブラリを使うよりもっと便利な構文や関数が標準装備されています。

例えば、immutable.jsの公式サイトのサンプルコードを借りてくると、

var Immutable = require('immutable');
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 50);
console.log(map1.get('b')); // 2
console.log(map2.get('b')); // 50

こんな感じで、なんかいちいちmap1.get('b')とかちょっと面倒くさくない?って感じですが、PureScriptだと同じようなコードは次のように書けます。

let map1 = { a:1, b:2, c:3 }
let map2 = map1 { b = 50 }
print map1.b     -- 2
print map2.b     -- 50

map1のプロパティbにアクセスしたいならmap1.bと書くだけです。一部のプロパティだけが異なる別のオブジェクトを作りたければ、map1 { b = 50 }のようなとても直感的でシンプルな構文で書くことができます。不変なデータ型しかないので、構文はすべて不変なデータ型を扱うときに便利なようになっているわけです。不変なデータ型を使いたいなら、immutable.jsのようなライブラリを使うより、もう言語ごと乗り換えてしまったほうがはるかに楽だと筆者は思います。JavaScriptでもどうせJSXとかBabelとかWebpackとかMinifyとかでコンパイルの過程はあるでしょうし、今どき「コンパイルの手間が増えるから」なんて言い訳にもなりません。

jQueryからReactに乗り換えるような大きなパラダイムシフトを乗り越え、Reduxのような抽象的な構造の扱いを熱心に学び、Redux-Sagaでジェネレータ関数のような新しい言語機能を学ぶという学習コストを支払っておきながら、なぜプログラミング言語の乗り換えにだけはみんな腰が重いんでしょうか。学習コストを払う余裕がないなら、jQueryや生DOM APIを今後も使い続ければいいと思います。学習の意欲があるなら、言語もライブラリも一新して理想の開発環境構築に望むのもいいと思います。ライブラリは一新するのに言語は古臭いままというのは、右足だけ新しい靴を履いているのに、左足は履きつぶした靴のままのようなものです。

でも、純粋関数型プログラミング言語は副作用の扱いが苦手だったり難しかったりするんでしょ?

たとえば、purescript-halogenでは仮想DOMの状態を更新するのには次のようにします。

state <- get
put (state + 1)

getで現在の状態を取得し、それがstate変数に代入されます。put (state + 1)で新しい状態state + 1に設定され、仮想DOMの再描画が走ります。状態が更新され、DOMの再描画という副作用を発生させたのですが、これは難しいでしょうか。うわさ話で判断するのはやめて、実際のコードを読んだり書いたりして、それから難しいかどうか判断してみてください。

不完全にしておよそ正しくないプログラミング言語小史の名ジョーク、『モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?』を真に受けてる人もいそうな気がします。小うるさい理屈を『Haskell難しいでしょ?それ使える俺はすごいでしょ?』みたいに振りかざす人は(ワドラーさんではなく)確かにときどき見かけるので頭が痛いところなのですが、関数型プログラミングのユーザの大半はそうではないです。私はもちろん理論的背景を知るのも嫌いではありませんが、「getで状態取得。put関数で状態設定してDOMを再描画。純粋関数型プログラミング言語といっても難しくないでしょ」っていう立場です。

『だいたい純粋』と『すべて純粋』の違い

9000円の価値は1万円の価値に近いように、『だいたい純粋』な言語も『すべて純粋』な言語に近い性質を持っていると期待してしまいがちですが、そうではありません。『9割の窓が施錠された建物』は『すべての窓が施錠された建物』の安全性に近いどころかあまり施錠している意味がないように、『だいたい純粋』にはできなくて、『すべて純粋』な言語にのみできるという、そういう全か無かという性質もあるのです。

『だいたい純粋』でも、純粋でない関数は存在することには変わりありませんから、純粋であるものと純粋でないものを区別できるようにちゃんと理解しておかなくてはなりません。たとえば『すべてが完全に自動化された自動運転自動車』に乗るためなら運転免許は不要でしょうが、『9割は自動だけど1割は手動で運転する半自動運転の自動車』なら、やっぱり自動車教習所に通って免許を習得する必要があるはずだし、運転技術の学習コストが10分の1になるわけではありません。『すべてが純粋な関数だから、純粋性についてはよくわからなくてもいいや』ということはできても、『9割が純粋な関数で1割が純粋でない関数だから、純粋性については1割くらい理解していればいいや』というわけにはいきません。少しでも純粋でない関数が混じるなら、それを区別できるように『十分な』理解が必要になってしまいます。『すべて純粋』な言語と『だいたい純粋』な言語は、一見近いように見えて、実は決定的な差があるのです。

この純粋関数型言語の便利な性質は、『純粋』関数型プログラミング言語であるHaskellやPureScript、Elmなどの言語にはありますが、『だいたい純粋』『わりと純粋な部分が多め』という程度のLispやOCaml、Scalaにはない性質です。『すべてが純粋』という純粋関数型プログラミング言語のコンセプトはかなりラジカルに見えるらしく、「必要に応じて純粋でない操作も使えたほうが便利なのではないか」と思う人もいるようなのですが、そこで妥協して「少しだけ」純粋でない操作を含めると、そのような純粋な言語の便利な性質が「少しだけ」失われるのではなく、「すべて」失われてしまいます。だからこそ「モナド」を導入してまで言語全体の純粋性にこだわるのです。そもそも、一見純粋でない操作も使えたほうが便利なのではないかと思いきや、実際に使ってみると別に純粋でない操作は欲しくはならないのです。それは、「純粋な関数だけなのにまるで純粋でない操作があるかのように書ける」というのがモナドの魔術だからです。

さいごに

『純粋関数型』というからには、純粋性についてさぞ深く理解している必要があるのだろうと思いきや、実際にはまったく逆で、純粋関数型プログラミング言語でコードを書いている時には、純粋性など一切理解する必要も念頭におく必要もありません。
純粋性とはなにか、参照透明性とは何か、副作用とはなにか、そういうややこしいことをいちいち学習したくない物ぐさな人、関数の純粋性など難しくてよくわからないという人、ライブラリを使う前にドキュメントを隅々まで読み込んで注意事項を頭に叩き込むのが面倒くさい人、どうするべきかは理解していてもうっかりミスが多い人、純粋にしたほうがいいことがわかっているのについつい誘惑に負けて作用を使ってしまうような人こそ、純粋関数型プログラミング言語を学ぶべきでしょう。

関数型プログラミングを使ったり学んだりするなら、『純粋』関数型プログラミング言語をおすすめします。『すべて純粋』な関数型プログラミング言語と、『だいたい純粋』な関数型プログラミング言語には、『関数型プログラミング言語』とひとくくりにするのが躊躇われるほどの歴然とした差があります。

  • 苦労したい人はJavaScript/Reactを使いましょう。Reactのマニュアルを隅々まで読み込んで純粋性とは何かを正確に理解し、純粋性を壊さないための注意事項を余さず頭に叩き込んで、既存の関数をすべて純粋なものと純粋でないものに振り分け、純粋な操作だけを注意深く使用することでコードを純粋にしましょう。あなたの苦労が運良く報われれば、純粋性が保たれてReactがちゃんと動くかもしれません。うまく動かない場合は、純粋性が破壊されている可能性が心配です。コードを端からすべて見なおして純粋性が保たれているか確認しましょう(無理ゲーな理想論)
  • 楽をしたい人はPureScript/Halogenを使いましょう。特にマニュアルを頭に叩き込む必要もなければ純粋性とは何かなどとお勉強する必要もないので、適当にサンプルコードをコピペしてコードを弄り、コンパイルエラーで怒られたらそこだけ渋々修正してコンパイラを納得させるだけです。あなたがどれほど手を抜こうが、どうやっても純粋性は勝手に保たれHalogenはちゃんと動きます。うまく動かない場合でも、それは純粋性が破られていることが原因ではないので、その点については心配いりません(現実的な解決策)

追記

はてブコメより:

matsulib "純粋性は必須条件なの?ただのプラクティスなの?面倒臭かったら破ってもいいの?どこまで忠実に守るべき?"/reactでは自己責任で破れるものなんだろうけど、その「だいたい純粋」が原因で起こる問題ってなんだろう。

先の筆者の説明では『面倒臭かったら破ってもいいの?』とよくわかっていないふりをしてとぼけていますが、これは自己責任でも破ってはいけないものです。そうでなければ、こんなお節介をわざわざドキュメントに書いたりはしません。わざわざ破る必要もありません。reactはそれを破らなくても済むように設計されています。

たとえば、状態を表すオブジェクトの一部を書き換えても、reactはそれを検知できないので正しく表示を更新できません。新しい状態全体をsetStateして初めて正常に状態を更新できます。ですから状態オブジェクトは不変オブジェクトであるかのように扱わなければならないわけです。純粋でない操作が可能であると、そんなふうに状態の一部を書き換えて状態と表示の不整合を生じる可能性が出てきます。他にも、reactはコンポーネントをいつ何度書き換えるかは予想できないので、レンダリング関数に副作用があるとその副作用がいつ何回実行されるかわからず、予想の付かない結果を招きます。詳しくはreactについて学んでください。

上で述べたことを繰り返しますが、言語全体が純粋であれば「reactでは純粋性を自己責任で破れる」などという勘違いはそもそも起きませんし、そのように『だいたい純粋が原因で起こる問題ってなんだろう』と考える必要がないと言っているのです。matsulibさんのようなひとが、そのような疑問を抱く必要がないようにするために、そのようなことについて学習する必要がないようにするために、純粋性が役に立つのです。このテキストで主張しているのはそういうことです。

107
89
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
107
89