Runner in the High

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

GPD PocketからOneMix3 Proに乗り換えた

かつてIndiegogoでバックしたGPD Pocket初代をときたま外出のとき持ち歩いたりしていたが、とうとうOneMix3 Proに乗り換えた。

性能も使いやすさも段違いになった。

izumisy.work

今の世の中のUMPC全体のトレンドが7型から8型台へ移りつつあり、OneMix 3シリーズに限らずMAG1やGPD Pocket P2 Maxなどもまた8インチモデル。

これはすごく理に適っているなと個人的には感じていて、なにより7型は正直キーボードがめちゃくちゃきつい。 GPD Pocketを買った最初のころは、見た目が好みというだけでキーボード入力を頑張っていたが、手の小さい自分ですらやはり7型だとタイプミスがかなり頻繁に起こる。

7インチは見た目こそ小さくてかわいいが、実際に使うとなるとキーボード入力の難しさがめちゃくちゃネックになってくる。

f:id:IzumiSy:20200919211958j:plain
左がGPD Pocketで右がOneMix3 Pro

しかしOneMix3 Proのような8インチ台に突入してくると圧倒的にキーピッチに余裕がでる。 OneMix3 Pro特有の部分で言うならばやはりスペースキーだけが若干遠くアクセスしずらいという点はあるものの、それ以外でキーボードに対する不自由さはほとんどない。 このキーボード配列は確実にUMPCを日ごろから触る人が設計している。

Windows10からはWSLも使えるので、Ubuntuデュアルブートにしなくても開発環境が整うのもうれしいところ。 Web開発をするにはやはりMac/Linuxじゃないと、という思い込みがあったが、近年のWSLの進歩を見るとWindowsだけでほぼ開発を完結させられそうな未来が見える。

ちなみにモバイルディスプレイはJapanNextの10inchのやつがコンパクト&超軽量でおすすめ。

isucon10に出て予選敗退した

iguchi1124hayashikunとisucon10に出た。一番伸びたスコアは1300くらい。

f:id:IzumiSy:20200913175854p:plain

やったこと

覚えていることベースでやったことを書いてみる。

自分が覚えているのは基本アプリケーションサイドの変更ばっかり。

マシン構成の変更

hayashikunとiguchi1124がAP2台+DB1台の構成に変更した。

index張り

使われてそうなとこにいくつか張った

create index char_stock on isuumo.chair (stock);
create index estate_latitude_longitude on isuumo.estate (latitude, longitude);
create index estate_height_width on isuumo.estate (door_height, door_width);
create index estate_rent on isuumo.estate (rent);

nginxでクライアントキャッシュ

hayashikunが椅子と物件の詳細情報をクライアントサイドキャッシュするようにした。効果は不明。

よく考えたら椅子は在庫数みたいなデータがあるのでキャッシュさせたらまずそうだがスコアには影響はなかった。

なぞって検索の処理をAPサーバでやらせる

ポリゴンの内外判定系をSQLでN+1してやっているということがわかったのでpolyclip-goというライブラリを使ってST系関数の呼び出し部分をアプリケーションサイドに寄せた。

スコアは10くらい伸びた気がする。

LowPriced系のオンメモリキャッシュ

APサーバで結果をキャッシュするようにした。これはめちゃくちゃ効いて400くらい伸びた。

DBのアプリケーションの最大コネクション数を変更

10→50にしてみた。効果があったのかは不明。

botリクエストの排除

UAを見てbotだったら503返すようにするあれ。効果は不明。

ランキング処理

任意の椅子にあう部屋を取得するエンドポイントで使われているSQLがorを5つ使っているのでそのぶぶんのorをひとつに。

椅子の縦横奥行きのうち最長辺を除いた二辺でドアの縦横幅と比較すればorをひとつにできた。

   var estates []Estate
    w := chair.Width
    h := chair.Height
    d := chair.Depth

    // 短い短辺ふたつをとりだす
    lengths := []int64{w, h, d}
    sort.Slice(lengths, func(i, j int) bool { return lengths[i] < lengths[j] })
    len1 := lengths[0]
    len2 := lengths[1]

    query = `
        select *
        from estate
        where (door_width > ? and door_height > ?)
            or (door_width > ? and door_height > ?)
        ORDER BY popularity DESC, id ASC
        LIMIT ?
    `
    err = db.Select(&estates, query, len1, len2, len2, len1, Limit)

featureテーブル

これはやろうとしてできなかった。

LIKEを発行してchairとestateのfeaturesカラムを舐めているクエリがいくつかあったので、それに目星をつけてchairとestate用のfeatureテーブルを別で用意してみることに。これがうまくいけば検索クエリが改善するので購入ボリュームが底上げされると思っていた。

