Runner in the High

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

Elmにおける比較処理の高速化テクニック

ここで触れられてた話がおもしろいかったのでざっくり紹介

discourse.elm-lang.org

文字列比較はcase文のほうが早い(ことがある)

これは遅い

ensureNonBreakingSpace : Char -> Char
ensureNonBreakingSpace char =
    if char == ' ' then
        nonBreakingSpace
    else
        char

これは速い

ensureNonBreakingSpace : Char -> Char
ensureNonBreakingSpace char =
    case char of
        ' ' ->
            nonBreakingSpace
        _ ->
            char

理由は以下の通り

Currently, a comparison with a character literal isn’t optimized in the same way an integer literal is. By using an elm case expression, we make the compiler generate a javascript case statement. These again are much faster because there is no function call involved.

今のElmコンパイラ(おそらく0.19)では文字リテラルの比較が最適化されていないため、こういう結果になる。

余談ではあるがElmコンパイラは比較の際にどちらかがリテラルではない場合にパフォーマンス劣化を起こす。なぜなら、ElmにおいてJSの等価演算子はオペレータで比較される2値のどちらかがリテラルでなければ生成されないからだ。

リテラル同士の比較は内部的にはJSの等価演算子だけではなくElmランタイムの比較処理を呼び出すため、その分のオーバーヘッドが発生することになる。したがって、数値の比較であっても x == y よりは (x - y) == 0 のほうがパフォーマンスには優れているとのこと。

これ以外にもcase文でMaybeやResultをパターンマッチするほうがパイプで関数をチェーンするよりも速い、レコード更新のシンタクス({ a | field = value } みたいなやつ)を使うよりも新しいレコードを生成するほうが速い、などの話が elm-physics の作者から出ているが、ベンチマークがあるわけではないので真偽は不明。

Elmにおけるビルダパターンには後方互換性というメリットがある

Elmにおけるビルダパターンというのは以下のようなインターフェイスを指す。

-- setXxxのような関数をパイプでつないでいく雰囲気のやつ

Button.new
    |> setPadding 10
    |> setBackground "blue"
    |> setLabel "next"
    |> view

このインターフェイスの優れた点は新機能追加の際に後方互換性の維持が容易な点にある。つまり、今後 setXxx 系の関数が増えたとしても、その変更は既存でそのモジュールに依存している箇所に影響を与えないため、変更による影響範囲を最小にできる。

メリットを分かりやすくするために、ビルダパターンを採用せずに上記のインターフェイスをレコード型で表現してみるとしよう。

シンプルに考えると以下のようになる。

Button.view
    { padding = Nothing
    , background = Nothing
    , label = Just "next"
    }

可読性という観点では、上記のようにパラメタの名前を分かりやすくするためにビルダパターンを用いずRecord型を使うことはおかしくはない。しかし、ElmにおいてRecord型ではoptionalな性質のフィールドをMaybe型でしか表現することができない。これが意味するのは、新しい設定値のフィールドを追加するたびに既存で変更を加えたモジュールを利用している箇所すべてを修正しなければいけなくなってしまうということだ。

一方で、逆に考えてみるとレコード型や単純な引数でパラメタを与える実装はインターフェイスでそのパラメタが"必須である"ということを表明できる。

たとえば、上記のButtonモジュールにおいてlabelを必須のパラメタにしたいとする。その場合にはlabelの設定はビルダのインターフェイスになっていることは適切ではないため、次のようなインターフェイスにするほうが望ましい。

Button.view
    { label = "next" }
    |> setPadding 10
    |> setBackground "blue"
    |> view

このようなインターフェイスであれば、labelには必ずStringでなんらかのデータを与えないといけないということが明示できる。もちろん空文字列を与えてしまうことはできるが、その時点で違和感があるということに気付けるだけでもインターフェイス設計としては意味がある。

少し抽象化して考えるならば、あえてモジュールの変更を影響範囲として明確にさせたい(コンパイラにエラーを起こさせたい)ようなケースでは、ビルダパターンが適しているとは言えないということになる。

