🥳

週一・一人・一年間・elmで開発した話

2023/10/27に公開

変に気負ってしまって書けなかった、elmで開発した経験について書こうと思う。エンジニアとして働いて間もない、一年にも満たないときの話だ。

この経験が今の私のプログラミング観を大きく飛躍させたと言っても過言ではない。もっともっと正確に書かなければ、と気負ってしまっていたのだけれど、いや忘れてしまったことは重要でないことだろうと思い直して書く。

当時、周りにelmについてを聞けるような有識者はいなかった。そのため、プログラミング ElmDomain Modeling Made Functionalと格闘した。特にDomain Modeling Made Functionalに関しては結果的に3〜4周立ち戻ることとなるぐらい参考にした。いかにコードにドメイン知識を落とし込むか。型によって制約を作り、同時にドキュメントとしても有用なコードにする技術・考え方について。

こういう型による制約、つまり不正な状態を撲滅する考え方は、今の私の糧となっている。


この経験は書き漏らさないようにと強く気負っていた。なかなか得られない経験だと思っていたからだ。

普段はReactを主に使うフロントエンドエンジニアだった。中規模のSPAの新規ページを実装する程度の技術レベルだ。そんな自分がelmを実業務で使えるということがとても恵まれていると思っていたから、気負った。

逆に適度に忘れてきた今こそ、自然に書ける頃合いだと思う。言語化できる部分を掬ってあげることによって、大胆に経験を一般化できるだろう。

プロジェクトについて

まずプロジェクトの背景について触れておきたいと思う。プロジェクトの発起人は社長本人です。社長は趣味でゴルフを嗜んでおり、経営者たちで構成されたゴルフ会に参加している。

そのゴルフ会では次の予定を設定し管理するのが煩雑だったようで、営業と趣味の実益を兼ねた案件として、予定管理アプリケーションを作ろうということとなった。

必要な機能は以下となる。基本的に、ゴルフ会と出欠について管理するのが目的だ。

  • 会員情報管理
  • イベント情報管理
  • その他静的情報などの管理
  • 実際にラウンドを回る組分けをランダムに行う機能

この経験が私にもたらしたもの

  1. 思考のコンテキストを広げ、応用しやすくする

前提として私はReactのフロントエンドエンジニア(経験は浅い)だった。そのため、elmという関数型パラダイムを学習できたことは、思考のコンテキストを広げるという意味で大変役に立った。

アプローチの相違点と共通点から、React/Elmそれぞれのアプローチの利点・欠点を身体から理解できた。

私が思うReactとElmの最も重要な違いは、データと状態に対するアプローチの違いであると言いたい。

Reactは状態をローカルに閉じ込め、Elmは状態を連続するデータとしてパラパラ漫画のように捉える。そのアプローチの違いが、コンポーネントとして閉じるかいい意味で行き当たりばったりにviewを書いていくかの違いとなる。

行き当たりばったり性

ここで、行き当たりばったり性の利点について簡単にまとめておきたい。

行き当たりばったりというのは、事前にオーバーに設計しないことを言う。別の言い方をすれば、YAGNIであろう。Elmの利点はその強力な型とシンプルな言語設計・エラーのわかりやすさによるリファクタリングの容易さだ。ElmとYAGNIの相性はとても良い。

なぜなら、小さく必要十分に変化することができる、小回りを持っているからだ。事前に決めておくメリットが少ない。

ElmではDRYよりYAGNIの方を優先する。本当に必要になってからDRYをする。完全に同じ実装であることが判明してから、DRYとして切り出すのだ。切り出すのは簡単だ。しかし似たものを同じ関数としてしまった結果、複雑怪奇に引数を取り動作が変化してしまうモンスターが生まれてしまいがちだ。そういうことをElmでは恐れている。

変化させやすいからこそ、複雑怪奇に変化させるのも簡単なわけで、オーバーエンジニアリングによる複雑化を恐れた。早すぎる抽象化、DRYを恐れた。最初からかっちりした設計をしてしまい、現実に則さない負債を抱えることを拒む。

それよりも、リファクタリングによる細かな修正により設計を育てるという方向性だ。

この方向性はインクリメンタルな実装・設計すなわちアジャイルな開発の方向性と類似しているのではないかと思う。

このようにReactとElmでは考え方が違う。その言語の力のベクトルの違いによって。似ているけれど違う言語を学んだことにより距離を得た。それが今の応用性に繋がっている。

  1. 忘れてしまう不安への対処方法

私は実装していてよく不安になる。今の自分がわかっていることはすぐに忘れてしまうのではないか?と常に心配している。自分が全て忘れても大丈夫なように実装したかった。

そんな不安に対処するための武器一式を、Elmは提供してくれた。

Elmはその強力な静的型と、明快なエラーメッセージによるリファクタリング能力によって、重要な知識を忘れてしまう不安に対処させてくれる。

プログラミングにおいて一番忘れやすい知識が、データの整合性についてだと思っている。プログラミングは根本的にデータの変換であり、変換に意識を集中していると対象となるデータへの注意がおろそかになる。