しかし、実際にはLIKEを発行するようなクエリは全体でみるとほんの数パーくらいしか占めておらず、改善する意味はほとんどがほとんどないということが終わってから分かる。また、意味がない割にfeatureテーブルを作ろうとするとめちゃくちゃ難しく、後半はここで時間をかなり使ってしまった。

思ったこと

用意されているDBテーブルがふたつだけ、そしてアプリケーション実装コードも短く、いままで以上にシンプルな構成のアプリケーション、という感じだった。

ベンチを動かしてみると一番負荷が集中するのは結局DBで、いかにキャッシュを効かせてDBへのアクセスを減らすか、いかにDB設計を考えて負荷を分割するか、という部分がポイントだったのかなという印象。しかし我々のチームはDBの分割などの戦略までは到達できなかったので、上位のチームがどのようなDB構成の戦略をとっていたのかが気になる。

また、今回のアプリケーションisuumoの特徴として

  • レコードの入稿がCSVを用いたバルクで行われる&頻度少なめ
  • 物件テーブルには更新がない(ユーザーの行動も資料請求で終わる)
  • 検索クエリが極めて複雑

など、実際のsuumoのようなプロダクトにものすごく近い状況が再現されており、おそらくベンチマーカーの挙動も 検索→商品詳細閲覧→資料請求or購入 という流れをたどるように作られていたように思える。つまり、パーチェスファネルのようにまずは検索部分のボリュームを稼がないとスコアに直接響く資料請求や購入につながらない、という感じ。

なので、早い段階で検索より先の部分(商品詳細とか購入とか)を改善しても、結局検索部分で詰まってトラフィックが増えないため結局スコアが伸びなかったのは極めて自然に思える。実際、トップページの商品表示で使われているLowPriced系APIをキャッシュした結果スコアがかなり伸びたりもしたので、アプリケーションの仕様からユーザーストーリー的な部分を把握して改善優先度を決定する、というのがISUCONで最も必要な考え方なんだろうなと感じた。

前回だか前々回だかのisucariでは「購入されやすい椅子」や「購入が多いユーザー」などのように、ベンチによるユーザーごとに特性を再現するギミックが仕込まれていたので今回も購買傾向を分析したりしたいと考えていたが、購入者の情報がUA程度でしか識別できないなどの理由がありやらできなかった。が、もしかしたらそこにもヒントがあったかもしれない。

isuconのアプリケーションチューニングにおいて優先度を決定するには、やはりまずアプリケーションの仕様や傾向を把握することから始めないといけない。gistの資料を読んだり、ログをとったり実装を読んだり。これは仕事でソフトウェア開発をするうえでも同じだし、こういう部分を無視してスコアが稼げないように問題設計がされているのはすごくおもしろい。

「チェアdeスクっと」は名前はダサいが気軽にスタンディングデスクを導入できる最強の製品

いわゆるコロナ禍で自分も在宅ワークを余儀なくされているが、やはりなんといっても座りっぱなしで問題になるのは腰。腰がイカれてくる。

Fitbitで歩数を見ても腰が壊れるのは自明で、自分はかなり通勤経路が長めの人間だったということもありこれまでは余裕で10k歩ほどあるけていたものが、ここ最近では歩いてもせいぜい5k程度にしかならない。歩かないとどうなるかというと、それは下半身のとくにハムストリングスが十分に伸びなくなるため腰痛に悩まされる。下半身を動かさない人間はだれでもみな腰痛に苦しむことになる。

これは座っていることがほとんどの原因なので、これを解決するために自分も据え置き型のスタンディングデスクの導入を検討していた。しかし、いかんせん値段が高い上に置くと机のほとんどのスペースをとられてしまう。昇降デスクも部屋狭いので置く場所がない。

というわけで見つけたのが「チェアdeスクっと」という製品。名前がめちゃくちゃダサい。

この製品はハイバックチェアの背もたれの部分に取り付けることで椅子をスタンディングデスクにできるというもの。

これなら机の上に据え置きにする必要もないし、昇降デスクを買う必要もない。

f:id:IzumiSy:20200905173502j:plain
天板の雰囲気

天板の大きさ的にはMacbook Proの15inchを置いてまあまあいっぱいという程度。

背もたれには以下のようにして固定されている。

f:id:IzumiSy:20200905173434j:plain
背もたれ部分に引っ掛けて固定されている

天板は角度を変えられるようになっている。

自分は若干傾斜があるのが好きなので、以下のような設置角にしている。

f:id:IzumiSy:20200905173446j:plain
横から見た雰囲気

