関数型愛と、ながいまえがき
ReaderTパターンというデザインパターンがHaskellには存在する。
今回はそれが他言語の人でも役に立つ考え方なのではないかと思ったので、書き始めてみた。他言語の人でも役に立つ「ReaderTパターン」と銘打って話をはじめてみたい。そこから、プログラミングでは本当に何をやっているのか、分からなさにたいして、私が愛する言語たちがどうやって対処しているのかを少し見てみたい。
関数型言語のことを知らない人に、この愛がちょっと伝わったらいいなと思っている。
ReaderTパターンは、Haskellなりに実行時に決められる定数をアプリケーションレベルで定義する方法だ。
ちょっとした自戒だが、書く必要のないところまで記事のスコープを広げてしまって、記事を完成させることに苦戦していたので、今回は絞って文章を書こうと思う。
記事を書くに当たって読者の視点を想像してみよう。
他言語の人から見てHaskellはどのように映るだろうか?どうしても数でいうと知らないよっていう人が多い。何かコマンドを覚えるみたいでワクワクしたが、それと同時に第一印象難しそうというか、変に小難しいように見えた。
例えばHaskellにはモナドという概念が登場する。大学数学の圏論みたいな、めちゃくちゃ難しい議論も登場する。それに純粋関数型言語だ。
慣れているオブジェクト指向からは遠く離れていると思う。関数型に熱心な人の中には激しく勧めてくる人もいるから、そこまで勧められてしまうともうやる気がなくなったりもする。(やっぱり好きなものは自分で選びたいものだ)
色々な人が関数型を語っている後、なぜ私が語るのか
色々な人が関数型プログラミングの魅力については語っている。だから、あんまり新鮮なことは言えないかもしれない。なので、私は身近な人に向けて書こうと思う。やっぱり最終的に、関数型プログラミングは素晴らしいなと思う。戻ってきてしまう。
私は暗黙的な知識を覚えるのが苦手だった。「ここの値にはundefinedが来るかもしれない」「このauthenticate関数の第二引数には、パスワードではなくパスワードハッシュをいれないといけない」みたいなコードに明示されていないことに気をつけながら実装することが苦手だったし、なにより必然性が無いように見えた。
たとえば、pythonのwebアプリケーションのフレームワークであるDjangoなんかは、触っていて気持ち良くはなかった。「これがあると便利だ」以上の論理的な必然性や美しさが、当時の自分には感じられなかった。
必然性や美しさについてもう少し言葉を費やしたい。
他にもDjangoと同じ理由で、私はVueが好きではない。Vueは、ただ便利だからといって様々な記法をユーザーに押し付けている。(これは偏見)
一方好きな言語はどういう必然性を持っているか?私はHaskellとEllmとClojureが好きだ。圏論というバックグラウンドがあるHaskellや、シンプルにWebアプリケーションに特化するという理念があるElm、コードすらデータとして見るClojure(LISP)。
3年ぐらい実務経験を持っているReactも好きだ。JSXが嫌いだという声を聞いたことがあるが、逆に捉え返すならJSX以外は一貫して関数を採用していると思う。(歴史的にはクラスコンポーネントがあったり、ErrorBoundaryは結構アクロバティックだったりするので、一貫性があるというような断言は避けておく)
大胆に言うと、私が好きな関数型プログラミングは引き算のパラダイムだ。ごうつくばりなのではなく、制限をかけることで研ぎ澄ますものだ。
…関数型言語まで風呂敷を広げすぎてしまった。話を戻すと、ReaderTパターンのいいところはImmutabilityであること、そして型として明示できること。
Haskellには様々なモナドが存在している。勉強していて色々なものに出会った。ListモナドやMaybeモナド、継続モナドなど、様々なモナドなるものが存在している。その中にReaderモナドというものがある。ReaderTパターンとは、Readerモナドを利用した、アプリケーションを実装する時のデザインパターンだ。
Haskellにはデザインパターンが存在しないと言われる。言語機能や型で解決することを好む(Immutabilityやモナド型など)
ReaderTパターンとは、そんなHaskellで、アプリケーションを実装するときのデファクトスタンダードのデザインパターンだ。
ReaderTパターンとは何か
ReaderTパターンとは何かを本当に理解しようとしたら、以下のことを実装したりして理解する必要がある。
- モナド
- モナド変換子(Monad Transformer)
- Readerモナド
- ReaderT(Reader Transformer)
だが、この記事を読むに当たっては、こちらだけで十分だ。
- モナドは文脈を作り、最終的に文脈で値をくるむ(広く抽象的な意味で使っている。)
- モナド変換子(Transformer)はマトリョーシカみたいにモナドを組み合わせられる
- Readerは「特定の値を読み取れる」文脈を定義する。
ReaderTとは、Readerモナドの**モナド変換子(Transformer)**だ。Readerモナドを他のモナドでも使えるようにしてくれる。
モナドとは
モナドは文脈を作れる。そして、文脈を知らないと出来ない計算を直列に組み合わせることが出来る
Readerモナドとは、「特定の値を読み取れる」文脈を定義するものだ。この文脈の中では、特定の値を読み取る能力がある。つまり、Readerによって文脈を始めたとき、その文脈の中ではask
という値を利用できる。
ask
はReaderの文脈に入っている値を取り出すことができる。
モナドは、郷である。郷に入っては郷に従え。郷に入ったら、askということで値を参照することができる。
モナドは今まで腐るほど例えが出されている。Promiseだってモナドだ。でも、モナドはポケモンではない。抽象的なものを本当に理解するためには具体的に使う必要がある。
Readerモナドとは
他のプログラミング言語の人たちにとっては、Readerって関数とすごく似ていると思う。Readerの文脈で参照できる値は、関数で言えば引数だ。
(argument: string) => string[]
という型の関数は、Reader String String[]
であると言える。
引数は引数でも、この文脈の中では変更できないということが大切だ。例えばJavaScriptでは値を再代入できることがあったりする。一方、Readerモナドでは値自体を変更することが出来ない。これをImmutabilityというのだが、それが一番重要である。
Readerは文脈を限定する。参照できる範囲を、文脈を限定する。値を参照できる場所を、Readerモナドによって始められたモナドの場所の中だけに限定する。
入れ子にできるReaderモナド
Readerは参照範囲を限定する能力がある。それも、入れ子にすることができる。入れ子にできるのは関数と同じだ。関数の中で関数を定義できるのと同じだ。
入れ子にできるというのはどういうことか?
アプリケーション全体でログレベルを設定したいという要件がある。
アプリケーションではInfoレベル、デバッグしたい関数ではDebugレベルにする。そういう、「ここから一歩進んだら森だからリュックからライトを取るようにする」「森から出たらライトはリュックに仕舞う」みたいに、値の参照範囲を限定することができる。
なぜ関数ではなくReaderか?
関数ではなくあえてReaderを使う理由は何だろう?要するに関数じゃないか。
reactを実装していて、reduxやuseContextを使ったことがある人には少しわかるかもしれない。他にも、グローバルレベルの状態を暴走させずに管理しようとしたことがあるなら共感できるだろう。本当はグローバルな定数にしたいが、コードを書いているときには決定できず、実行する時に決定したい値たち。
関数の引数と、Readerの力で参照できる値は扱いが違ってて、Readerのほうが「環境変数」っぽい。
まさにそうやって環境変数をうまく渡すのがReaderTパターンだ。環境変数をどうやってアプリ全体に渡すか。Immutabilityとともに、中身の関数に勝手に環境変数を編集させずに環境変数を渡すか。
javascriptとかは引数の値を再代入できたりすることがある。そういうことを絶対にさせない。それがReaderである。それが限定された範囲内でできるのが「参照」ということだ。
Immutabilityの良さ
プログラミング初心者だった頃kotlinを学習した。ちょうどdata classが実装された直後だった。様々な言語でImmutabilityがどんどん取り入れられて行っているということ。Immutabilityが良いものであることは理解されて行っている。
Immutabilityとは不変性である。データが変わらないことを保証する。
JavaScriptを勉強しているとき、varよりconstを使おうということを聞いたことがある人もいるだろうと思う。それは、まさにImmutabilityである。
Immutabilityは、「こいつはそういう権限ないから大丈夫だろ」という安心感を生み出してくれる。会社内で誰でも機密データを改ざんできるなら、改ざんの犯人は絞れない。逆に、役職を持っていないと編集できないのなら、犯人特定が容易だ。
予測不可能性に対処する
予測不可能性に対処する2つの方法がある。
- stateを使わないことでImmutableにする
- stateを使うときは明示的にする
オブジェクト指向のように、状態を隠蔽すると腐敗してしまう。うまいこと発酵させる技術力がなければ、状態は避けよう。
実はReaderTの文脈の中で「変更可能な値」を参照できるようにもできる。Haskellの汚れた魔法だ。
stateを使っていいところはどこだろう
基本的にstateはだめだって思っている。じゃあ、オッケーになる条件ってなんだろう。
immutableの利点ばかり見てきた。それは片方しか見ていない。stateであることのメリットって何だろう?限定的なスコープで使えば、stateだってうまいことハマる場面があるはずなんだ。
ドメイン的にあまり重要でないところは使ってもいいのではないか?
statefullである必要があるところって一見わからない。
例えば、ゲームとかはどうだろう。ゲームやUIなど、ユーザーと直に接触するようなところ。頻繁に変わってしまう、予測不可能なところと接するときはstateがあったっていいんだ。正確にはstateなしでは要件内に予測不可能性に対処できない。
フロントエンドに特化したelmだってstateがあると言えばある。elmでは、M(e)s(ssa)g(e)を定義することで、予測不可能なユーザーを予測可能なステートレスな関数へと変換している。
ユーザーはエントロピーが大きすぎる。
stateがあることで、対応できることがすごい広がる。
思いもよらないバグも生まれる側面が大きいけれど、思いもよらない素晴らしいことが楽に出来たりする。
だけど、そういうことが求められることは、ビジネスロジックではあまり頻繁にない気がするんだよな。ビジネスロジックではもっとかっちりしているものが求められている。
予測不可能であるということが問題なんだ。予測不可能性が良いとされるのは、予測不可能性のスコープが小さくて、管理できる場合だ。管理できる予測不可能性は、万華鏡のように素晴らしいものになる。
でも、グローバルなところで、全てに影響するところが予測不可能なstatefullになってしまっていたら、それは万華鏡というよりカオスに入れ込まれているみたいなことだ。
stateを使うときには、できるだけそのスコープを小さくする。つまり、stateを使っているような関数をできるだけ少なくする必要がある。それは、予測不可能性を低くするためだ。
結局ReaderTって、実行時に決定する「constantモジュール」なのだ。定数を参照できる範囲を限定する。それがReaderだ。
引数を少なくすることで、予測不可能性を下げる
関数の引数を少なくしたほうがいいという話を聞いたことがあるだろう。これは、Immutabilityと同じ要因である。組み合わせの爆発を防ぐことができる。組み合わせの爆発も同じく予測不可能性だ。
Immutableにする。関数で利用する引数は少なくする。
Has型クラス
HaskellではReaderTパターンと一緒にHas型クラスパターンがよく使われる。それは、引数はなるべく少なくということ。組み合わせの爆発を防ぐのHaskellなりの別の手段である。(なので、Haskellを使わない人は「引数の情報を少なくして複雑さを減らしているんだな」ぐらいでいい)
ReaderTパターンをHas型クラスと一緒に使うといいところは、stateを使う関数を型で明示することができるということなんだ。
Haskellでは型クラスというものが出てくる。
a :: HasState e => ReaderT e IO ()
a = do
env <~ ask
let
state = getState env
in ...
こんな感じだ。上から下に、<-
で値を取り出しているんだな?ぐらいで見ていけばいい。これはHaskellだけの記法だから。
こういうふうにすれば、このa
はstateだけを要求していることが明確なる。明示的になる。明らかになる。だから、意識的になる。
反対に、HasState eという型クラスを使っていない限り、stateを使っていないということが保証されるということだ。
stateを使うときは明示的にする
型によって明示的にstateを使っているかがわかるようになっていれば、それを意識することができる。明示的に示されているものは、人間は意識することができるのだ。
明示的なものは意識に入る。だから、予測するための体力が豊富にある。もちろんそもそも状態は無い方がわかりやすい。だけど、存在するならば明示的にする。上司には懸念点を報告する。危険なところがあるなら標識を立てる。そういうことだ。
逆に、明示的な言語のデメリットも存在する。それは、明示的な言語は学習コストが高くなってしまうということ。上司に報告をするのはおっくうだ。危険なところかどうか確認して標識を立てるのはめんどくさい。
ただし学習コストは、めんどくさいものではなく、むしろ将来の不安を取り除いてくれるものだ。将来間違ったことをすることにならないか?そういう不安を取り除いてくれる。
そうなんだ。
関数型言語、特に静的型付け言語って、わからないということが明確になるのがいい。分からなければ、知識をつけて勉強すればいいだけである。
勉強しなくてもなんとなく使えてしまうが、それ故にドキュメントも読まないし、やるなと書かれていることをどんどんやってしまって時間を食ったり、他人のコードに影響する変なコードを書いてしまって、他の関数がデグレするみたいな言語がある。pythonやJavaScriptはどちらかというとそちら側よりだと思っている。
万人の万人に対する闘争である。
分からなくても動かせる言語は、今は大丈夫だとしても自分の首を締めてしまう言語だ。勉強を先延ばしできる言語。サボっていい言語。短期的に人間をサボらせる言語。
それは、あまり良くないんだ。サボらせる言語は、実はサボれるわけではない。
正しくは、長期的には人間側が頑張らないと、正しさを保証しづらいということなんだ。長期的に見ると悪な言語。短期的に人間をサボらせる言語。
反対に、Haskellとかelmとかは、先に勉強を要求する。でも、長期的に見て自分を助けてくれる言語なんだ。先に慣れておけば複利でどんどんご褒美をくれる、楽にしてくれる。長期的に考えるのが苦手な人間を一番助けてくれる言語たちなのである。
予測可能にするために、私の愛する言語たちが取っている手段たち
人間や世界の予測不可能性に対して、様々な言語が様々に対応している。十言語十色だ。
オブジェクト指向言語は、主に予測不可能性を隠蔽してなかったことにしようとする。これは、ちゃんと考えられる人にしかうまく行かないのだった。では、例えば私が愛する言語たちはどうやって対応しているんだろうか?
私はClojureとHaskellとElmが好きだ。それぞれ、予測可能にするための手段が違う。表情が違っていてとてもおもしろい。
Clojure
Clojureはデータの種類の少なさ、そしてコード自体もデータとして見ることで対処している。
データは、基本的にリストと辞書しかない!だから、データ操作についてもその2つに関連するものを使えば色んなところで使えるはずだ!
根本的な、辞書と配列のことについて理解したら、Clojure力は一段上に行けると思うんだ。だって、色々なところで共通的なことをできるから。
コードだって、文字列のリストじゃないか!だから、関数を実行して関数を作り上げたっていい!
たとえばprint-a, print-b, print-c, ..., print-zという関数を実行時に作れてしまったりもする。コードだってデータだからだ。これをマクロという。(ただ、マクロは予測不可能性が高いので避けるべきだ。実行して関数にし→更に関数を実行しないといけないから、正しいと確認するまでとにかく時間がかかる。大いなる責任をもって扱うべき大いなる力だ)
Haskell
Haskellは静的型と圏論の力で対応している。
静的型は実行前に明示的にありかなしか確認できる。コンパイルエラーを見たことがあると思う。そもそも型にあっていないおかしいソースコードは、実行可能にすらならない。だから、間違っていたらすぐに見ることができる(=予測可能性が高い)
そして、数学の圏論に裏打ちされた多大なる抽象化力。抽象的なものは要素が少ないので、理解するまでに時間はかかるが理解してからは確認することが少なくすむ。
Elm
Elmは静的型と諦めることによって対応している。
静的型はHaskellと同じだ。
その他に関してはHaskellの正反対を行っている。Haskellみたいな難しい機能は一切諦めて、パターンマッチと型、関数だけに絞った。慣れてしまったら実装していて退屈だと感じてしまうぐらいだ。それぐらいシンプルだ。
諦めたのは、抽象化力。
そして、やれることも少なくして諦めた。フロントエンドしか作れないようにした。この言語ではフロントエンドに特化したフレームワーク一つしか使うことが出来ない。それがいい。フロントエンド特化型、めちゃくちゃシンプル実用言語だ。
バージョンアップすることすらできるだけしない方針で、安定している。それぐらいシンプルである。
あとがき
ReaderTパターンを書いているうちに、急遽関数型言語への愛へと話が膨らんでいった。色々なことを書いたけれど、伝わっている自信はあまりない。
もし関数型言語を知らない人で、ちょっと興味をもった人におすすめの資料を以下に。
基本的に一番最初におすすめしたい関数型言語はElmだ。私が実務で使っていたとき、HTMLとCSSしか勉強したことがないという社長の息子が、Elmを使って作られたWebアプリケーションを保守できるようになった。Elmの公式ドキュメントは非常にわかりやすいので、そちらで十分だと思う。
実行して対話的に見ていくのが好きな人にはClojureがぴったりだ。ClojureにはREPLという対話的な実行環境があり、コードを書きながらサクサク動作確認できる。REPLを使えば、Clojureの文法をその場で手軽に確認しながら学習を進められるのだ。
Clojureをプロダクトに導入した話#コードを書きながらシームレスにREPLでサクサク試せる(REPL駆動開発)
Clojureをプロダクトに導入した話#Lispの読み書きに慣れるまでが大変(に見える)
色々な抽象的な概念を学んでパズルを解くように楽しく実装したい人は、HaskellかPureScriptを見てみるといい。モナドは抽象的な概念だが、一度理解すると様々なプログラミングの問題を統一的に扱えるようになる。モナド変換子などのテクニックも学べば、より柔軟で表現力豊かなコードが書けるようになるだろう。
学習は一筋縄ではいかないかもしれない。でもきっと新しい世界が見えてくるはず。