pythonやjavascriptを生で書いていたときに感じていた、

「このデータはnullなんじゃないか?」
「もしかしたらここで正しいユーザーは取得できていないかも……?」

そんな不安を、正しく定義されたElmの静的型は撲滅してくれる。猜疑心に基づく防御プログラミングをせずに済む。弱い型付の言語は、データの正しさまでユーザーに押し付ける。制約のないデザインは役立たずである。

できるというのは罪だ。

Elmであれば、型による信頼から、ドメイン内の知識の整合性を保つことができる。

こういう、不正なデータを撲滅する考え方を総称してMake illegal state unrepresentableという。

  • Opaque Types
  • Parse, Don't Validate
  • Anti Corruption Layer
  • 全関数

Opaque Types(Smart ConstructorもしくはFactoryパターン)により、データの生成条件を保証する。

Parse, Don't Validateによってデータが正しいかどうかを判定するのではなく、そもそも正しいデータ型を定義しそれだけが存在できるようにする。

Anti Corruption Layerによって、ドメイン知識に適した型しか存在しないように境界で不適切なデータを弾いてしまう。

このような考え方のツールを総動員して、変換の対象について考えることをElmに任せておくことができる。

変換自体の整合性はテストによって保証すればよい。

実装において考えることが少なくて済むというのは、大きなアドバンテージになる。人間の脳は貧弱で有限だからだ。

動的型付け言語は、意思による正しい実装を求める。Elmは、環境が正しい実装に導いてくれる。

あなたは何度正月に「今年は頑張ってダイエットするぞ」と一念発起し直しただろうか。環境を整えることによる正しい実装は賢い選択だろうと思う。

私は怠惰なので、意思によって正しい実装ができると思っていない。なるべくプログラムに正しさを任せていきたい。

反省、次へ活かせること

プロジェクト管理

プロジェクト管理についてはとても難儀した。データの整合性を高めるための手法を手懐け使いこなすには、今のエンジンをブーストすることをより意識したタスク管理が必要だった。

将来のためにデータ型を駆使した。そうすると、今どこまで実装が進んでいるかが分かりづらくなる。たとえば単純にコンパイルして実装をするのが遅れる。

どこまで実装できたか、どういうUIが見えるようになっているか。そういう成果が見えやすい実装の進め方ができなかった。私は将来不都合になったり知識を忘れてしまうのが怖かったため、潔癖症的にSmart Constructor、Opaque Typesを徹底した。

それと同時にテスト意識も希薄だった。それも現在のエンジン燃料不足に繋がっていった。当時は型を使ってテストをしているという意識をしていたのだった。

ただ、テスト駆動開発とは明確に異なるフローだったと思う。型を書くだけ書いて、型自体をテストすること送らせてしまったということだ。テスト駆動開発では、テストを書きテストを実行するのを繰り返す。テストを実行することで、テスト自体もテストする。そういう工程が遅れていたのだった。

向かい風もあった。特にこの案件では週に一回しか実装する機会がなかったのだ。週に一回先週のことを思い出すことから始める羽目になった。

「え〜〜、前回どこまで実装したっけ?」

そういう最初のちょっとした障壁が仕事へ入るための壁となってしまうわけで。そういう小さな障壁は一番モチベーションの低下に影響を与える。現在のエンジンであるモチベーションが少なくなり生産性が低くなっている中で、更にフィードバックを欠如してしまっていた。

どうすればよかったか

全体のうちどの関数までを実装できたのかが分かりづらく、ゴールまでどれぐらい進んだかがわからない。だから、この手法を取るときには反対に現在の自分の駆動力を維持しましていくのが大切です。どうやって取っておくか、守るか、むしろ湧かせていく。

見える化をする努力をする必要があった。

たとえば一番最初にわからなくなる瞬間のために、はじめのスタートアップ的な肩慣らしのタスクを作っておくこと。準備運動を作ることが大切なんだと思う。

一番最初に簡単なタスクを残しておくことで、複雑な実装タスクをやるスピードがつき、一日の満足度も高まっていく。簡単で退屈なタスク群には全く意味がないことだけれど。

もしくは、とりあえずの実装を行う範囲を広げること。潔癖症を和らげていいという範囲を決めておくこと。潔癖症が必要なのはドメインデータを変換するためのロジックの周りだけである。表示に関することに関しては、潔癖症はいらないんじゃないかなと思う。

むしろ、viewとかはすぐに見えたほうが退屈でないし、モチベーションも高まっていくわけで、それらのためにはviewの引数などはプリミティブな型で定義することだ。

結論として、両輪が大切だ。

両輪というのは、潔癖症的にバリデーションを行ったりすること。そしてviewは素早く実装すること。

データ的に不正な状態を撲滅するところが大切なところがあります。そういうドメインの知識にかかわるデータと変換。かっちりバリデートしACLなども定義する。レイヤーわけをすること。

viewの引数からはドメインデータを避けておく。表示に必要なデータだけに集中する。敢えて必要な状態をレコードとして受け取り表示するだけ。仮データでもいいからとにかく表示できるようにする。

