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では必要ではあるが。

elm-multi-waitableで非同期処理の完了待ちをModelに表現する

Elmアプリケーションにおいて非同期処理の完了待ち実装をいい感じにするelm-multi-waitableというモジュールを公開した

github.com

内部的には状態遷移のタイミングでデータを受け取って保持するステートマシンのようなものだが、これを使うことでフロントエンド・アプリケーションによくある「非同期ないし直列に複数データの取得を行い完了待ちをする」というような処理の実装を改善できる。

非同期処理をModelで表現する難しさ

例えば、以下のようにLocalStorageへのアクセスと複数のWebAPIへのリクエストの完了待ちが必要になるケースがあるとする。

また、すべてのリクエストが完了するまではローディング中画面を出し、すべてが完了したらローディング完了画面を出すという仕様がある。

f:id:IzumiSy:20200711234003j:plain

この仕様のModelを最も簡単に表現するのであれば、データの取得が完了している/完了していないをMaybeで素朴に表現することになる。

すべてがJustになっていればview関数でローディング完了画面を出すといった具合になる。

type alias Model =
    { auth : Maybe Auth
    , user : Maybe User
    , option : Maybe Option
    }

これはこれで一旦動くものは作れるが、以下の問題点を含んでいる。

  • データを扱うのに毎回Maybeを剥がす処理が必要になりコードが冗長になる
  • 直列にすべてのデータを取得する実装ではデータの取得順が変わる度に完了チェックの場所が変わる(UserとOptionの取得順を入れ替えるような場合には都度完了チェックのタイミングも変える必要がある)
  • 並列にすべてのデータを取得する実装では個別のデータ取得完了毎に完了チェックの処理が必要になる(どの順番でデータの取得が完了するか分からない&完了チェック自体が抜け漏れる可能性がある)

そもそも、値をMaybe型で包んでいるのはローディング画面を出したいという仕様のためだけであって、ローディング画面が終了したら存在しているものはMaybeである必要がない。可能であればMaybeではない型であってほしい。できれば、以下のようなデータ構造であってほしい。

type Model 
    = Loading -- ローディング中
    | Loaded Auth User Option -- ローディング完了

しかし、このコードは理想論であって実際には実現できない。なぜならローディング中には非同期的にLocalStorageやWebAPIからのデータが取得されていくため、取得過程のデータも保持する必要があるからだ。

ここでelm-multi-waitableを使うともう少しだけ近い形で実現することができる。具体的には、以下のようなModelの設計にすることができる。

type Model
    = Loading (MultiWaitable.Wait3 Msg Auth User Option)
    | Loaded Auth User Option

elm-multi-waitableの使い方

まずinit関数でLoadingの初期化を行う

init : Model
init =
    Loading <| MultiWaitable.init3 Done

MultiWaitable.initN関数は待ち合わせているすべての非同期処理が完了した際に発行されるMsgを第一引数に取る。

第一引数で指定されているDoneは完了待ち対象となるデータの型(今回の例ではAuth, User, Option)をタグに持つもので、Msg型は以下のような設計になる。

type Msg
    = AuthFetched Auth
    | UserFetched User
    | OptionFetched Option
    | Done Auth User Option

上記のMsgをハンドリングするupdate関数は以下。

update : Msg -> Model -> ( Model, Cmd msg )
update msg model =
    case (model, msg) of
        ( Loading waitable, AuthFetched auth ) ->
            waitable
                |> MultiWaitable.wait3Update1 auth
                |> Tuple.mapFirst Loading

        ( Loading waitable, UserFetched user ) ->
            waitable
                |> MultiWaitable.wait3Update2 user
                |> Tuple.mapFirst Loading

        ( Loading waitable, OptionFetched option ) ->
            waitable
                |> MultiWaitable.wait3Update3 option
                |> Tuple.mapFirst Loading

        ( Loading _, Done auth uesr option ) ->
            ( Loaded auth user option, Cmd.none )

MultiWaitableはupdateのための関数としてwaitNUpdateN関数群を提供しており、たとえばそのうちwait3Update1関数は以下のようなシグニチャになっている。

wait3Update1 : a -> Wait3 msg a b c -> ( Wait3 msg a b c, Cmd msg )

この関数はaを適用して更新されたWait3型の値と、完了時であればinit3関数で与えられた完了Msgの発行コマンドをタプルで返している。

MultiWaitableモジュールを使えば、update関数においてはWaitN型の更新だけをすればよく、待ち合わせ中のデータがすべて揃ったかどうかを開発者自身が都度気にする必要はない。また、機能追加による改修の際にも、Modelの設計からローディング/ローディング完了という状態が存在しているとひと目で分かることや、init関数の初期化部分で完了時のMsgが把握できることなどから可読性も高くなる。