また、常ににビルダパターンを適用しようとすると対象のモジュールをほぼ必ずOpaque Typeの形に設計する必要があり、ここにも若干の手間がある。しかし、対象のモジュールが今後少なくない箇所から依存される未来が見えているのであれば後方互換性を維持する優先度が高いであろうし、ビルダパターンの適用はその時点で支払うべきコストだとも言える。

このように、状況に応じた観点でElmにおけるビルダパターンの使い分けを考慮してみると、モジュールのインターフェイス設計にはっきりとした意図を込められるだろう。

余談

なお、さらに応用的な実装例としてファントムビルダパターンというものがある。

medium.com

これは幽霊型を利用して「ある関数Aが呼ばれた場合のみ、関数Bも呼び出せるようにする」という制約を与え、ビルダのインターフェイスに依存関係を作りだすテクニックである。

内部的にやっていることは自分がelm-firestoreのインターフェイス設計で話したことと同じであるので、詳しくはそちらの記事を参照のこと。

izumisy.work

朝会と夕会を両方やっている

自分のチームでは朝会と夕会を両方実施しているので、その話を書きます。

アジャイルとかスクラムとかは分からないです、念のため)

自分のチームにおける朝会の課題

もともと自分のチームでは「朝会」のみをデイリーのmtgとして実施しており、そこで「今日やること」「やっていたこと」「困っていること」などをメンバに共有してもらっていた。

しかし正直のところ、朝会におけるこの報告時間はなんとなく形骸化した雰囲気が蔓延していて、とくに「やっていたこと」「困っていること」に関しては前日の話が抜けて、実質午前中の作業の話だけ(なぜなら前日の仕事はほとんど忘れてしまっているから)ということがほとんどだった。

これのまずいところは、実質朝会あとの5-6時間を占める大部分の作業報告が翌日の朝会で報告されず空白になってしまい、結果的にメインで業務を進めている午後の時間で発生した重大な報告事項などが抜け漏れてしまったり、場合によっては遅れてあとから「実は朝会で言い忘れてたんですが...」というようなコミュニケーションが発生したりするリスクがある。

とりわけ自分は「チーム開発はとりあえず朝会やっとけばOKよな」くらいの軽い気持ちで朝会を実施しており、そういう意味でもリスクトラッキングへの認識が激甘だった。

夕会の実施

インターネットで調べると「デイリーのmtgは朝会と夕会のどちらか一方をやる」という形態が(主観的には)多いように見られたが、朝会から夕会にしたとしてもそのmtgの目的性が変わらない限り、形を変えて朝会と同じ課題を抱えることになる。

というわけで、自分のチームでは朝会と夕方の両方をそれぞれ(15min-30min)で実施することとした。

それぞれのmtgの目的は次の通り。

MTG 目的
朝会 ・今日やる作業の報告
・必要に応じて作業関係者とのMTGを設定
夕会 ・今日やった作業の報告
ガントチャートへ進捗データの入力
・明日やる作業の確認

かつては朝会だけで実施していた内容を未来と過去の時間軸に基づき朝会と夕会に分離した。これによって進捗が最速で把握できるようになり、報告事項の忘却リスクを減らしている。

自分のチームでは夕会の時間を18:30に設定しているため、朝会の時点で確認した日当たりのタスクのデッドラインも常に18:30になる。タスクの内容や稼働開始時間にもよるが、夕会時点で作業が終わっていないことが判明すれば、その時点でそのタスクにビハインドのリスクがあるということを早い段階で明らかにできる。

進捗を日単位で把握するなら朝会で前日の進捗をチェックするだけでもいいのでは? とも思えるが、夕会にはそれ以外の良さもあった。例えば次のようなもの。

  • 1日作業して出てきたチームで共有したい事項をアツいうちにシェアできる
  • 進捗確認があるので進捗データの入力漏れが防止できる
  • ダラダラと作業をせず退勤するタイミングが作れる

自分は可能なかぎり夕会が終わり次第退勤するようにしている。

夕会の課題

しかし一方でまだ課題もある。

  • 夕方に他のmtgが多いメンバは参加できないケースが増える
  • 夕会で共有事項が充実してしまうことがある(個人的にはあんまり夕方過ぎに長いmtgしたくない)
  • タスクの最小単位が1日以上になればなるほどトラッキングが意味をなさなくなる