固定の仕組みがかなりよくできているので、相当重いものを置かない限り落ちることはない。とはいえ、当たり前だがアームを付けたりディスプレイを置いたりするのは止めたほうがいい。

一つ気になるのは、椅子の背もたれが動くものだとタイピングをしている最中にすこしグラグラすること。しかし、このあたりは使う分には慣れる。

所感

この製品は極めて日本的だと思う。誰かの記事でも読んだが、そもそも日本は部屋が狭い。そういう部屋に住んでいる人たちのための製品だ。

現状、スタンディングデスクを導入するには机の上に据え置くタイプか別に昇降デスクを購入するしかほとんど選択肢がない。しかし、チェアdeスクっとであれば不要なときはどこかにしまっておけばいい。必要なときだけ場所を取らずにスタンディングデスクにできる。

いい製品だと思う。

なお、余談だが正座をやるようにしたらそこそこ腰痛が改善した。

スタンディングデスク+正座を組み合わせれば腰痛フリーになれるだろうか...

「朝30秒の正座」で腰痛が治る

「朝30秒の正座」で腰痛が治る

Elmアプリケーションにおける型と抽象化手法

Elmは機能を絞った言語であるためScalaHaskellのような高度な抽象化は言語機能上できないが、使う使わないに関わらず知識として「やれること」「やれないこと」を知っておくと、コード表現に幅が出る。

Extensible Record

ざっくり言うとレコードのプロパティを抽象化することができる。

例えば以下のNamableレコードは、nameというプロパティを持つレコードすべてを対象にする。

{-| 特定のレコードに依存しない抽象的なnameというプロパティを持つレコードを対象とする
-}
type alias Namable a
    = { a | name : String }


getName : Namable a -> String
getName { name } =
    name

つまり、以下のようなデータ構造があるとしても、データ構造としては受け付けるものの直接的な依存はしなくなる。

type alias User =
    { name : String
    , email : String
    , age : Int
    }


-- getName関数はUserレコードに依存しないがUserレコードを受け付ける
getName : Namable a -> String
getName { name } =
    name

上記のようなインターフェースになることで、getName関数はUserレコードの構造を持つ値を受け取るとしてもUserレコードのデータ全てに関心があるわけではなく、ただ単にnameという属性のみに関心があるということがインターフェースレベルで表明できる。また、関数の実装も特定のレコードに依存しないものになるため抽象化できる。

一方で「ageやemailの値によってgetName関数の挙動を変える」などの仕様が別とある場合には、それは間違いなく仕様レベルで"getName関数はUserレコードに依存しているべきだ"ということが分かる。なので、そのようなケースではあえてExtensible Recordではなく直接的にレコード名を指定することで関数のインターフェースに依存を表明させるべきだろう。

Narrowing Types

関数が受け取るインターフェースを特定の型に依存させるというのがNarrowing Typesの考え方にあたる。

たとえば以下のようにUserというカスタム型の各バリアントの情報をHTMLにする関数があるとする。

type User
    = UserLoggedIn LoggedInInfo
    | UserSuspended SuspendedInfo
    | UserBanned BannedInfo

viewLoggedIn : User -> Html msg

viewSuspended : User -> Html msg

viewBanned : User -> Html msg

受け取るデータがUser型に限定されている点ではまずまずだが、それでもまだ上記の関数はインタフェースの設計が適切とは言えない。なぜなら、User型はカスタムタイプであるため、コード上で実際に渡る値がバリアントのうちどれであるかはインタフェースからは判断できないからである。こうなると関数をさらに読んでいかなくては実際の挙動が把握できない。

なので、正しくは上記の関数は以下のように、User型の各バリアントが持つデータに依存させるべきだ。こうすることで、さらに関数の依存を限定でき、インタフェースから関数の挙動が推測しやすくなる。

viewLoggedIn : LoggedInInfo -> Html msg 

viewSuspended : SuspendedInfo -> Html msg

viewBanned : BannedInfo -> Html msg

ではこの考え方を抽象化でどのように使うかというと、異なるデータ構造において共通となる型を用意し、抽象化したい関数はその共通の型だけに依存させる。考え方としては若干Extensible Recordに近い。

例えば、以下はMember型とAdmin型に共通なName型を用意し、関数のインタフェースをName型に依存させる例。

-- Member.elm

type Member =
    Member Name


-- Admin.elm

type Admin =
    Admin Name


-- Name.elm

type Name
    = Name String

こうすることで、データとしてName型を持っているものともっていないものを区別できる。また、NameモジュールにはMemberとAdminが必要とする「名前に関するロジック」を凝集させることができるため、Nameという軸でMemberとAdminを抽象化できる。

