Runner in the High

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

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

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

Intellij IDEAのVMオプションとVSCodeの設定をイジってフリーズを減らす

去年末にお金をケチって買ったiMac 2019(Core i3 3.6 GHz, 8GB)でIntellij IDEAとVSCodeを起動していると、文字の入力もできないくらいレインボークルクルが頻発して仕事どころじゃないことがここの所よくあった。

数年前に買ったThinkCentreで動かしているUbuntuのほうがまだ快適という謎もあり「さすがにコレはどうにかしたい」という強い意思からいろいろ調べてみた。

結果、どうやらアプリケーションが使うメモリサイズが大きすぎて、GCによるストップ・ザ・ワールドが頻繁に起こっているっぽいということが分かってきた。CPUをケチったせいでGCを捌ききれず、結局8GBもメモリを積んでいても無駄になるとは悲しい...

Intellij IDEAの設定

ということで、最大メモリサイズを1Gまで減らしみると、見違えるほどフリーズ時間が減った。

あとから気がついたが、実はUbuntuで動かしていたIntellijは元から最大メモリサイズが少なめの設定になっていた。こりゃどうりでサクサクなわけだよ。

-Xms128M
-Xmx986M
-XX:+UseConcMarkSweepGC
...

なお、G1GCみたいなメモリがたくさん使えること前提のGCアルゴリズムを選択するのもたぶん良くないだろうとということで、とりあえずConcurrent Mark Sweep GCを代わりに指定しておくことにした。

VSCodeの設定

また、VSCodeもそこそこ大きいGoアプリケーションを開くとレインボークルクルの頻発につながっていて非常に鬱陶しかったので、ファイルで使う最大メモリをデフォルト4GBとなってたところ1GBまで落とした。

加えてwatcherExcludeに.gitディレクトリ配下を追加してみた。

{
    "files.maxMemoryForLargeFilesMB": 1024,

    "files.watcherExclude": {
        "**/.git/objects/**": true,
        "**/node_modules/**": true
    }
}

これだけでめちゃくちゃいい感じになった。今までのストレスはなんだったのか。

メモリがたくさんあるからといって全部使えるとは限らない好例だったと思う。たくさんメモリを使うには、性能のいいCPUも必要ということが分かる。作業マシンへの投資をケチってはいけない。

gcloudコマンドでGAEアプリケーションデプロイ時に指定できるバージョン名には使える文字種に制限がある。

gcloud app deploy コマンドを使ってGAEアプリケーションをデプロイする際には--versionというオプションを使うことでバージョン名を自分で指定できるので、ブランチ名とかIDとかをバージョンとして出したくなる。

しかし、試しにブランチ名を含めてバージョンに指定してみたら以下のエラーが出た。

May only contain lowercase letters, digits, and hyphens. 
Must begin and end with a letter or digit. Must not exceed 63 characters.

つまり、バージョン名には小文字、数字、ハイフンしか含められないとのこと。

なので、もしブランチ名をバージョンに含めようとすると fix_123 みたいなブランチ名のアプリケーションだけデプロイできないことになる。 あと、ブランチ名が長いのもNGだし、大文字が入っている場合もNG。

なので、結局コミットIDだけ含めることにした。正直もうちょっと使える文字種の制限を緩めて欲しい...

Elmのモジュール分割における区分軸の観点

Elmアプリケーションにおいてモジュールを作る際に、モジュールへ機能をどう凝集させるかに関して。

機能による区分

かつてElmにおいて一般的だった分割方針。グレーの部分がひとつのモジュールを表している。

ModelとUpdate, Viewなどをすべて別のモジュールへ分割し、Userなどのネームスペース配下に配置する形で、Railsなどのフルスタックフレームワークディレクトリ分けと非常に近い。

f:id:IzumiSy:20200517012328p:plain
機能による区分

この分割方針の大きな欠点は、モジュールを不必要な単位で分けすぎていることで結局すべてのデータをモジュールから公開しなければならなくなっている点にある。

ViewモジュールやUpdateモジュールは、実装のためにModelモジュールの内部実装を知る必要がある。その結果、公開された内部実装へ知る必要のない他のモジュールが依存する可能性がでてしまう。

Elmにおいてモジュールを作る理由というのは公開したいものと公開したくないものを分けることであり、他モジュールからの依存をコントロールし変更に強い「インターフェースに対する依存」を生み出すこと。Opaque Typeのような手法も、まさにこれが理由になる。

モジュールの分け方が不適切な単位で小さすぎると、実装のためにモジュール内部をすべてexposingしないといけなくなってしまいモジュールの機能が存在意義を失ってしまう。このような「機能による区分」が持つ問題への反省を踏まえて、現在のElmにおいては以下の「ドメインによる区分」の方針でモジュール分割が行われる。

ドメインによる区分

「機能による区分」で行われていた分割とは異なり、モジュールはUser, Article, Commentのようなアプリケーションにおけるドメインの単位のみで作られる。

最も卑近な例では、リチャード・フェルドマンによるrtfeldman/elm-spa-exampleがモジュール分割にこの区分を採用している。

f:id:IzumiSy:20200517012408p:plain
ドメインによる区分

このモジュール分割が行われるのは、例えば「画面」がそうである。Elmアプリケーションにおいて、User画面、Article画面、Comment画面はそれぞれがひとつづつモジュール化される。