まず最初の「夕会に参加できないメンバがいる」というのはまあまあ致命的で、結局そこで参加できないメンバが出る限り進捗のトラッキングにはリスクを抱えていることになる。人によってはmtgの優先度を上げてもらうこともできるが、必ずしもそうではないこともある。

このあたりはまだ模索中である。

elm-firestoreにおける型安全なパス組み立ての設計

elm-firestoreのパス組み立て周りの実装を型安全な設計にしてみたのでその話。 github.com

Firestoreにおけるパスの仕様

まず大前提としてFirestoreにはリソースの場所を示すパスという概念があり、ざっくりパスは以下のようなルールに基づいている。

  • ルートにはコレクションのみが所属する
  • ドキュメントは必ずどこかのコレクションに所属する
  • コレクションはドキュメントの配下にも所属できる(サブコレクション)

つまり、必ずコレクションとその配下に複数のドキュメントがあり、ドキュメント配下ではまたコレクションを起点とした入れ子構造が形成される。例外的にルートには直接コレクションが所属するが、これはルート自体をひとつの更新/削除不可能な疑似ドキュメントであるとして考えればなんとなくしっくりくる。

elm-firestoreにおける表現

さて、上記の親子関係ルールに基づくパスの組み立て処理はelm-firestoreではパイプの構文を用いて以下のように表現される。

firestore
    |> Firestore.root                  -- ルート
    |> Firestore.collection "users"    -- コレクション
    |> Firestore.document "user1"      -- ドキュメント
    |> Firestore.subCollection "books" -- サブコレクション
    |> Firestore.document "book1"      -- ドキュメント

これが例えば、ドキュメントの配下にコレクションが来たり、サブコレクションの配下にルートが来たりするとコンパイルエラーがでる。

というのも、これらパスビルダ関数のシグニチャは次のようになっているからだ。Firestoreのパスが持つルールに基づいて、関数ごとにパイプでつなげる値の型が型変数で指定されている。

root : Firestore -> Path RootType

collection : String -> Path RootType -> Path CollectionType

document : String -> Path CollectionType -> Path DocumentType

subCollection : String -> Path DocumentType -> Path CollectionType

上記で使われているPath型の実装は以下。

type Path pathType
    = Path (List String) Firestore

この型が持つ pathType という型変数は幽霊型であり、この型でルート、コレクション、サブコレクション、ドキュメントの呼び出し関係をコンパイル時にチェックする仕組みになっている。

クエリ関数

ひとつ変わり種としてFirestoreには runQuery というオペレーションがある。これは少し特殊で、パス指定にはルートまたはドキュメントのみを指定する必要がある。それ以外のパスを指定すると、サーバーからエラーが返ってきてしまう。

上のパスビルダのシグニチャを考慮すると、関数のインターフェースとしては Path RootTypePath DocumentType の両方を受け入れるような設計にしたい。しかしElmにおいては型でそのようなORの構造は表現できない。

ではどうするかというと、レコード型を用いて呼び出し可能なオペレーションの許可リストのようなものを表現することで解決する。

type Specified
    = Specified


type alias RootType =
    { collectionPath : Specified
    , queriablePath : Specified
    }


type alias DocumentType =
    { documentPath : Specified
    , queriablePath : Specified
    }


--  いくつか本実装から省略...


type alias QueriablePath a =
    { a | queriablePath : Specified }

ここでポイントになるのは queriablePath というフィールドで、レコード型にすることによってRootTypeDocumentTypeが部分的に一致する条件を作り出している。

Path型を受け取る関数では型変数へExtensible Recordを当てはめることで部分一致をチェックする。たとえば、runQueryを実行する関数はQueriablePathというExtensible Recordを受け取る型として指定することでRootTypeDocumentTypeが受け取り可能になる。

runQuery : Decoder a -> Query.Query -> Path (QueriablePath b) -> Task Error (List (Query a))

