※ カジュアルに投稿したかったため、記事の清書や詰めができていませんがそれでもよろしければご覧ください。
手続き(命令)型のプログラミングに慣れきってしまうと関数型プログラミングを主体とした言語に学びの足を伸ばそうとしたときに文化的な違いから強い違和感を覚えてしまい学習が進まない現象が起きます。その文化的な違いを「手続き型脳」「関数型脳」なんて呼んだりすることが多いようです。私自身も初めて関数型プログラミング言語を学んだ際はその違いを受け止めることがなかなか出来ず頭の切り替えに1年以上要しました。その後、いくつかの言語を学びましたが関数型プログラミング言語Elmは言語仕様を主にWebフロントエンドのために限りなく削ぎ落とした上で関数型のエッセンスがギュッと詰まった入門にぴったりの言語だと感じています。今回はそんなElmの良さを活かして簡単なWEBプログラミングの例を通して、手続き型と関数型のスタンスの違いを感じてもらい関数型脳に素早くシフトチェンジ出来るようなお手伝いができればなと思います。関数型を学んでその良し悪しを学びたいと言う方やWebフロントってなんかしっくり来ないから学びを避けているな、なんて人に向けた記事になっています。
この記事ではいくつかの例をJavaScriptとElmの両方で動くサンプルを用意しています。これらのサンプルは結果全く同じになっています。JavaScriptの例は誰でも理解がしやすいように標準のDOM操作を利用したプログラムになっています。しかし、見る人から見ると変テコで「そんなコードは書かないよ」と感じられるかもしれません。あらかじめこの冒頭で意図を書いておきますが、これは敢えてそのようなプログラムを書いています。これはJavaScriptの柔軟にプログラムを書けてしまうと言う良いところと場合によっては悪いところを同時にお伝えするためです。逆にその柔軟な性質を利用することでJavaScriptを堅牢な書き方に変えることもできます。Elmを通して関数型プログラミングを学ぶことで、その学びをJavaScriptや他の言語に確実に生かすことも可能なため是非その旨味を掴み取ってみてください。
関数型のメリットとデメリット
結論から言うと以下が関数型(Elm)を使う・学ぶことのメリットとデメリットになります。ここはこれから先、紹介するサンプルで肌で触れて感じてもらうことになるため先に要点だけをまとめさせていただきました。
メリット
- コードを統一しやすい
- テストがしやすい(式変形で値になる)
- リファクタやリライトがしやすい
- コードの再利用がしやすい
デメリット
- プログラムを手続き単位でデバッグできない
- ボイラプレートが多い(行数が長くなりがち)
- 一般的なアルゴリズム知識が活かしにくい
- パフォーマンスの最適化がしにくい(ゲームプログラミングやグラフィック処理など)
偶数・奇数を求めるサンプル
このサンプルは2つの数値を足した合計の値が、偶数であるか奇数であるかを判定するプログラムのサンプルとなります。
JavaScript
JavaScriptの例では以下のアルゴリズムに沿って実装を行なっています。初歩的なAPIのみで構成しているので、細かく説明はしません。アルゴリズムの流れとその処理が他人に説明できるぐらい理解できるまで読み込んでみてください。
- DOMから要素を取得する
- 要素が持つ文字列要素を取得し、数値に変換
- 2つの数値を合計し、偶数・奇数の判定
- 偶数なら「even」奇数なら「odd」をDOMを操作して結果として書き込む
<span id="n1">4</span> + <span id="n2">2</span> is <span id="answer"></span>
const n1Element = document.querySelector("#n1");
const n1 = parseInt(n1Element.textContent, 10);
const n2Element = document.querySelector("#n2");
const n2 = parseInt(n2Element.textContent, 10);
const answer = document.querySelector("#answer");
if((n1 + n2) % 2 === 0) {
answer.textContent = "even";
} else {
answer.textContent = "odd";
}
Elm
- 2つの数値 n1(=4), n2(=2)を定義する
- n1とn2を使ってHtml(div, span, text)を定義する
- 数値はtext関数を通して、Htmlの文字列要素として定義される
- n1 + n2 の結果はevenOdd関数を通して、"even", "odd"の文字列に変換される
- 変換された文字列はspanタグの値として定義される
type alias Model =
{ n1 : Int, n2 : Int }
initialModel : Model
initialModel =
{ n1 = 4, n2 = 2 }
evenOdd : Int -> String
evenOdd n =
if modBy 2 n == 0 then
"even"
else
"odd"
view : Model -> Html Msg
view { n1, n2 } =
div []
[ span [] [ text <| String.fromInt n1 ]
, text " + "
, span [] [ text <| String.fromInt n2 ]
, text " is "
, span [] [ text <| evenOdd (n1 + n2) ]
]
「副作用が起きている手続きを隠蔽している」 ← ポイント
JavaScriptで組んだプログラムとElmで組んだプログラムのアプローチの違いを考え・感じてみましょう。JavaScriptはDOM操作のAPIと文字列変換や数値計算の道具を組み合わせ、一連の手続きの流れを作ることでプログラムを組みます。一方Elmは〜を定義すると言う、最終的な結果としてこうあって欲しいとだけ記述していることがわかります。このアプローチの違いが手続き型脳と関数型脳になります。
違いがある二つのプログラムですが共通点はなんでしょうか?そうです。数値を合計して奇数であるか偶数であるかを判定すると言う点で共通しています。実際成し遂げたい課題がそこに集約されていて、DOM操作であるであったりHtmlタグを組むことと言うのはWebフロントとしての仕様であったりフレームワークのアプローチの違いでしかありません。つまりプログラムにミスがあるかないか一番確かめたい箇所はevenOdd関数になるはずです。Elmの(だけで完結させたい)場合、今回私自身がDOM APIを利用しないアプローチで実装したわけではありません。このように組むことしか許されていません。これは足枷であり、強い武器です。おそらく多くの読者が思っている通り現代のWebプログラミングにおいてDOM操作をするケースは少なくなってきています。例えばXSSなどのセキュリティリスクであったり、やはり副作用を考慮しながらのプログラミングはミスを起こしやすいことがわかっているためです。今回のケースでは数値が正しく"even" "odd"に変換されるかテストがしやすい形になっています。またHtmlを組み立てるview関数にこのロジックを埋め込むこともできますが、Htmlがどう組み立てられるかも簡単にテストすることができます。まとめると今回のプログラムを通して以下のメリットがあることを確認できました。
- コードを統一しやすい
- リファクタ・リライトしやすい
- テストがしやすい
一方でElmでプログラムを組むことで以下のデメリットが生じます。敢えて深掘りはしませんが、「プログラミングを入門する」であったり「規模が小さい・大きいプロダクトを組むのであれば」のような視点でこのトレードオフがどう効いてくるかを考えてみましょう。
- プログラムを手続き単位でデバッグできない
- ボイラプレートが多い
乱数の偶数・奇数を求めるサンプル
続いての例は先ほどの数値の合計を奇数・偶数判定するサンプルの拡張を施したものになります。変更点は数値があらかじめ決め打ちではなく乱数を利用しています。
JavaScript
- 乱数n1,n2を生成する
- DOMから要素を取得する
- 取得したDOMに2つの乱数を書き込む
- 2つの数値を合計し、偶数・奇数の判定
- 偶数なら「even」奇数なら「odd」をDOMを操作して結果として書き込む
Elm
- 2つの数値 n1(=0), n2(=0)を定義する
- 乱数(0~100の間の2の数)を定義し生成をランタイムに任せる
- ランタイムが生成した2つを受け取りn1, n2とする
- n1とn2を使ってHtml(div, span, text)を定義する
- 数値はtext関数を通して、Htmlの文字列要素として定義される
- n1 + n2 の結果はevenOdd関数を通して、"even", "odd"の文字列に変換される
- 変換された文字列はspanタグの値として定義される
一見するとElmとJavaScriptのプログラムに大差がないように感じられます。しかしそこには大きな違いがあります。それはJavaScriptが命令のフローであるのに対して、Elmは定義の集合に過ぎない点です。
より具体的なことを言えば、JavaScriptは乱数を生成すると言う命令とDOMに書き込む、合計を求めると言う命令が地続きになっています。これぐらいの規模のプログラムであれば乱数が単なる数値でしか過ぎないことがわかり、合計が奇数・偶数かを判断する計算を切り離しテストを行えるようにリライトすることは可能ではありますが、そこには人の判断が入るためミスがないとは言い切れなくなってしましいます。また、乱数を含んでいる場合、リライト前と後でデグレードが起きていないことを証明することは極めて困難になってきます。
一方Elmのプログラムは乱数を生成することと、生成された乱数を使ってModel(n1, n2)を更新すること、Modelを使って偶数奇数判定の計算をすることHtmlを生成することは完全に分離されています。このような分離の仕方は極めて理想的であると言えますが、Elmはこの書き方を強制されています。つまりこのような書き方しかできない作りになっています。これはとても強力な機構と言えます。
いくつか言葉でメリットを並べましたが、プログラムを見て冗長さと難解さがあるのではないか?と言う違和感を抱いた人も少なくないと思います。私自身も最初に乱数(副作用を起こす)を生成するプログラムを見たときにひどく苦痛を感じました。高々、乱数を生成するだけのプログラムなのにrandomPairsのようなジェネレータを作る必要があったり、乱数の結果を得るのも一苦労です。しかしこのような形にすることで、乱数が正しく生成できているかのテストを書くこともできますし、この乱数を生み出す機構や今まで通りevenOdd関数などを再利用することができます。短いプログラムを書くこと以上にフレームワークに守られることのメリットはプログラムの規模が大きくなればなるほど重要になってきます。それを考えると多少の冗長さは書けば書くほど気にならなくなってきます。
- コードを統一しやすい
- リファクタやリライトがしやすい
- テストがしやすい
- コードの再利用がしやすい
イニシャルリストを求める
最後は名前の情報の列からイニシャルのリストを生成するサンプルです。
JavaScript
- ulの要素を取得する
- 名前のリストをforEachで回し以下の処理を繰り返す
- 名前からイニシャルを生成し、li要素を作る
- ul要素に追加する
Elm
- 名前のリストをliの列に変換する
- イニシャルの文字列要素をliの子要素と定義する
- 変換されたliの列をulの子要素と定義する
これはとても短いプログラムですが、わかりやすく手続き型プログラミングと関数型プログラミングに差が出る例です。イニシャルを求める計算は手軽なのでインラインに書くことはさして問題はないのかもしれませんが、ループ操作+DOM操作と言う副作用にまみれた処理の中に書かれているため例のよってテストとして書くのはとても難しいです。今回はforEachで書いているため多少マシですが、forループを使いindexで名前を参照しているプログラムを書いてしまうこともできてしまう。すると、テストをするために危険なリライトをする選択をしなければなりません。Elmの場合は単なる定義と変換で構成されているためテストもテストのためのリファクタも容易です。
まとめ
後で書く