✏
Elmのlintツールelm-reviewのProjectRuleSchemaを使ってボイラープレート生成する
Elmのlintツールelm-reviewのProjectRuleSchemaを使ってボイラープレート生成する
By ymtszw
@Elm-jp Online #3 (2023/07/22) [1]
jfmengels/elm-review
- 一言でいうと「Elmでルールを書けるElmのlintツール」
- Previous studies:
コード生成ツールとして
- 今日紹介するのもコード生成ツールとしての活用
- 特にelm-review悪用でコード自動生成を改良した記録です
elm-reviewの構造
-
Rule型のデータでlintルールを表現する- かいつまんで言うと「プロジェクトまたはモジュールを評価する関数」
-
Ruleを実行した結果、Errorが1つ以上あればlint警告となる -
ErrorはFixを提供しても良い
ProjectRuleSchemaとModuleRuleSchema
- ほぼ読んで字のごとく、
- プロジェクト全体を評価するルールか、
- モジュール(Elmの場合=ファイル)を単体で評価するルールか
ボイラープレート生成 (1)
- Elmの文脈でよく言われる「ボイラープレート」
- Client-side routingのあるアプリで、個別のページモジュールを
Mainモジュールに結線する -
Model,Msg,init,update,subscription, (view)からなる、いわゆる「コンポーネント」的モジュール同士を結線する
- Client-side routingのあるアプリで、個別のページモジュールを
- といった「モジュール間の結線」を指すことが多い(体感)
→つまり複数のモジュールの情報が必要となる。したがって、
ボイラープレート生成 (2)
-
ProjectRuleSchemaを使ってプロジェクト全体から情報を集め、 -
未結線のモジュールが見つかったら
Errorとし、 -
Fixを提供して結線する実装を生成しよう!
…となる。 [2]
実際のコード例
以下、Siiibo証券で実際に使っているコードをもとに紹介していきます。 [3]
(elm-review 2.13.1現在)
rule : Rule
rule =
Rule.newProjectRuleSchema "GenerateBoilerplate" initialContext
|> Rule.withModuleVisitor moduleVisitor
|> Rule.withModuleContextUsingContextCreator contextCreator
|> Rule.withFinalProjectEvaluation finalEvaluation
|> Rule.providesFixesForProjectRule
|> Rule.fromProjectRuleSchema
ProjectRuleSchemaの構造 (1)
Rule.newProjectRuleSchema "GenerateBoilerplate" initialContext
-
ProjectRuleSchemaを使うよ、という宣言とRuleの命名。 -
initialContextでは、プロジェクト全体から必要な情報を集めて格納するデータ置き場(ProjectContext)を初期化する
type State
= Visiting
| Visited { key : ModuleKey, implementedPages : PageFileDict, endOfDeclarationsRow : Int }
initialContext =
{ state = Visiting, currentPages = Dict.empty }
ProjectRuleSchemaの構造 (2)
|> Rule.withModuleVisitor moduleVisitor
- プロジェクト内の個別のモジュール(ファイル)を評価するのが
moduleVisitor
moduleVisitor =
Rule.withModuleDefinitionVisitor accumulateCurrentPageFile
>> Rule.withImportVisitor accumulatePageFileFromPreviousImport
>> Rule.withDeclarationListVisitor annotatePageFilesFromPreviousImplementation
- 内部的には
ModuleRuleSchemaが使われており、withXXXXXXVisitor系関数はそこに評価器を加えていく(Builderパターン)
ProjectRuleSchemaの構造 (3)
|> Rule.withModuleContextUsingContextCreator contextCreator
-
moduleVisitorが利用するModuleContextの初期化と、moduleVisitorが評価されたあとにProjectContextに結果を反映する処理を定義
contextCreator =
{ fromProjectToModule = Rule.initContextCreator fromProjectToModule |> Rule.withModuleKey |> Rule.withModuleName
, fromModuleToProject = Rule.initContextCreator fromModuleToProject
, foldProjectContexts = foldProjectContexts
}
ハマりどころ
- このあたりが
ProjectRuleSchemaの難しいところ。全体構造がデカい - ドキュメントに
ProjectRuleSchemaでどのようにプロジェクトが走査されていくか説明があるので、困ったらそこに立ち返ろう - 逆に言うと、走査の仕組みを初めに大まかに把握しておかないと、デカい型定義リストを目の前にしてハマることになる
moduleVisitorの中身
- elm-syntaxで各モジュールのソースコードがASTとして評価可能な形で関数に渡ってくる
- 関数記述自体はそれに対する愚直なパターンマッチで実装します
- 詳細は割愛(発表時には投影するかも)
fromModuleToProjectとfoldProjectContexts
- 2つの関数で、
moduleVisitorが各モジュールを評価した結果をProjectContextに蓄積する - (恐らく)並列処理を可能にするため、プロジェクト内のファイルは順不同で
moduleVisitorに評価されるという前提がある - 評価結果を順不同に
ProjectContextに合流させて支障なきよう、-
foldProjectContextsは順番に依存しないよう実装 -
ProjectContext自体、DictやSetなどの挿入順序に依存しないデータ構造を活用
-
ProjectRuleSchemaの構造 (4)
|> Rule.withFinalProjectEvaluation finalEvaluation
- すべてのプロジェクト内モジュールを評価し終わり、最終評価に必要な情報を
ProjectContextに蓄積し終わったあと、finalEvaluationでErrorの有無を判定する - ここでやりたいボイラープレート生成なら、
-
ProjectContextのcurrentPagesに現在のページモジュールの一覧が蓄積されているので、 - それを元に必要なコードを
elm-syntaxのAPIを使って書き出すことになる
-
Tips & Tricks
漸進的なFix
- 実はここまでの内容はelm-review悪用でコード自動生成をちょっと詳しくしただけ
- 当時は毎回のfix時に必ず生成先ファイル全体を書き換えていた