このようにレコード型を幽霊型に用いると型レベルでのOR条件のようなものが表現できる。この手法を知っていればある程度複雑な型レベルでの状態遷移の仕様もコンパイラにチェックさせることが可能だし、特にFirestoreのパスビルダではまさにうってつけの方法だ。

デメリットがあるとすれば、やはり初見理解するのが難しいことと、コンパイルエラーがとてもじゃないがわかりやすいとは言えないところ。やっていることもまあまあ複雑なのでしょうがないと言えばしょうがないが。

なお、内部実装的には以下の記事で紹介している状態遷移パターンの実装とやっていることはほとんど同じである。 izumisy.work

elm-firestore v10.0.0をリリースした

引き続きチビチビと作り続けていたelm-firestoreだが、とうとうv10をリリースした。

elm-firestoreとはなんぞや、という記事はここで書いていた。

izumisy.work

変更点

Experimentalなライブラリということもあって毎バージョンいつもドラスティックな変更を加えているが、今回特筆すべきは以下の3つ。

  • トランザクション関連関数の整備
  • クエリオペレーションのサポート
  • Firestore Emulatorとの統合テストの追加

特にクエリオペレーションに関してはissueでも要望されており、たしかにFirestoreでクエリ系が使えないと検索処理も実装できずプロダクトでは使い物にならないところがあった。

また、トランザクション関連の関数に関しても Transaction 型は存在しているものの、それを使った更新処理を書くことが実際的には用意されている関数のインターフェイス上不可能というかなりちぐはぐな状態であった。しかし今回ようやく関連する関数群を整備&Firestore Emulatorとの結合テストも追加したことでトランザクション周りの実装まで動作を担保した状態になったと言える。

トランザクション関連関数

FirestoreのREST APIにおけるトランザクション周りのインターフェイスはUnit of Work*1的なものになっているのでelm-firestoreでもその雰囲気を踏襲した。パイプで更新/削除をトランザクションに積み上げて最後にコミットするようなスタイル。

    model.firestore
        |> Firestore.commit
            (transaction
                |> Firestore.updateTx "users/user1" newUser1
                |> Firestore.updateTx "users/user2" newUser2
                |> Firestore.deleteTx "users/user3"
                |> Firestore.deleteTx "users/user4"
            )
        |> Task.attempt Commited

また、更新系以外にも getTx, listTx, runQueryTx などのTxな取得系関数も増やした。

以下は統合テストからの抜粋になるが「usersコレクション配下から一覧取得したuserドキュメントの名前をアトミックに全更新する」みたいな取得系関数と組み合わせたコードは以下のようになる。

model
    |> Firestore.begin
    |> Task.andThen
        (\transaction ->
            model
                |> Firestore.path "users"
                |> Firestore.listTx transaction (Codec.asDecoder codec) ListOptions.default
                |> Task.map (\{ documents } -> ( transaction, documents ))
        )
    |> Task.andThen
        (\( transaction, documents ) ->
            Firestore.commit
                (List.foldr
                    (\{ name, fields } ->
                        Firestore.updateTx
                            ("users/" ++ Firestore.id name)
                            (Codec.asEncoder codec
                                { name = fields.name ++ "txUpdated"
                                , age = fields.age
                                }
                            )
                    )
                    transaction
                    documents
                )
                model
        )
    |> Task.attempt RanTestListTx

クエリオペレーション

とりあえず CompositeFilter, FieldFilter, UnaryFilter の3つをサポートした。

この中ではCompositeFilterのインタフェースのみ特殊で、いわゆるNon-empty list的なインタフェースにしている。これはFirestoreのCompositeFilterの前提条件が「少なくともひとつはフィルタを与えなければならない」ため。

-- usersコレクションからageが10以上かつ30より小さなメンバをクエリ
Query.new
    |> Query.from "users"
    |> Query.where_
        (Query.compositeFilter Query.And
            (Query.fieldFilter "age" Query.GreaterThanOrEqual (Query.int 10))
            [ Query.fieldFilter "age" Query.LessThan (Query.int 30) ]
        )

