Type-First Developmentが良いという話
F#と言えばType-first developmentである(以下TFDと略す)。 TFDに関してちゃんと知りたければ、Why type-first development mattersが良く書けているし、F# for Fun and ProfitのCalculatorの記事で具体例も見れる。 TFDは別にF#以外の言語でも出来ると思うのだが、やっぱり代数的データ型(とパターンマッチ)がある言語という条件は要る気もする。
あんまりTFDの記事って見かけない気がするので、ここにもうひとつTFDの記事をwebに追加する事にも意義があるかもしれないと思い、書いてみる。 TFDの話は、型によるモデリングや代数的データ型の意義など、関数型言語によるプログラミングらしさが見えやすい視点でもあると思うので、そういう話にも触れてみる。
TFDをする為には、型での表現が複雑である必要がある
上記のリンクを読まない人向けに雑にTFDとは何かと説明すると、型を先に定義する、という開発手法だ。名前のまんま。
型を先に定義するってだけなら別にC言語やC++でもヘッダファイルから書けばそうなんじゃないの? Javaだってinterfaceとかpublicメソッドのシグニチャから書けば似たようなもんじゃん!と思われるかもしれない。その考えも全部が間違いとも言い切れないのだけれども、わざわざTFDという単語を作るからにはそれだけでは無い部分がある。
TFDという時には、もうちょっと型定義の段階でいろいろな要素が入っている事が想定されている。端的に言えば代数的データ型でドメインモデルが表現されている事、という事になるのだが、もうちょっと具体的な話を以下にしていく。
TFDと言うと、暗黙のうちに、型の定義の段階で「試行錯誤」がある、という前提があると思う。 型を先に定義するだけじゃなくて、その定義した型を眺めて「うーん、いや、このunionはおかしいな、こっちにくくりだすか」とかそういう試行錯誤。
例えばさっき書いていた自分のコードを例に見てみよう。twitterのバックアップデータからマークダウンを生成しよう、と考えていた。
最初は
type BaseTweet = {
Date: DateTime
FullText: string
}
type ImageTweet = {
Base: BaseTweet
Id: int64
FNames: string array
}
type Link = {
ShortUrl: string
ExpandedUrl: string
}
type LinkTweet = {
Base: BaseTweet
Links: Link array
}
type Tweet =
| Normal of BaseTweet
| Image of ImageTweet
| Link of LinkTweet
と書いて、いやぁ、全部にBaseがあるのっていまいちだよなぁ、とか思って以下に変えたりしていた。
type Image = {
Id: int64
FNames: string array
}
type Link = {
ShortUrl: string
ExpandedUrl: string
}
type TweetType =
| NormalTweet
| ImageTweet of Image
| LinkTweet of Link array
type Tweet = {
Date: DateTime
FullText: string
Type: TweetType
}
これで良いかはおいといて、こういう試行錯誤をする、という前提がTFDには含まれていると思う。もっと例が見たい人はDesigning with typesシリーズがオススメです。
さて、こうした試行錯誤をする為には、型でより多くを表現する、という前提があると思う。 例えば最初に以下のように定義してそのまま突き進むなら、型の定義で試行錯誤を行う余地は無い。
type Link = {
ShortUrl: string
ExpandedUrl: string
}
type Tweet = {
Date: DateTime
FullText: string
Id: int64
FNames: string array
Links : Link array
}
これはJavaやPythonではありがちなコードと思うし、これはこれでメリットもあるのだけれど、 これでは先に型を定義してもTFDにはならない。
抽象概念やビジネスロジックをなるべく型で表現する、という大前提があって、 型の段階で多くが表現されているからこそ試行錯誤をする意義がある。 その試行錯誤がビジネスロジック(の一部)などの試行錯誤になるからだ。 その試行錯誤がなければあまりTFDの意義は無い。
これが代数的データ型が無いとあまりTFDとは言えないという話につながるし、 また、ドメインモデルを型で表現する、というスタイルを理解していないとTFDがあまり意義が無い理由でもある。TFDはドメインモデルを型で表現する事を強制するためのプログラミング手法とも言える。
TFDのメリットその1: 早い試行錯誤
TFDは、型の定義をした段階では、実装がまだ無い。だから定義はすぐに終わるし、変更もすぐに出来る。実装が無いから。変更をする時にそれを邪魔する、水の抵抗みたいなのが無い。気軽にバンバン変更出来る。試してみないと分からない事は多いので、バンバン試行錯誤していろいろ試す。そうすることで、良いモデリングが見つけられる。試行錯誤のスピードが、良いデザインを生む。その為には実装が無い事が大切だ。
でも、型の定義はそれ自体正当なプログラミングであるので、 インテリセンスの支援が得られるし、コンパイルやtooltipによる間違いの検証が行われる。 変更した時に直すべき所がばーっと波線になる。よき。 リファクタリングブラウザでもいいんだけど、型の試行錯誤くらいだったら手でちょこちょこっと直す方が早い事も多いので、 変更した瞬間波線でばーっと見える方が楽でいいかな。
自然言語やホワイトボードよりも試行錯誤が楽。これはTFDが良い理由の一つに思う。 実現可能な制約が、思考を助けてくれる。
TFDのメリットその2: 早いフィードバック
TDDの良い所に、書き始めてから実際にフィードバックを得るまでの時間が短い、というのがあった。 フィードバックがすぐ帰ってくる方がやる気が出る。TDD本にも書いてある。 何故かは分からないが、これは自分も正しいと思っている。 ちょっと触ってすぐフィードバックが帰ってきて、それを元に続きをやる、という所まで行ってしまえば割と作業にのめり込めるので、なんかやる気が出ない、という問題に対する処方箋となっている。
TFDも型を定義して試してみる所までは実装を書かずにイケるのですぐ終わる。だから早いフィードバックが得られる。 プログラムの構造の大きな部分が実装を書かずに試せる、というのは良い所だし、 書き始めてからコンパイルが通るまでがすぐだと、なんかやる気が出やすい。
「あー、やる気でないな〜」という日でも、とりあえず型だけ定義してみるか、というくらいのやる気は出せる事が多い。 で、型を定義していくと、そのまま作業が進みやすい。 これは日々の雑用を片付けようとする時に結構重要と思う。やっぱ「やる気出ない」が一番の問題なので。 ちょっと型定義だけ、で始められるのは良い。
さて、TFDは語源からType-firstなので、あくまで最初がtypeだ、というだけで、drivenでは無い。最初にtypeを書いたその後は、type以外も書く。当たり前だけど。 ただTFDではその後というのがいつも似たパターンになって、そのパターンもまた早いフィードバックの積み重ねになっている。これもTFDのセールスポイントの一つと思う。
まず型を定義したら、次はfsx上で外部のデータをつつきつつ、定義した型が作れるかを試したりする。 その過程で型の方の不備を見つけて直したりする。 外部のデータから型への変換は、つついて試したコード片をまとめるだけだ。
つまり、
- 型を定義する(すぐフィードバックが得られる)
- M-Enterで外部データをつつく(すぐフィードバックが得られる)
- M-Enterした残骸をまとめて関数にする(インデントして変数名変えて必要ならtype annotationつけるだけ、これもすぐ終わる)
このそれぞれのステップが、作業を開始してからフィードバックが得られるまでの間隔が凄く短い。fsxでREPL万歳。 ただ普通にREPLで開発する場合に比べると、最初に型での試行錯誤がある所に違いがあって、これがトップダウンというか構造的な方を試行錯誤していて、 REPLのボトムアップの試行錯誤といい感じに相互作用してプログラムが進んでいく。 ボトムアップのボトムの所で延々と作業して全然上に進まないというREPLでの開発でありがちな欠点が無い。
また、この進め方は自然と、
- pureなドメインモデルを作って、
- 外部とのやり取りは最初にドメインモデルへの変換をして
- 以後はpureなドメインモデルの世界でコーディングを行う、
という、関数型プログラミング的にも良いとされているスタイルになりやすい。 (Calculator designのDefining the input and output to the functionあたり参照)
代数的データ型とパターンマッチの話
関数型言語の人はみんな代数的データ型とパターンマッチって言うけれど、なんでこの二つがセットになりがちなのか、 というのも、TFDのコンテキストで見ると部外者にもわかりやすいと思うので触れておく。
代数的データ型というのはようするにunion型の事で、F# などのunion型というのは複数の型のorの型が定義出来る、という事を意味する。 で、このunion型を定義すると、それがパターンマッチで使えるようになる。だからこの二つは良くセットで言及される。それがTFD的にはどういう意味があるのかを以下で少し述べてみよう。
さて、TFDにおいて、型による試行錯誤が有益である為には、なるべく多くが型で表現されている方が良い、という話をした。 つまり、プログラムを書く時に、ある部分が
- コードで表現するか
- 型で表現するか
が選べる場合は、型で表現する方が良い。
例えば上記のツイートの例の場合、イメージを含むツイートかテキストのみのツイートか、という事を区別する場合を考えてみる。 コードの中で画像のリンクがあるかどうかifで判定する事で区別する事も出来るし、 型としてそれを表現する事も出来る。 こういう、コードでも型でも表現出来る時には、型で表現する方が良い、という事になる。なるべく多くを型で表現する事で、プログラム全体に占めるTFDで試行錯誤出来る範囲の割合が広がる。嬉しい。
union型を定義していく、というのは、新しいカスタムなパターンマッチを定義する事だ。 型を定義する事が、抽象化したswitch文のようなものを作っている事になる。 これは型を定義する事が、ようするにswitch文とかif文の一部をコーディングしている事に相当する。 だから型を定義する事で、既存のコードの一部を、型の方に持って行っている事になる。 型を新しい述語として使っていける訳だ。
型の定義が新しいパターンマッチになってくれる言語では、 型の定義がコーディングでもある。これが、代数的データ型とパターンマッチが、より多くの既存コードを型定義に持っていける理由であり、それがそのままTFDの有効度を高めている。
当然型で表現すれば静的なチェックが働くとかの、ふつう型といってイメージされるメリットも享受出来る。