Runner in the High

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

deferで参照している変数のポインタを更新しても参照は変わらないっぽい

こういうやつ

type Closer struct{
    Value string
}

func (c *Closer) Close(id string) {
    fmt.Printf("Closed(%s): %p\n", id, c)
}

func newCloser(c *Closer) {
    n := &Closer{Value: "aaa"}
    fmt.Printf("New: %p\n", n)
    c = n
}

func main() {
    c := &Closer{Value: "bbb"}
    fmt.Printf("Main: %p\n", c)
    defer c.Close("main") // <-- ここのcはnewCloserで作られたCloserのポインタになっているはず...?
    newCloser(c)
}

上記を実行するとこうなる

Main: 0xc000010240
New: 0xc000010250
Closed(main): 0xc000010240

newCloser関数の内部で参照渡しとしてリソースを上書きしたとしてもmainの中のdefer節で呼ばれるCloseメソッドの呼び出し対象の変数cの参照は古いままなので、newCloser関数の中で作られた方のリソースのクローズ漏れが起きる可能性がある。

これを防ぐためにはnewCloser関数のなかでもdeferでリソースのクローズをするしかない。

BlueoothドングルTP-Link UB400をUbuntuで使う

これを買った

バイス情報を見てみるとどうやら Cambridge Silicon Radio とかいうやつらしい。

at 15:26:52 ❯ lsusb
Bus 005 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 004 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 002 Device 003: ID 2357:011e TP-Link 802.11ac WLAN Adapter 
Bus 002 Device 005: ID 046d:c52b Logitech, Inc. Unifying Receiver
Bus 002 Device 004: ID 0a12:0001 Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode)
Bus 002 Device 002: ID 05e3:0610 Genesys Logic, Inc. 4-port hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

軽くググってみるとこのCSR製のやつはUbuntuと相性が悪いとか... beautifulajax.dip.jp

とりあえずBluetooth周りのデバイスは自分の使っている5.8.0-59-genericのカーネルにドングルがあれば動くには動くが、ちょっと気になるところもあり

あたりがちょいちょい不便で困っている。

バイスに原因があるのかLinux側に原因があるのかは分からないので、まだ試行錯誤中。

試したこと

なんとなくデバイスサスペンドが原因かもと思い、カーネルパラメータに usbcore.autosuspend=-1 btusb.enable_autosuspend=n をつけてみたりした。体感、急にBluetoothが切断されることは無くなったような気がする。

起動直後に繋がらないのを改善するために /etc/bluetooth/main.conf にあるFastConnectionとかいうやつもtrueにしてみたが、これもあんまりワークしてる気がしないな...

もし同じように困ってる人がいたら、このaskubuntuの回答がなんかヒントになるかも。

askubuntu.com

フロントエンドアプリケーションにおいて状態をどこに置くべきか論

後学のために自分の考えていることをまとめてみる。

考えられるパターン

これまでの経験から以下4つのパターンがある。

  1. ローカルStateでprop-drillingする
  2. ローカルStateかつイベント経由でデータ交換をする
  3. グローバルStoreとローカルStateを併用する
  4. グローバルStoreのみを使用する

1. ローカルStateでprop-drillingする

propとしてコンポーネント間のデータをやりとりする手法。

ほぼすべてのUIコンポーネントを親からデータを受け取りDOMを出力するだけの純粋な関数として表現できるため、全体の設計自体はシンプルになる。手間は多いが魔法は少ない。

コンポーネントの粒度が小さいアプリケーションの場合にはいわゆるバケツリレーと揶揄されるデータの受け渡しが頻発し、これに嫌悪感を持つエンジニアもいる。

2. ローカルStateかつイベント経由でデータ交換をする

Flux登場以前のフロントエンド・アプリケーションでよく見られた手法。

ほとんどFluxライブラリの再発明と同じであるが、コンポーネント間で双方向にデータが飛び交う点が大きな違い。イベントをインターフェイスとしてコンポーネントの関係性を疎結合にできる点がメリット。このデータの流れを単方向にしたものがFluxアーキテクチャである。

Vue.jsやAngular.jsにおいてはかつて親子間でのイベント送信を相互に行える機能が用意されていたため、これを使って横断的にコンポーネント間通信をする設計で開発が行われ徐々にカオスになっていく事例が見られた。いまはFluxパターンの登場によってアンチパターンになったと思われる。

3. グローバルStoreとローカルStateを併用する

仕様に応じてグローバルStoreとローカルStateを使い分ける。

一見良さそうなアイデアに見えるが、この手法の大きな問題点は「なにをStoreに置き、なにをStateに置くか」のルールづくりが大変なこと。チームが大きくなるとすぐにルールが守られなくなる。また、UIの改修によって状態の置き場がStateからStoreに移動したりなど、仕様変更時の影響範囲が状態管理部分を巻き込んで大きくなる可能性もある。

一方で、ローカルStateにすることで影響範囲を小さくしたり、別コンポーネントからの意図しないデータ参照を防ぐなどの設計も可能になるのがメリット。しかし、これはグローバルStoreにデータを置いたうえで仕様に応じた個別のデータ型を用意することでも達成できる。

4. グローバルStoreのみを使用する

グローバルStoreのみを状態管理に使う手法。

この手法では「グローバルStoreが膨らんで困る」とよく言われる。ココで言うところの「困る」とは大抵の場合「グローバルStoreのフィールドが増えて見づらくなる」と「グローバルStoreが大きくなると状態更新差分パフォーマンスが悪くなる」のふたつの懸念に集約される。これがクリアできるのであれば有用な手法になる。

利用するFluxライブラリによってはこれを解決できる機能や追加のプラグインなどが用意されていることがある。

どれを使うべきか。

まず、チームメンバが1人とか2人とかの場合にはどれを使おうが気にする必要はない場合が多い。好きなのを使えばよい。よっぽどめちゃくちゃな人がいなければコードベースは破綻しない。

しかし、経験上4-5人目くらいからが分岐点で、更に「2-3人のメンバで構成されるチームが複数ある」のようなケースになると途端に画一的なルール作りのほうが重要になってくる。設計方針に秩序を持たせたい場合には「いい感じでよしなにお願いします」がワークしなくなる。

ルールの作りやすさ順でいうと1>4>3>2かなという感じ。

Elmの場合には1しか選択肢がないのであんまり悩むことがない。

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 ↑ドキュメントの一覧取得でページサイズ指定してもページネーショントークンが返されない