もしフィルタのパラメタを単なる配列にしてしまうと、フィルタとして空リストを与えた場合にエラーが返ってきてしまう。Elmなので実行時エラーにはならずResult型にはなるものの、型やインタフェース実装で守れるところはこのようにできるだけ守っておくほうが設計としては望ましい。

Firestore Emulatorとの統合テスト

Firebaseの製品のほとんどはエミュレータ用いてローカルで動かすことができるので、それを利用してelm-firestoreの統合テストを追加した。

やっていることはシンプルで、elm-firestoreを利用したWorkerという小さなテスト用のElmアプリケーションを別都用意し、テストランナー(ava.js)で実行とアサーションをやっている。Elmには Platform#worker というヘッドレスアプリケーション用のランタイム実行関数が用意されているため、それを使ってテスト用アプリを作る。あとはポート経由でテスト用アプリと通信しテストの実行と結果の受け取りをする。詳しい実装はリポジトリintegration_tests ディレクトリ配下にすべてある。

テストランナーにava.jsを使っているのは、Firestoreとの通信をモックせずIOを発生させるテストが多いため並列実行との相性がいいテストランナーを選択したかったという背景から。最初はmochaを使っていたがthisコンテキスト関連の黒魔術が多くあまり体験が良くなかったこともあり乗り換えた。

ところでElmとは話が逸れるが、エミュレータREST APIにはバグがまだたくさんあるようで、elm-firestoreのテストを書く過程でクリティカルなバグに2件遭遇している。本番では動くので問題ないが、2021年4月時点ではエミュレータとプロダクションの挙動が完全に一致しているとは思わないほうが無難だと思われる。

github.comトランザクション内でドキュメントの取得系メソッドを呼ぶとエミュレータが実行時エラーで止まる

github.com ↑ドキュメントの一覧取得でページサイズ指定してもページネーショントークンが返されない

Acer C720のSSDを換装してUbuntu 20.04 LTSをクリーンインストールする

Acer C720というChromebookが手元にあるのだが、しばらく前にサポートが切れたので気合を入れてUbuntuクリーンインストールする。

ついでにSSDも換装する。

acer ChromeBook C720 (日本正規品)

acer ChromeBook C720 (日本正規品)

  • 発売日: 2014/11/13
  • メディア: Personal Computers

SeaBIOSを有効化

USBメモリからUbuntuをインストールしたいのでSeaBIOSを有効にしたい。そのために、まずは開発者モードに入ってchronosユーザーで以下のコマンドを実行する。

# USBブートを有効にする
$ sudo crossystem dev_boot_usb=1

# CTRL+LでSeaBIOSが起動できるようにする
$ sudo crossystem dev_boot_legacy=1

# CTRL+Lを押さなくても勝手にUbuntuが起動するようにする
$ sudo /usr/share/vboot/bin/set_gbb_flags.sh 0x489

SSD換装

以下のSSDを購入して換装した。安い割に普通に動いているのでイイ。

換装の仕方は他にも記事がたくさんあるので省略。

Write-Protect Screwの除去

C720ではディスクへの書き込みを防止するビスみたいなやつを外さないと直接SSDへ書き込みができないようになっているので、ラップトップの裏を開けてボードからそのビスを外す。

以下の7番がそれ。

f:id:IzumiSy:20210314192214p:plain

Ubuntuの起動

天板を閉じるとシャットダウンしちゃうなどの問題はあるが、それ以外は特に問題なくいい感じで動いている。

f:id:IzumiSy:20210314193941j:plain

20.04だからなのかは分からないが、ログイン画面から起動時のセッションで "Ubuntu on Wayland" のほうを選ばないと画面がチラついて使い物にならなかった。

USBブートから起動したときは問題なかったのにディスクに書き込んでから起動するとこのトラブルが起きた。理由は不明。

追記: GPUのハング対策

"Ubuntu on Wayland" を選択したとしてもアプリケーションを起動すると画面がハングしてしまう。

dmesg でログを見てみるとどうやらGPUがハングしている様子。ログで調べてみた結果、どうやらi915のGPUドライバ周りにカーネルのバグがあるらしく5.7以降のカーネルにしてしまうとAcer C720ではLinuxがマトモに動かないとのこと。ワークアラウンドもあったりなかったりでよく分からない。

