Dev日記

プログラミング学習のまとめなど

関数型よく知らない人の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 です。

これはいったい何なのでしょうか?


結論からいいますと、これは ジェネリクス です。

JavaList<Integer> をelmにすると、 List Int になります。

この辺は実に関数型らしい部分ですね。型宣言のオプションとしてジェネリクスがあるのではなく、第一引数として型を受け取るイメージです。

よって Cmd MsgJavaっぽく書くと、 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;
}

ただしJavaenumではちょっと機能不足。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だと対応する機能が完全にあるわけではないので、あくまでだいたいのイメージをつかむためのものと思ってください。