- 1年くらいこの実装だったが、自動生成をdevサーバで継続的に実行したくなった
- すると、ファイルを一旦空にして全体を書き換えるのは一瞬コンパイルが通らないコードになって不都合
- ということで必要な箇所だけ書き換える・書き換える必要がなければno-op、という漸進的なFixにしたい
直前の実装から情報を集める
-
ProjectContextにあったstateがここで意味を持つ -
state = Visited { ... }状態になった場合、ペイロードのimplementedPagesに評価直前のファイルで実装済みだった関数情報が入っている仕組み
finalEvaluationで"diffをとる"
- すると
currentPagesとimplementedPagesで差分があるか、"diffをとる"実装が可能になる - Diffが何らか存在したら(=ボイラープレートに更新が必要になったら)
Errorとしてfixを提供、そうでなければ何もしない - (時間あったら投影でコード紹介)
フォーマットされたコードの生成
-
finalEvaluationでfixを提供する際、elm-syntax-dslのElm.Prettyが活用できる - その名の通りprettyな(=elm-format済みの)コードが生成できる
elm-reviewによる差分検知の仕組み
- 実はフォーマット済みのコードをfixで生成するのには意味がある
- elm-review CLI自身も内部的にelm-formatを呼び出すようになっており、fixが提供したコードはelm-reviewが最終的に整形する
- 2.13.1現在、「Ruleのfixが提供したコード」と「それをelm-reviewが最終的に整形したコード」に差分があると、elm-review CLIは「fixあり」と報告し続けてしまう
- Fix時点で整形済みのコードを吐くようにすればきれいにfixが完了する、という理屈
elm-review 2.13.1でできないこと
前回実行との差分検知
-
直前の実装から情報を集めるでやったようなことは、多くのコンパイラやツールでは「前回実行時のコンテキスト情報を記録ファイルに書き出しておき、次回実行時に読み出す」という形で実現されることが多い
- いわゆるmanifest(目録)ファイル
- この機構が今のelm-reviewにはないので、実際のソースコードから情報を再構成した、という事情
- とはいえmanifestファイルも万能ではなくて、例えば別のメンバーがコード生成をしたときのmanifestが手元マシンになければ、結局実装からの再構成は必要
任意ファイルの読み出し
- manifestファイル機構を自前で実装できないのはこれができないからでもある
- 今はREADME.mdファイル、elm.jsonファイル、Elmファイルにしかvisitorを定義できない
- (無理やりやるなら、README.mdに何らかデータを仕込めば悪用できそうではあるが)
ファイルの新規作成
- そもそもelm-reviewはあくまでlinterが出自
- なので既存ファイルを評価して警告・fixすることはできても、全く新しいファイルを作り出すことはできない
- プロジェクトルールでコード生成する場面ではほしいこともあるのだが…
- それもあってか、純粋な「Elmで書けるElmのコード生成ツール」としては最近mdgriffith/elm-codegenも出てきてます
あとがき
- Siiibo証券のフロントエンド開発で日常的に使っているelm-reviewの
ProjectRuleSchemaによるコード生成を紹介した - 表現力が高いので複雑ではあるものの、一度分かればいろいろ応用できます✌
- 著者紹介(tokyo.ex#20のスライド)
Discussion