Elmにおいて複数の非同期処理の完了待ちはModelの設計が比較的難しい部分であるが、elm-multi-waitableを使うことでModelのデータ構造をシンプルかつ分かりやすく表現させることができる。

関連パッケージ

elm-multi-waitableは複数Taskの完了待ちを支援するelm-task-parallelというパッケージから大いに影響を受けている。

izumisy.work

また、非同期処理の完了待ちに関してはリチャード・フェルドマンもelm Europe 2018の"Make Data Structures"で同様の話をしている。

f:id:IzumiSy:20200712105918j:plain "Make Data Structures" by Richard Feldman

彼のトークスライドはIndexeDBとWebAPIの両方からデータの取得を行うケースでのModel設計ではあるが、Maybeという「ある/なし」しか表現できない文脈の少ない型をできるだけ減らしていこう、という方向性では同じく参考になるだろう。

vim-lspでElm用のVim環境を構築する

ElmCast/elm-vimをやめてvim-lspとasyncompleteを使ってelm-language-serverに対応したVim環境を整えたのでメモ。

github.com

プラグイン導入

vim-plugでvim-lspとasyncomplete系のプラグインを一気に導入する。すでに導入していれば不要。

Plug 'prabirshrestha/async.vim'
Plug 'prabirshrestha/asyncomplete.vim'
Plug 'prabirshrestha/asyncomplete-lsp.vim'
Plug 'prabirshrestha/vim-lsp'
Plug 'https://github.com/IzumiSy/vim-lsp-settings'

最後のvim-lsp-settingsは正しくはmattnさん作のプラグインで、vim-lsp用の設定を自動で適用してくれかつ各言語用のサーバーがなければインストールを実行してくれる超便利なプラグイン。これは絶対あったほうがいい。

しかし、この記事を書いた時点ではelm-language-server用の設定にバグがあり正しくサーバーが起動されなかったため、一旦自分で修正してフォークしたものを使っている。

PRは出しているので、VimでElmの環境を整えたい人は以下のPRがマージされるまでは自分のやつを使ってもらうか、自分でvim-lsp用のelm-language-serverの設定を手で書くのがいいだろう。

github.com

なお、Vimは8.2 patch 929からビルトインでElmのシンタクスハイライト設定を内蔵しているため、もはやElmをVimで書くのにシンタクス用プラグインも導入しなくてよい。

github.com

設定

自分は基本的に各プロジェクトのdevDependenciesにElm関連のバイナリをインストールするのでelm-language-serverの設定としてバイナリのパス設定を追加する。

let g:lsp_settings = {
\   'elm-language-server': {
\     'initialization_options': {
\       'elmPath': './node_modules/.bin/elm',
\       'elmFormatPath': './node_modules/.bin/elm-format',
\       'elmTestPath': './node_modules/.bin/elm-test'
\     }
\   }
\ }

ファイル保存時のフォーマッター実行も追加。

au BufWritePre *.elm :LspDocumentFormat

あとカーソルがある部分の関数やレコード定義を左右分割か別タブで表示する設定も入れている。

nnoremap <leader>df :vert LspDefinition<CR>
nnoremap <leader>tt :tab LspDefinition<CR>

以上、これだけでVimでも補完を効かせてElmが書ける。定義元へも飛べる。

vim-lspの設定はかなり面倒だと思い込んでいたが、実際にはvim-lsp-settingsを入れておけば勝手に言語サーバーのインストールから設定までやってくれるので、ほとんどIDEと体験は変わらなかった。

あとはElmファイルをVimで開くと「elm-language-serverをインストールするか」というような旨のメッセージが出るので、それに従ってインストールさせればよい。elm-language-serverがグローバルインストールされている場合にはそちらが優先されるのかも知れないが、自分は試していない。

2020年6月のクラウドバンク

izumisy.work

今年もふと思い出したので書いてみる。以下が先月時点での収益レポート。

f:id:IzumiSy:20200705002604p:plain
2020年6月の収益レポート

ちょうど新型コロナウイルスが盛り上がってきたのが多分3-4月頃で、その直後しばらく新規の投資案件が出てこなくて非常に心配になった。だが今は新しくスター・マイカとの共同ファンドを出したりするなど、アメリカンファンディングのようにサービス自体が完全停止するということはなさそうである。

投資額も増えてきたからか、税引後収益も1万超えに突入してきた。コロナウイルス真っ只中で6%超えの利回りが出る原理はよくわからないが、おそらく太陽光系案件がコロナウイルスとは関係なく安定して高利回りを生んでいるのではないかと推測している。CEO金田さんもクラウドバンクのセミナーにてFITの権利を入手するルートをクラウドバンクは持っており引き続き高利回りの太陽光案件を出していくと言っていたので、若干期待が持てる。