画面をモジュール化する理由は、画面を利用するMainなどのモジュールは各画面の実装詳細を知る必要がないからだ。これらのモジュールを利用するMainモジュールは、各画面モジュールが提供する関数を用いてモデルの更新をしたりHTMLを作ればよいだけであり、画面モジュール内部の実装詳細を知る必要はない。以上の理由から、画面単位のモジュールが作られる。

このような単位でモジュールを作ることで、「機能による区分」では過剰に公開されていたModelやUpdateの詳細が外部から適切にカプセル化され、意図しない依存が生まれにくい状態になっている。

余談: 共通化によって生まれる孤児モジュール

プログラミングをしている人間であればおなじみのDRY(共通化)は一般的には「機能による区分」で行われる事が多く、上記の「ドメインによる区分」の軸とはモジュールの設計の観点で直行することになる。

例えば「すべての画面モジュールにおいて"View"や"Update"の軸で共通化した実装を用意したい」などのケースがこれに当たる。

このような区分軸の直行が起こるケースでは、各ドメインを横断して仕様の分析を行い、その共通化が単なる「コード上の共通化なのかそれとも「概念としての抽象化」なのかを見極める必要が出てくる。

しかしながら、実際の事実としてもっとも起きがちなのは、このようなタイミングでcommonやutil, helperなどの「どこにも属さない概念」爆誕することである。彼らはコードとしては共通化されたが、概念上の所属の観点からは考えることを放棄された孤児モジュールである。

彼ら孤児モジュールは、ある意味「所属待ち」を行っている状態でもあるため、継続的にリファクタリングなどのタイミングでどこかのドメインに所属させたり、新しい抽象概念に昇格させたりしていくことが必要である。孤児モジュールがコードベースにおけるマジョリティーになってしまうと、アプリケーションにおけるドメインの概念が揺らいでしまう。

rbenvの初期化が遅いのをどうにかする

rbenvの初期化がすげー遅いときがある

eval "$(rbenv init -)"

探してみると、ちょっと昔の記事だけどこんなのが出てきた。

mattgreensmith.net

つまりrehashとかいうのが遅いらしいので、それをbackgroundで処理させるといい感じになるらしい。こんな感じで。

eval "$(rbenv init --no-rehash -)"
(rbenv rehash &) 2> /dev/null

たしかに体感早くなったような気がする。

DEVでフォームにおけるOpaque Typeの設計に関するシリーズを書いた

DEVにシリーズ機能というものがあることを知って試してみた。全2作。

dev.to

dev.to

elmbitsでも取り上げられた

内容について

全部は書けないのでざっくり。

一般的にOpaque Typeは以下のようなnew関数を公開して、バリデーションの結果Result型ないしMaybe型でカプセル化された型を返すような設計になっていることが多い。

type Name =
    Name String


new : String -> Maybe Name
new value =
    if String.length value > 100 then
        Nothing
    else
        Just (Name value)

ElmのパッケージではUrlモジュールなどがいい例で、fromString関数で受け取る文字列が正しいURLであればUrl型のデータがもらえる、みたいな設計になっている。

これはつまり、バリデーションを通らないものはアプリケーションのライフサイクル上存在できない(DDDで言うところの不変条件に近い)ものにするための設計手法だと言える。つまりJustであるものは常に有効なデータだと型で表現されている。

フロントエンドにおけるバリデーションと有効性

この考え方をフロントエンドに適用しようとすると少し難しい。なぜなら、フロントエンドは状態として「不完全」なものも受け入れなければならないことが多いからである。

ユーザーがフォームに文字列を入力している過程などは、必ず不正な値の状態から始まる。もしURLの入力を求めるフォームであれば、ユーザーがURLを入力し終えるまでは常にフォームに入っているデータの状態は不正であると言える。

これを素直にサーバーサイドと同じものとして捉えてしまうと、ユーザーの入力中のデータは不変条件を満たしていないので常に無効にする、のようなちぐはぐなロジックを生むことになる。フロントエンドでは入力中のデータも状態の一部として捉えるため、無効なデータであるとして破棄してしまうと永遠にユーザーが入力を完了することのできないフォームを作ることになってしまう。

つまり、フロントエンドではバリデーションにおける「有効」「無効」の状態以外に「入力中」のような状態が必要になる。では、これを現実的にどう設計し実装するか。これが、自分がDEVに投稿した"Designing Opaque Type for form fields in Elm"シリーズのPart 1でカバーしている内容である。

モジュール・コンポジション

Part 2ではPart 1で実装されたフォーム・モジュールの再利用性を高めるために、モジュール・コンポジションを用いた設計の導入を解説している。

f:id:IzumiSy:20200506145449p:plain
モジュール・コンポジション

Elmにはクラスの概念がないため、もちろん継承の概念もない。したがって、異なるモジュールの振る舞いを再利用するためには、特化したモジュールが抽象化したモジュールに対して処理の移譲を行うようにする。このような明示的な振る舞いの獲得を、一般的にはモジュール・コンポジションと呼ぶことが多い。

Elmにおけるモジュール分割は膾炙したノウハウがないためとっかかりが難しいが、この"Designing Opaque Type for form fields in Elm"のシリーズでは

  1. まず型に関する振る舞いを中心にモジュールを作成
  2. 同様の機能をもつモジュールから抽象的な振る舞いをさらにモジュールへ抽出

というふたつの流れに沿ってモジュール分割を行う手順を示している。

型の定義を中心としてモジュールをつくるという設計そのものはElmにおいて一般的ではあるが、さらにそこからどうモジュールを分けていくべきか、という点をモジュール・コンポジションの考え方と共に解説している。