この二つは、原因療法と対症療法と捉えることができる。どちらもやっていく。

将来の不安を軽減しつつ、現在のモチベーションを確保することができます。そういう小さな進捗と、過去の自分が軽減してくれたちょっとしたスムーズな線路が、私を助けてくれるはずです。

インクリメンタルな設計

インクリメンタルな進捗も大切だったけれど、インクリメンタルな設計も大切だった。YAGNIな進め方である。

設計をうまいこと、コードの発展とともに発展させることが難しかった。設計の本来の難しさだと思うが、言語化が大変だった。

率直な感想として、お客さんでもある社長から適切に設計を引き出すのが難しかったなと思っていた。

ただ、今思い出して思うことだけれど、最初から設計を煮詰めてしまっていたのが主な課題だったかもしれない。最初から良い設計を。今までより良い設計でなければ、というこだわりが強すぎた。将来への不安が大きすぎて、elmのリファクタリング能力を信頼しきれなかったのだ。

今は、小さく始めて変化に順応するやり方が良いかもしれないと思い始めている。変化のコストが低いから。

お客さんの言っていることだけではなく、もっと適切なものを話し合わないといけないと思っていた。より正確に、と。そこから、もう少し小さく初めて、後から変更できるような進め方を開拓して行きたい。

もしかしたら将来こう変化してしまうかも……???今のうちに抽象化しておいて、楽できますように……。じゃない。もっとYAGNIに

いくつ進められたか聞かれても答えられるように、計画性を高める。どこまで進めたかを雑に報告できるように。私は計画を実行できるかがとても不安だったわけなんだけれど、もっと雑に大雑把に未来を信頼してしまっても良かったかもしれない。

YAGNIにやるという意識を持つだけじゃなくて、システム化も大切だ。対症療法的に行う。特に一人で実装する場合はエンジンが少ないわけで、インクリメンタルに小さく進められる設計が大切だと思う。

そのために、ADRのようにちょっとずつ進める設計を採用すること。徐々に育てる設計へ!

他にもコードからドキュメントを生成する方向性も大切だった。コードの型情報から、その関数が何を行っているのかをわかりやすくすることだ。

そうすれば、先週やった設計の変更がわかりやすくなる。そして、どう実装すれば良いのかを理解しやすくなるだろう。

潔癖症が発動した実装をした結果、現在の見える化を怠ったことによって結果が分かりづらくなった。そして急かされることにも繋がり更に設計が後回しになってしまったわけだ。一人の場合設計は後回しにされがちなことを忘れてはいけない。

一貫性をもって、ちょっとずつ設計を進めていくこと。

オーバーエンジニアリング

今思い返すとやはりオーバーエンジニアリングであるな、と思ったりします。ここで再度取り上げておきたい。

将来が不安だった。自分が大切なことを忘れてしまうのではないかという不安が大きかった。

elmは型を存在条件やバリデーションなどからかっちりと定義し、ドメイン内のデータの知識を保全することを意識しています。プログラムをデータの変換と捉え、変換する中で不正なデータにならないように注意を払う。

どこまでをデータの不正かと考えることが大きな問題でした。

データの不正を許さないということは、常にデータの変換に不正な結果がつきまというということです。つまり、データの変換をすることが難しくなる。

データの不正がないことが正義だと強く信じていた。ただ、やはりそれはトレードオフなので、変換のしやすさと天秤にかけるのだ。

その他

思い返すと、JavaScriptとの接続が散らばっているのが面倒でした。これは多分elm特有のもの。Firebaseのデータベース設定がとても面倒だった。

たとえばPersistenceについて、読み出しと書き込みそれぞれにおいて別々にエンコーダー・デコーダーが必要だった(CQRS)。それも、わかりやすいところに。JavaScript側から見てどこがデータ変換機構をなのかをまとめておいたほうが良かった。

DTOに関しては関連する型のモジュール内に集めるより、むしろ一つのDTOというファイルにまとめていたほうが、後で閲覧しやすいかもしれない。これは、Portsに関しても同様である。

DTOに関しては、ドメインの型という性質と同じぐらいJavaScriptの性質も強い。だから、ちゃんとjavascriptからも見やすいところにあるべきなんだ。ドメイン型はElmコンパイラーの力添えがある分、JavaScript側から見やすいようにする方を重視したほうが良いと思う。

ドメイン性と外界性、どちらも持っている。たとえばMember.elmの中に入れるのではなくDTO.elmの中にmemberDTODecoderというものを作ってあげたほうがよい。

Portsに関しても同様、一つのPorts.elmというファイルで一元管理したほうが良いなということだ。

そうすることで、elmとjavascript間でヌケモレがないかを確認できる。型で補助できない部分なので見やすいところに一覧しておくこと。

またデータベースに関してだが、どこまでを正規化して非正規化するかが難しかった。NoSQLだったので非常に悩んだ。結局非正規化してページごとにできないかとやってみたけれど、非正規化するメリットが有るほど複雑なアプリケーションではなかったため、オーバーエンジニアリングだった気がする。

多少取得が面倒でも正規化してしまったほうがわかりやすい。

Discussion