東京でまたコロナウイルス感染者が増え始めている現状の影響がどの程度出てくるのかわからないが、余剰資金がある限りは少しづつ突っ込んでいきたい。

開発チームの振り返りタイミングのパターン

考えることがあったので雑にメモ。

1. 毎週末(木曜日or金曜日)

  • 最も一般的なパターン。木曜日ないし金曜日にその1週間を振り返る。
    • 1スプリントを1weekと置いていれば毎週何らかの成果物があるはずなので、それに対する振り返りがこの時間にあたる。
  • 毎週30分-1時間近くの時間を毎週ブロックすることになる。
  • 振り返りで出てきたトライを翌日からすぐに始められない(土日に入ってしまう)ため、トライ実施のモチベーションに懸念が残る。
    • 週明けにトライの内容を再確認するなどの工夫で解決できる可能性がある。

2. 毎週明け(月曜日or火曜日)

  • 1の問題点であったトライ実施のモチベーションが改善され、翌日からモチベーション高くトライを意識できる。
  • 一方で、振り返る内容は先週のものになるためメンバの振り返りに対するモチベーションが低くなる懸念がある。
    • 振り返りに対するモチベーションの低下すごく致命的で、これが起きてしまうとそもそも振り返りの時間そのものが遊んでしまう。
    • 開発過程での課題を記録しておいてもらうなどの方法も考えられるが、基本的に人間は「忘れずになにかをやる」というのは不可能であると考えたほうがよいため、あまりまともなアイデアではない。

3. 開発アイテム終了後

  • 定期での振り返りを実施しないかわりに、開発アイテム単位で振り返りを実施する。これによって毎週1時間を必ずブロックされることがなくなる。
  • 少なくとも毎週定期で振り返りをするよりも凝縮されたものにはなるが、一方で開発期間が長いアイテムの場合には開発中に発生した課題を誰かが記録していないとすぐに情報が失われてしまうため、振り返りのタイミングで「なにも覚えてない」の状態になる懸念がある。
  • 「振り返りをするアイテム」と「振り返りをしないアイテム」の定義も必要になる。
    • 開発メンバ全員、開発メンバ個々人、リーダー/マネージャで見えている風景が異なるため、振り返りのモチベーションに対する合意をとるコストが発生する場合もある。この意思決定をナアナアにしたり無視したりすると今度は振り返りに対するモチベーションに影響がでる。

4. 振り返りを不定期実施にする

  • 朝会のタイミングで問題定期をしてもらうなど、不定期的になんらかの問題が起きたタイミングで開発プロセスに対する改善を加える。
  • 一方で、集団で話しながら振り返りをするということがなくなり視点が個々人に閉じるため、振り返りの対象が近視眼的なものになる懸念がある。
  • メンバーのモチベーションにもよるが「場がなければ行動を起こせない」という性質のチームであると基本的に自主的な改善は発生しない。

振り返りのROI

「振り返り」はチームの中で気づいた改善点や強みを共有しあうことで改善のサイクルを回し、より効率のよい開発を行えるようにするための重要なプロセスのひとつである。

一方で我々の時間は有限であり、週あたりの労働時間において「振り返り」が占める時間と、その「振り返り」から生まれる結果に対して、振り返りを実施する人間は気を配る必要がある。振り返りをしたからと言って必ずしもチームのアウトプットの質が改善されるわけではなし、振り返りをしなくても自律的に改善が行われるチームもある。振り返りをしなくても自動的にチームが良くなるような高ROIなチームなら、敢えて時間をとる必要もない。

そのようなチームはいわゆる自己組織化されたチームと呼ばれるものであるが、そのようなチームにおいてはルール化/フレームワーク化された仕組みはただ単に形骸化するどころか逆にチームのパフォーマンスの足かせにもなるため、注意が必要だと言える。

shelm: 脱法Elmパッケージマネージャー

elm/core などのカーネルモジュールにバグがあるとき、公式のバグ修正を待っていられないことがある。物によってはアプリケーションの動作に大きな影響があったりして早急な修正が必要になる場合もある。

そのような場合にはshelmを使うことで、自分たちで野良フィックスを取り込んだカスタムの elm/core が使えるようになる。

github.com

原理としてはelm-git-installと近い。

Elmアプリケーションのビルド時に使用されるelm-stuff配下のデータを先に作ってしまい、キャッシュされていることにしてpackage.elm-lang.orgからダウンロードできるものと置き換えている。

使い方

例えば、カーネルモジュールであるelm/timeを自分たちでカスタムしたもので置き換えたい場合には、次のようにlocationsフィールドをelm.jsonの中に新しく作り、オーバーライドする手段を指定する。

