Runner in the High

技術のことをかくこころみ

Interpreter Pattern in Elm: 副作用をテスタブルにする

Elmは純粋関数型プログラミング言語なので基本的にはアプリケーションを構成するすべての関数はテスタブルであるが、唯一Cmd型だけはテストすることができない。

たとえば以下のようなupdate関数においてPersistToStorageのメッセージが渡された際に必ずストレージへの保存を実行するCmdが発行されているかどうかをテストしたいとする。

update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
    case msg of
         PersistToStorage value ->
               ( Saving, persistToStorage value )

         -- ...


-- ports


port persistToStorage : Encode.Value -> Cmd msg

ElmアプリケーションにおいてCmdは一度ラップされてしまうと、中のmsgを取り出すことはできない。またカスタム型であるためelm-testにおいて等価チェックのアサーションも使うことができない。以上の理由からCmdはテスタブルな型ではないということが分かる。

もちろん、CmdをテストせずModelの状態のみをチェックすることで実質副作用が発行されていることと同義とすることも可能である。Elmアプリケーションにおいて副作用というのはアプリケーションの状態に直接的に影響を与えるものではないし、そもそも「副作用が発行されているかどうかをテストするべきなのか?」という疑問は常に持っておくべきだと言える。

しかし、どうしても副作用をテストしたいという場合にはInterpreter Patternを用いることで副作用をテスタブルにすることができる。

Interpreter Patternとは

Interpreter Patternはざっくりいうと「アプリケーションの処理と実装を分離する」ためのパターンにあたる。ScalaHaskellなどの関数型プログラミング言語の文脈においては有名なパターンで、計算機の実装などがよくある例である。

www.youtube.com

より具体的な実装方法で言うと「アプリケーションがどう動くか」を型など純粋なもので表現し、その型を解釈(Interpret)する別の関数に副作用の発行を移譲することで副作用のある部分と純粋な部分を切り離すという形になる。

ElmにおけるInterpreter Pattern

さて、ElmにおいてInterpreter Patternはどのような実装になるかというと、ただ単にupdate関数の副作用を自前で定義したカスタム型で置き換えるだけである。こうすることでupdate関数は完全にテスタブルな型のみを返すようになる。

update : Msg -> Model ( Model, ExCmd )
update msg model =
    case msg of
        PersistToStorage value ->
              ( Saving, ExPersistToStorage value )

        -- ...


type ExCmd
      = ExPersistToStorage Encode.Value

そして、ExCmdを解釈してCmd型の発行を行うInterpreterとなるtoCmd関数を用意し、main関数の呼び出し時にupdate関数と接続する

toCmd : ExCmd -> Cmd msg
toCmd exCmd =
    case exCmd of
         ExPersistToStorage value ->
               persistToStorage value


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = (\msg model -> Tuple.mapSecond toCmd <| update msg model) 
        , subscriptions = \_ -> Sub.none
        }

これでCmd型に依存するのはtoCmd関数だけになったため、もともとの目的であった「update関数はPersistToStorageのメッセージでストレージへの保存を実行するCmdを発行しているか」を以下のようにテストすることができる。

suite : Test
suite =
    test "update" <|
        \_ ->
            init
                |> update (PersistToStorage "this is an apple")
                |> Tuple.second
                |> (\exCmd ->
                    case exCmd of
                        ExPersistToStorage _ ->
                            True

                        _ ->
                            False
                )
                |> Expect.equal True

これでupdate関数を完全にテスタブルなものにすることができた。

余談

Elmにおいてはアプリケーションでランタイムエラーが起きることはないため、どちらかといえばテスタビリティを上げるという観点でInterpreter Patternのような設計を行うことが多い。

見方を変えれば、Elmアプリケーションの実行を司るElmカーネルそのものがJavaScriptとの連携を行うInterpreterとしての役割を果たしているため、他の言語におけるInterpreter Patternのように「実行時エラーが起きる可能性があるレイヤとそうでないレイヤを分離する」というような実行時安全性を維持するための分離はElmにおいて全くもって必要ない。

もちろんJavaScriptやTypeScriptでは必要ではあるが。