gitlab.freedesktop.org

雑な解決策として、一旦カーネルバージョンを適当に 5.4.0-26-generic にして使うことにした。このバージョンであれば問題なく動く。

追記: サスペンドの有効化

天板を閉じるとサスペンドせずシャットダウンしてしまう件についてはGRUBの設定に以下を追記することで対応できた。

GRUB_CMDLINE_LINUX_DEFAULT="tpm_tis.interrupts=0"

elm-typedパッケージを使えばもっと型で仕様を表現できるかも

elm-typedというパッケージを作った。詳しくは以下のGithubのREADMEに書いている。

github.com

パッケージ自体が根本的に実現したいことはPunie/elm-idとかjoneshf/elm-taggedあたりとほぼほぼ同じ。プリミティブ型をそのまま使わず型エイリアスでもなく、プリミティブ型をラップしたカスタム型を使うことで仕様をもっと型に表現したいというモチベーションから来ている。

実際に使う場合には、以下のようにelm-typedが提供している Typed 型に対して、タグとなる型(MemberIdTypeとかAgeTypeあたり)とその型が内部的に持つプリミティブ型、そして最後にパーミッション(後述)を指定する。

import Typed exposing (Typed, ReadOnly, ReadWrite)


type MemberIdType
    = MemberIdType

type alias MemberId
    = Typed MemberIdType String ReadOnly

type AgeType
    = AgeType

type alias Age
    = Typed AgeType Int ReadWrite

type alias Model =
    { memberId : MemberId
    , age : Age
    }

パーミッションはelm-idとelm-taggedを折衷するための機構で、ある Typed なデータに対して任意のパーミッションを指定することで、Elmアプリケーションにおけるそのデータの作成/更新を制限することができる*1

たとえば、上のサンプルでは MemberIdReadOnly に指定されているため、newやmapなど更新系関数の呼び出しはできない。例外的に ReadOnly であっても encode/decode はできるようになっているが、Elmアプリケーションに値が生まれた時点ではもう書き込み不可である。

newAge : Age
newAge =
    Typed.new 30 -- OK!

newMemberId : MemberId
newMemberId =
    Typed.new "1" -- ReadOnly指定なので新規にデータを作成できない(コンパイルエラーになる)

パーミッションの利点として、フロントエンド・アプリケーションにおいて参照のみで使うデータ(サーバーサイドから取得されるIDなど)をReadOnly指定することで、仕様上更新してはいけないデータを更新してしまうなどのバグを未然に防止できる。また、コードレビューやコードリーディングの際にもModelを読むだけでどのデータが更新される仕様なのかが把握しやすい。

実際に個人開発のElmアプリケーションでこのパッケージをテスト導入をしているが、それまでOpaque Typeなモジュールとして設計していた型が置き換えられていてイイ感じ。カスタム型による仕様の表現だけじゃなく、型の定義でデータのライフサイクルまで制御できるのはわりかし便利だ。

*1:パーミッションごとに呼び出せる関数を幽霊型で制限して定義している

ScalaではIntとStringを比較してもコンパイルエラーにならない

社内で以下のScalaコードがコンパイルエラーにならないというのが盛り上がっていた。

println(1 == "a") // コンパイル通るし結果はfalseになる

普段ElmとGoばっかり書いている&Scalaのことをとりあえず型に厳しい言語だと思いこんでいたこともあり若干意外だったが、調べてみると以下の記事が詳しかった。

stackoverflow.com

IntegerもStringも等価比較の際、内部的には Any 型を比較対象の引数に取る java.lang.Object#equals メソッドが呼ばれるため、比較対象がどんな型であってもコンパイラー的にはエラーにならない。ElmやGoと異なりScalaはこういうものらしい。

ちなみにScastieだと次のような警告がでる。

comparing values of types Int and String using `==` will always yield false

IDEやsbtで出るようなこういう警告を事前にちゃんと潰しておく、あるいは等価比較ではなくパターンマッチを使ってコンパイルエラーにすれば、こういう比較周りの挙動の罠には気づけるかもしれない。