関数型よく知らない人のelm入門(1) - update関数をバラす
普段色々勉強していることを、せっかくだからBlogにまとめていきたいと思います。
第一弾は elmhttp://elm-lang.org/。Haskellっぽい記法を持ったWebフロントエンド用言語です。
まだまだフロントエンド自体も入門したてですが、実戦で利用するかはともかく勉強する価値はあると判断しました。
しかし関数型がベースで色々クセが強いのも事実。勉強しながら理解していったことをまとめていきたいと思います。
elmとは?については省略。検索すれば色々でてくると思いますので、苦しんだ部分の解説を中心にしていきます。
第一回目のテーマは、update関数の引数/戻り値について。
elmのupdate関数 - Reduxのそれとほぼ同じ役割を持つ関数 - ですが、なかなかに初見では苦しめられます。普段オブジェクト指向で、関数型の経験がほぼない自分は理解に相当苦しみました。
公式のサンプルなどにあるupdate関数がこちら。
update : Msg -> Model -> (Model, Cmd Msg)
この時点で結構な人が脱落する気がしますね…Haskell等に触れてないとおそらく厳しい。
そこで比較的型にキッチリしてて、かつ利用者が多そうなJavaと比較しながら解説していきたいと思います。
(1)引数
まず引数と戻り値。ここで躓く人が多そうなので、理解すれば簡単なことではありますがちょっと念入りに。
最初のほうだと、 String -> Int -> Int
という表記ですら混乱します。
例としてJavaの Integerクラスにある、 int parseInt(String s, int radix)
をelmにしてみると..?(※Exceptionについては無視)
parseInt : String -> Int -> Int
となります。ようは、「Stringを第一引数、Intを第二引数とし、Intを戻り値とする関数」 になります。
最後の型が戻り値で、それまでは引数の組と思えば理解が早いです。(といってもちょっと進むとこの理解では厄介なのですけど…)
よって update関数、 update : Msg -> Model -> (Model, Cmd Msg)
は
MsgとStateを引数とし、 (Model, Cmd Msg) を返す関数 といえます。
強引にJavaっぽく書くなら
(Model, Cmd Msg) update(Msg msg, Model model)
となるでしょう。
(2) (Model, Cmd Msg)
とはいってもこの戻り値の型、 (Model, Cmd Msg)
とは何なのでしょうか?
まず ()で囲まれたもの、これはいわゆる Tupleを表します
最近の言語ではTupleが実装されているものが多いですが、Javaにはないので完全に例えることができませんね…強引にかくなら
class Tuple<T1, T2> { T1 left; T2 right; Task(T1 left, T2 right) { this.left = left; this.right = right; } } //updateを置き換える Tuple<Model, Cmd Msg> update(Msg msg, State state)
こんな感じでしょうか?ここは理解しやすいかと思います。
とはいえまだ置換しきれていない部分があります。そうです、 Cmd Msg です。
これはいったい何なのでしょうか?
結論からいいますと、これは ジェネリクス です。
Javaの List<Integer>
をelmにすると、 List Int
になります。
この辺は実に関数型らしい部分ですね。型宣言のオプションとしてジェネリクスがあるのではなく、第一引数として型を受け取るイメージです。
よって Cmd Msg
をJavaっぽく書くと、 Cmd<Msg>
になります。
(3)完成!
これでパーツが出そろいました。update関数をJavaに置き替えてみましょう。
まず元となるupdate関数は
update : Msg -> Model -> (Model, Cmd Msg)
でしたね? まずは引数を抽出しJavaっぽく
(Model, Cmd Msg) update(Msg msg, Model model)
さらにTupleもJavaっぽい記法に
Tuple<Model, Cmd Msg> update(Msg msg, Model model)
最後にCmdのジェネリクスを変換し
Tuple<Model, Cmd<Msg>> update(Msg msg, Model model)
完成です! こう書くとぐっと理解しやすくなったのではないかと思います。
(4)さらにJavaっぽく
それでは各クラスも役割からJavaでよく使うクラスに変換してみましょう。
注意点として ModelとMsgは「自分で定義した型」、Cmdは「標準パッケージの型」 になるので注意です。
Model
Modelは一般にアプリケーションの状態を格納するクラス(構造体)です。
なんでもいいですが、Javaっぽく書くなら
class Model { int state; boolean isLoading; String[] result; ChildModel child; }
こんな感じでしょうか? とにかくデータのカタマリです。
Msg
一方でMsgは、何らかのイベントの種類 みたいなものです。「xxxしてくれ!」だったり「xxxしたよ!」というものですね。
これに応じてupdate関数内で処理を行い、新しいmodelを返すわけです。
なので MsgはJavaで書くならenum が近いといえます。
//まだ経験が浅いので、elmでこういう命名が適切か分かっていないです enum Msg { RequestData, OnDataReceived, UpdateCheck, OnKeywordChanged; }
ただしJavaのenumではちょっと機能不足。Msgにはパラメータを付属させることができます。
F#用語ですが、判別共用体が最も近いのではないかと思います。Swiftのenumともいえるでしょう(たぶん)。
Cmd
一方でCmdはちょっと厄介です。非同期処理というのがイメージしやすいでしょうか。
Java標準パッケージだとFutureクラスが近いですかね? なんらかの処理を行い、結果を上記で宣言したMsgとして返すものです。
//サーバと通信するタスクとする Future<Msg> future = executor.submit(new Callable<String>() { public Msgcall() { String result = httpAccess(url); return new Msg.OnResultReceived(result); //javaではこれはできませんがイメージはできると思います }});
流行りのRxJavaで書くなら Single
でしょうか?
//実際にこんなメソッドはないですがイメージとして Single<Msg> task = Single.create({ String data = httpAccess(url); return new Msg.OnDataReceived(data); })
ここで返したMsgをもとに、再びupdate関数が呼び出されて状態(Model)が更新される、ということです。
サンプル
ためしに、
- RequestDataメッセージでサーバアクセス開始
- OnDataReceivedメッセージで受信完了
というupdate関数をJavaで作ってみましょう。
//状態。データは文字列とする class Model { boolean isLoading = false; String result = null; } //メッセージ一覧 enum Msg { RequestData, OnDataReceived; } Tuple<Model, Future<Msg>> update(Msg msg, Model model) { switch (msg) { case RequestData: { //実際はHttpアクセスのTaskをCmd化するなどもちょい面倒です。 Single<String> http = httpAccess(url); Single<Msg> cmd = http.map(x -> new Msg.OnDataReceived(x)) model.isLoading = true; return new Tuple(model, cmd); } case OnDataReceived: { //判別共用体がないのであくまでイメージ... model.isLoading = false; model.result = msg.result; return new Tuple(model, null); //nullが何も処理なし。elmでは Cmd.none } default: return new Tuple(model, null); } }
という感じになります。Javaだと対応する機能が完全にあるわけではないので、あくまでだいたいのイメージをつかむためのものと思ってください。