F# はOOPも出来るぜ、ってみんな言うし、WPFとかFormsとかASP.NETとかやる人にとってはそこらへんが全部ちゃんと書けるのがすごい重要なのも良く分かるのだが、 F# for Fun and Profitでも最初はOOPするな言っているし、 実際自分も一ヶ月くらいF# で結構なコードを書いたが、一回もclassは定義していない。 .NETのライブラリを呼ぶのでnewしたりメソッドを呼んだりはするのだけれど。 という事で言うほどOOPしなくね?と思うのだった。 特にコマンドラインでの雑用をメインとする自分みたいな人はほとんどクラスを定義する事は無いんじゃないか。プログラムの規模が小さいという意味では無くて、intrusiveなライブラリを使わない、という意味で。

で、代わりに使うのは何か?と言えばmoduleだと思う。

moduleとは何か?

moduleは何かと言うと、C++とかのnamespaceに相当する、ようするに単なる名前空間。 より原始的にはC言語のstaticとかでファイル内ローカルの関数やグローバル変数を定義するのと同レベルだ。しょぼい。 なお.NETのインターオペラビリティにはトップレベルはnamespaceを使う方が望ましいとかどっかのドキュメントには書いてあった気がするが、知るか、って感じでmodule使う。他から呼んだりしないし。

moduleとC言語的なstaticとの違いは、機能的な違いというよりは使われ方の違いに思う。 moduleの方が単位を小さく使うと思う。なぜかというと後述するがインテリセンスを絞るのに使うから。

で、moduleは単なる名前空間なので、複数のインスタンスを作ったりは出来ないし、moduleをfirst class objectとして引数に渡したりもたぶん出来ない(出来るかもしれないが自分はやった事無いし普通やらない)。

じゃあ複数インスタンスが必要な物はどうするのか、というと、レコード型およびそのunion型でデータ型を定義して複数データのインスタンスを作る。C言語の構造体と同じような話。 複数のデータのインスタンスは作れるが、これにはメソッドはくっつかない。しょぼい。

moduleで抽象データ型する場合は紳士協定(が普通)

じゃあ関数をデータとカプセル化したい時はどうするの?というと、「普段はあまりやらない」が最初の答えなのだろうけれど、 そうは言ってもたまには抽象データ型(ADTとここでは呼ぶがAlgebraic Data Typeとめっさややこしいんだよなぁ…)を定義したいという場合もある。

その場合には、moduleにADTの名前っぽいものを選び、実際のデータ型は「T」とつけるという紳士協定に従う事になっている。

この場合moduleの中に型はこのT一つだけ定義する、というルールで、 このモジュール内の関数はこのデータ型にアクセスする専用の物だけを主に入れる、 という事になっている(と思うがこの辺は曖昧)。

で、型は一つしか入ってないので、「module名 = 型名」であるかのように自己暗示してソースを読む。一対一の対応なのでそれで困らないし、ソース上もTという一文字だけなのでそれほど邪魔にならない。

上記リンクのPersonの例が分かりやすいが、自分のコードの例も挙げておこう。 uitでは独自の仮想的なディレクトリを扱う必要があるので、その抽象データ型であるUDirを定義した。 以下のような感じ。

module UDir =
    type T = V of string

    let fromOSPath str = V str

    let fromDI (repo:Repo) (from:DirectoryInfo) =
        ... 省略 ...

    let toOSPath (V str) = str

    let toDI repo udir =
        ... 省略 ...

Vがなんなのかはおいといて、UDir.Tというのが型名で、UDir.XXXがこの型のデータを扱う関数になる訳だ。 使う側は、

let di = DirectctoryInfo "/home/hoge/ika"
let udir = UDir.fromDI di

みたいな感じで型推論で受け取るので、Tはそんなに触る事無くUDirの関数をそのまま使ってデータを作る(たまにtype annotationで必要になるが)。 で、この型にアクセスする関数は UDir.toOSPath, UDIr.toDIなどのように、UDir.XXXにまとめる。

これは別に言語的に何かそれを強制する仕組みがある訳じゃなく、Tって名前だったらそういうものと思う、みたいな風習に過ぎないし、 与えられた生成関数を無視してTのtype constructorを使ってしまう事も出来るし、 中にパターンマッチして取り出す事も出来るし、 このモジュールの中でT以外の型を定義するのもマナー違反ではあっても普通に出来る。

えっ?紳士協定とかダサいんだけど…というのはまぁそうなんだけど、慣れればそんなには困らない。 より面倒なシンタックスでちゃんと隠したりする方法はあるのだけれど、あまり使う意義を感じないので自分は使ってないし、Fun and Profitの大きめな例でも使ってないのでそういうものだと思う。

全体的にF#は無理に隠してコードが長くなるのを嫌う。まぁどうせimmutableだしさ、みたいな。 一箇所のコードの変更が広範囲に〜といっても所詮コンパイルエラーというかhintで全部波線出るから簡単に治せるしねぇ。

なのでencapsulationは重視してもinformation hidingは重視しないのだ。(一部では怒られが発生しそうな主張ではあるが)