ここで重要なのがName型を本当に必要な型だけに与えることだ。もしAdminとMemberで名前に関する仕様が異なるのであれば、それは同じName型に依存させてはだめで、AdminとMemberでそれぞれ異なるName型を用意するべきだ。同じ型に依存しているということは、仕様が同じであるということを意味する。

これはプリミティブ型の扱いにも同じことが言える。通常、Stringというのはアプリケーションの中でもっとも頻繁に表れる型だが、Stringへの依存は文字列というほぼ制限のない仕様に対する依存を表明している。しかし、我々が作るアプリケーションにはメールアドレス、ユーザー名、ニックネーム、などの"仕様を持つ文字列"が存在しており、関数やデータ構造はその仕様に特化した型に依存することでインターフェースとして仕様を表現することが望ましい。型と仕様が見つかれば、それはモジュールとして抽象化して抽出できる。

また「ある型が特定の型をもつかどうか」というのは幽霊型というテクニックにもつながる。幽霊型を使うころで状態遷移のパターンを型で制限できたりもする。 izumisy.work

Tagger

Elmはカスタム型もレコードもすべて関数として扱うことができるため、それを利用して任意の値を持つカスタム型のバリアントを受け付けることができる。ライブラリなどを作っていると使う場面がでてくるが、普通のアプリケーション開発ではあまり使う場面があるとは言えない。

type Msg
    = UpdateEmail Email
    | UpdateName Name
    | NoOp


{-| この関数はUpdateEmailのバリアントだけを引数で受け付けることができる
-}
emailUpdating : (Email -> a) -> Cmd msg

つまりタグを持つバリアントというのは、タグを受け付ける関数とイコールである。このような関数をElmにおいてはTagger(タグする関数)と呼ぶ。

あるのか分からないが、たとえば使いどころとして特定のMsgバリアントだけを受け取ってupdateを実行するような関数を実装したいときなどが挙げられる。その場合にはTaggerを使えば、上のemailUpdatingの例のようにUpdateEmailだけを受ける関数が作れる。

抽象化の観点では言えば、上のemailUpdating関数はUpdateEmailに限らずEmail型をタグとして持つものであればなんでも受け入れることができる。したがって、タグの型は限定するが、タグの型が同じでさえあれば値はなんでもよい。

移譲、DIP

DIPに関する記事はここに書いていた。 izumisy.work

また、英語になるがDEVにもモジュールの実装を移譲モジュールで抽象化する件の記事も書いていた。 izumisy.work

所感

書いてて思ったけどこれ全部Elmというよりは静的型付言語に共通する手法という感じがする。

でもElm書いてると細かい言語文法とかライブラリの使い方みたいな場当たり的なものじゃなくて、型を中心にした設計方針みたいなのを考えるほうに気が向くからいいですね。

メルカリでiMac 21inchを売るときのあれこれ(発送とか初期化とか)

先日2019年モデルのiMac 21inchをメルカリで売った。

下取りとか買取で調べると6-7万くらいで買い叩かれる&一度発送しての査定なのでかなり不安(送ったら1000円とかにされたりするかもしれないし)だが、メルカリであれば先に値段が決まるので嬉しい。

手数料は高いが仕方ない。

初期化について

オークション等で譲渡する場合にはiMac本体の初期化は必須。

手順に関しては公式が用意しているものがあるので、これを参照するのがよい。

support.apple.com

ひとつ注意点があり、以下のようなiMacのスペックのダイアログを画像として商品説明に載せる場合には、かならず初期化の前に写真として撮っておく必要あり。

f:id:IzumiSy:20200821102752j:plain
iMacのスペックのダイアログ

一度初期化してしまうと、再セットアップせずにはスペックを見ることができないので、またセットアップからの初期化が必要になってしまう。

シリアルナンバーに関してはそれ用いて保証期間をAppleの公式サイトでチェックしたりする用途で使うようだが、公開してしまうと偽造などに利用されてしまうとかなんとかで念のため隠している。

本当に隠す意味があるのかは不明。

保証について

コメント欄で「この商品は保証書はありますか?」とか聞かれることがあるが、それに関しては「ないが、シリアルナンバーから保証期間を確認できる。この製本の保証期間は〇〇までです」と伝えておけばよい。

www.apple.com

Appleの公式ストアで買うと確実についてこないが、家電量販店などで購入した場合にはその量販店用の保証書がついてくることはあると思う。

発送について

Apple製品というのは箱にも価値があるとか言われるので、人によっては発送するに当たってiMacの箱を包むダンボールをさらに用意したりする。あるいは、iMacを買うと純正の化粧箱を覆うダンボールがさらについてくるが、自分はそれは処分してしまっていた。