"dependencies": {
    "direct": {
        ...
        "elm/time": "1.0.0"
    },
    "locations": {
        "elm/time": {
            "method": "github",
            "name": "IzumiSy/elm-time"
        }
    }
}

methodのセクションにはgithubだけではなくfileも指定できる。

    ...
    "locations": {
        "elm/time": {
            "method": "file",
            "name": "../my-pkg/time"
        }
    }

あとはshelmでfetchを叩くことでelm-stuff配下にローカルパッケージのキャッシュデータを作らせることができる。

$ ./shelm fetch

あとはいつもどおりelm makeすればよい。

ほかにもいくつか機能があるが、それに関してはREADMEを読むか実際のコードを読むことで理解できると思う。shelm自体は1ファイルのシンプルなシェルスクリプトであるため、とくに複雑なものでもない。

余談: shelmはなぜ脱法か

このshelmの存在は、ElmlangのSlackワークスペースで「カーネルパッケージにあるバグを一時的に治したい場合にはどうすればよいか」という質問への回答として得られたものであるが、基本的にはElmのコミュニティにおいては脱法である。

shelmのREADMEにも"Note: Best not talk about this on the offical Elm channels unless you're trolling."(Elmの公式チャンネルでshelmについて話さないこと)と書かれていることからもそれが分かる。

Elmのカーネルモジュールにはポートを使わずにJSとのデータをやりとりできる基盤が存在しているため、容易に実行時例外を発生させるコードを書くことができてしまう。しかし公式はElmのことを「実行時エラーの存在しないAltJSである」と謳っていることから、カーネルモジュールに対する機能追加には非常に慎重である。また、Elmを使う人達の体験を損なわないためにも、shelmのようなElmの概念を揺るがす存在には厳しく目を光らせている。

いずれにせよ、実行時エラーのない言語としてElmを使うのであれば、自己責任のうえでshelmの利用は如何ともし難いカーネルモジュールのバグ修正程度に留めるべきだ。

Fitbit Charge 4でSpotifyアプリ連携がインストールできない場合の対処法

Fitbit Charge 4を買った

Spotify連携が利用できない

Charge 4には、Bluetoothで接続している端末と連携してSpotifyをコントロールできる機能がある。

しかし、買った直後その機能が有効にできないトラブルに遭遇した。

有効にしようとFitbitアプリからSpotifyアプリを選択しても、デバイスのバージョンが古いなどと言われSpotify連携がインストールできない。

f:id:IzumiSy:20200527221606p:plain:w300
インストールできない

解決策: アップデートを待つ

身も蓋もない解決策だが、大体24時間ほど待つと唐突に新しいアップデートが利用できるようになり、これをインストールするとSpotify連携が有効になる。

f:id:IzumiSy:20200527222338p:plain:w300
新しいアップデート

上記のファームウェアアップデートのインストールが完了すると、以下のようにめでたくSpotify連携が利用できるようになる

f:id:IzumiSy:20200527222451p:plain:w300
Spotify連携が利用できる旨が表示される

自分はCharge 4のセットアップ時に、すでにデバイスのアップデートを1度実行している。

しかし、どうやら初回時点のデバイスアップデートとは別のアップデートを受け取らないとSpotify連携を有効にすることができないらしい。

nvmやrbenvを遅延ロードさせる

nodeとかrubyとか特定のコマンドを使うときまでバージョンマネージャのローディングが遅延できたらなー思って調べてみたら、alias/unaliasを用いて遅延ロードっぽいことをやるテクニックを知った。

例えばnvmは以下のようにnodeやnpmなどのようなコマンドが呼び出されたときに初めてロードさせられる。

export NVM_DIR="$HOME/.nvm"
if [ -e "$NVM_DIR/nvm.sh" ]; then
  alias nvm='unalias nvm node npm && . "$NVM_DIR"/nvm.sh && nvm'
  alias node='unalias nvm node npm && . "$NVM_DIR"/nvm.sh && node'
  alias npm='unalias nvm node npm && . "$NVM_DIR"/nvm.sh && npm'
fi

やっていることはシンプルで、nvmが必要なコマンドにaliasを貼っておき、コマンドが必要になったときにはじめてnvmが初期化されるようになっている。

一度呼び出されたらunaliasするので、次回以降はnvmの初期化は行われない。

同じくrbenvもやれる

export PATH="$HOME/.rbenv/bin:$PATH"
if [ -e "$HOME/.rbenv" ]; then
  alias ruby='unalias ruby bundle gem && eval "$(rbenv init -)" && ruby'
  alias bundle='unalias ruby bundle gem && eval "$(rbenv init -)" && bundle'
  alias gem='unalias ruby bundle gem && eval "$(rbenv init -)" && gem'
fi

なお、元ネタは以下のツイートの記事。