関数のグルーピングとしてのmodule

インスタンスに直接ドットをつけて一覧が出る事に比べると、運用でカバーな感じで一見ダサいようだが、メソッドではなくmodule名にドットでつけて関数の一覧が出る方が、F# の実用上は便利だ。

例えばList.mapと組み合わせてパイプ演算子にわたすようなケースを考えると、thisに相当する物がコード上は無いからインテリセンスを効かせるのが難しい。 でもUDir.などのようにmodule名にドットをつけてインテリセンスが出るなら、List.mapの次に置く時にもインテリセンスから簡単に探せる。 メソッドの場合はList.mapに渡すラムダ式の引数にドットをつけなきゃいけない訳だが、これはF# 的には書くのが少し面倒くさいので、あまり嬉しくない。(kotlinならもうちょっと楽なんだけど)

高階関数はコードを書いている時にthisがなかなか確定しないのだよなぁ。 コードは左から順番に書いていくので、書いている段階ではまだ確定していない。 でも、まだthisが確定してない所でもmodule名にドットをつければ関数の一覧が出るので、 そっちの方がパイプ演算子につなげてコードを書いている時には都合が良い。

ADT的にmoduleを使うのは例外的な使い方で、 普段は関数のグルーピングとして使う方が普通と思う。 関数を単にグルーピングするだけのmodule、原始的には思えるけれど、使い慣れると便利だ。

この辺の事情は冒頭に貼ったDownsides of methodsの、高階関数とうまく混ざらない、というあたりにも書いてある。なお、このリンクにあるtype inferenceがうまく働かない、という所もclassを使わずにmoduleを使いたくなる重要なポイントと思う。

また、F#は型推論をなるべく利かすため、コード上に型の名前が出てこない。 hintには出るのだけど。 だから、グルーピングの時に名前が出ている方がコードが読みやすい、 という逆転現象が起こる。

面倒だからとtype annnotationをつけないで推測させておきながら、 読みにくいからモジュール名つけるって本末転倒じゃない? というのはそんな気もするのだが、type annotationがシンタックス的にめんどくさいんだよねぇ。 なので現実問題としてモジュール名が出てくる方がコードも書きやすいし読みやすいので仕方ない。

この辺はmoduleは本質的にclassより優れている、という話じゃなくて、 F#の言語仕様がそうだ、というだけの話とは思う。

moduleはREPLと相性が良い

モジュールのいい所としては、REPLというかfsxと相性が良いというのがある。

fsxでへこへこevalしていって、ある程度動いたら適当に.fsファイルに切り出す、というのが通常のF#開発と思う。 この時、moduleならそのまま持っていけばそのまま動くので切り出すのが簡単。

classに抜き出すにはnewしたりといった変更が必要で、これが結構かったるいのはJupyterでPython書いている人ならみんな知っているんじゃないか。 セルでM-Enterしている時はいいんだが、それをclassにするのはひと手間あるじゃん。 moduleはあれが無い。

そういう訳でfsxでevalしていってコードを書き散らすだらしないプログラマでも、 簡単にモジュールに切り出せるのでコードがカオスにならない。

しかも切り出すのが簡単なので、小さな単位に切り出すのも抵抗が無い。 インテリセンスの都合的に余計なのが出てほしくないな、という程度の理由で小さなmoduleにまとめたり出来る。 これがC言語的なモジュールよりも小さな単位で気軽に分けやすい理由に思う。

手抜きの為に切り分けたといっても、ひとたび切り分ければ、外からはqualifier的なひと手間が生まれる。だから自然とモジュール内の関数は密結合でモジュール間は疎結合になっていく。 頑張って良い構成を作ろうとしなくても、インテリセンス的な手抜きの為に小さなモジュールをポコポコ作っていくと、勝手に良いモジュール構成に育っていく。ズボラも安心。

楽なプログラミングが悪い事とされるのは良くないと思うんだよね。.pyファイルでコード書くよりipynbでM-Enterの方が楽な訳じゃん。 楽な事をやっていって良いプログラムになる方がずっと良い。 M-Enterでコード書くのが悪いんじゃない、.pyに抜き出すのがかったるいのが良くないんだ。

そういう点でも、moduleでグルーピングしていくのはfsxのプログラムを自然と構成していく手段として望ましい。

まとめ: なんだかんだでみんなmoduleしか使わない

という事でいろいろな事情から、classは定義せずにmoduleで凌ぐのが普通に思う。 F# はマルチパラダイムでOOPも出来てうんたらかんたら、ってみんな言うが、 そんなの気にせずmoduleだけでコード書いてればいいんじゃない?と思うのだった。

ASP.NETでカスタムコントロール書いているんだけど?みたいな人は知らない。

もちろんBCLはクラスライブラリだしnugetでもC#用のライブラリは良く使うのだから、インスタンスをnewsしてメソッドを呼ぶのにgenericsやstructやinterfaceで受け取る例も含めてフルな機能があるのは重要ではあるのだけれど。