だが、自分の場合は追加でダンボール買うのもだるい(あと値段も上げて交渉合戦になりたくない)ので、説明欄に「この商品は純正の箱そのままで送ります」としっかり書いて出品することにした。

この記事を書いた時点でiMac 21inch 2019年モデルはヤマトの140サイズ扱いになる。自分はらくらくメルカリ便で送った。

www.mercari.com

Ubuntu 20.04でUSBのWiFiアダプタ(WDC-300SU2SBK)を認識させる方法

インターネットで調べたところ、どうやらUbuntuがサポートするrtl8192cuというチップとして認識させれば動くとのこと。

というわけで、rtl8192cuのモジュールをmodprobeでロードし、lsusbで分かったベンダIDとプロダクトIDをカーネルに認識させる。

$ sudo modprobe rtl8192cu
$ echo "056E 4009" | sudo tee /sys/bus/usb/drivers/rtl8192cu/new_id

こんな感じで、対象となるモジュール名の配下に置かれている new_id ファイルへデバイスの情報を書き込むとドライバの対象として未登録の機器であっても認識させることができる。つまり、実質的にどんな野良WiFiアダプタであっても中のチップセットの種類さえサポートされているものであれば、この方法で動かすことができる。

あとはudevを使ったり /etc/rc.local に上のコマンドを書いておくなりして、起動時にWiFiアダプタ用のモジュールをロードし自動的にネットワークへ繋がるようにすればよい。

askubuntu.com

追記(2021/02/06)

USB2.0のポートに刺すとダウンロードがめちゃくちゃに遅く、Googleスピードテストをしてみると1Mbpsかそれくらいしかでないことがある。

理由はわからないが、これはUSB3.0のポートに刺し変えることで解決できる。製品自体は2.0用と書かれているが、Ubuntuにおいては2.0に刺すと使い物にならない。逆にWindowsでは3.0に刺すと接続が安定しないことがあったが、Windowsに関してはElecom公式から提供されているWDC-300SU2S Driverをインストールすることで解決された。

特に理由がなければUSB2.0ではなくUSB3.0のポートに刺しておくのがいいと思われる。

BigQueryのDATE関数とタイムゾーン周りの挙動

よく分からんところがあるので整理した。

SELECT 
  date("2020-7-1T23:59:59+0900", "Asia/Tokyo") as tzDateWithTzString, 
  date("2020-7-1T23:59:59", "Asia/Tokyo") as dateWithTzString,
  date("2020-7-1T23:59:59+0900") as tzDate,
  date("2020-7-1T23:59:59") as date_

上記のSQLを実行すると以下の結果になる

tzDateWithTzString dateWithTzString tzDate date_
2020-07-01 2020-07-02 2020-07-01 2020-07-01

注目すべきはdateWithTzStringで、ISO8601形式における時差指定がない場合にはdate関数に指定されたタイムゾーン分の時差が時間に適用されている。

タイムスタンプの時差とタイムゾーン文字列の両方が指定されている場合の挙動

上の結果におけるtzDateはタイムスタンプ側でも+9時間の時差が指定されているが、同時に Asia/Tokyo というタイムゾーンも指定されている。

結果だけ見るとdateWithTzStringとは違いタイムゾーンの指定が無視され時差の適用が行われていないように見えるが、タイムスタンプにおける時差指定とタイムゾーンの時差が異なる場合を見てみると実際には適用が行われているらしいことが分かる。

以下はその例となるクエリ。

select 
  date("2020-7-1T23:59:59+0900", "Asia/Vladivostok") as date1,
  date("2020-7-1T23:59:59+0900", "Asia/Ulaanbaatar") as date2,
  date("2020-7-1T23:59:59+0900", "Asia/Tokyo") as date3

結果は以下になるが、タイムスタンプ側で時差指定がすべて+9時間であるにも関わらずタイムゾーンによって結果となる日付が変動していることが分かる。

date1 date2 date3
2020-07-02 2020-07-01 2020-07-01

この挙動を見るに、おそらく時差指定を含んだタイムスタンプ文字列とタイムゾーンの両方が指定されている場合の内部的な挙動はまずタイムスタンプ時間をUTCへ変換し、その上で改めてタイムゾーンの時差を適用するという動きになっているのではないかということが推測できる。

ウランバートルは時差が+8時間であるためタイムスタンプをUTC変換した上で時差適用しても 22:59:59 にしかならない。一方で Asia/Vladivostok は時差が+10時間なため、タイムスタンプがUTC変換された上で時差が適用されると翌日の 00:59:59 になる。

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