F#のクラス定義の構文について、ずっと「なぜこのような書き方をするのか?」という疑問を持っていました。考えても分からないので、とりあえずアプリオリな規則として使い続けて来ました。しかし先ほどふと、状態を持ったクロージャを返す関数に見立てているのではないかと気付いて、自分の中では一応納得できました。
あくまで推測(というか自己満足)に過ぎませんが、そのことについて書いてみます。
例
次のようなクラスがあるとします。
type Test(step) = let mutable num = 0 member this.GetNum() = num member this.Next() = num <- num + step
参考のためC#に翻訳すると以下のようになります。
class Test { private int step; private int num = 0; public Test(int step) { this.step = step; } public int GetNum() { return num; } public void Next() { num += step; } }
疑問 1. 引数
F#では型名にコンストラクタの引数がくっ付いています。なぜこんな所に書くのか疑問に思いました。
引数の扱いをよく見ると、フィールドとしてメソッドから呼ぶことができます。経験上、コンストラクタで初期値を渡してフィールドに保存するようなケースは多いので、それに特化して便宜を与えるための構文ではないかと推測しました。
疑問 2. this
letで定義した変数にメソッドからアクセスするには名前だけで済みます。F#にはletとは別にvalでもフィールドが定義できますが、こちらは呼ぶのにthis(に相当するもの)が必要です。なぜletだけ特別扱いされているのか疑問に思いました。
letがpublicに出来ないことから、関数のローカル変数に見立てているのではないかということまでは推測しました。
関数型とオブジェクト指向
ここまで来て、関数型とオブジェクト指向を比較した説明を思い出しました。どこで読んだのかは忘れましたが、CLOS関連だったような気がします。およそ次のような内容です。
【追記】いげ太さんのご指摘で思い出しました。ITmediaの川合さんの記事です。
試しにこの考え方で先ほどのクラスを書き直してみます。
let test(step) = let num = ref 0 let getNum() = !num let next() = num := !num + step getNum, next
これを.NETの型システムに合うように改変したのが、最初に挙げた書式ではないかと気付きました。正しいかどうかは分かりませんが、これで自分的には一応納得できました。
なお、クロージャの方でmutableではなく参照を使っているのは、レキシカルスコープの実装に由来します。F#ではクロージャからレキシカルスコープで参照される変数は値束縛されます。もしmutableな変数を参照しても、それはコピーされた値のため、変更が反映されません。そのためエラーではじかれて、参照を使うように誘導されます。
呼ぶ側(考察なし)
ここからは蛇足です。
定義の構文についてだけ気になって、呼ぶ側のことは気にしていませんでした。あまり考えていないのですが、参考までに対比させてみます。
// クラス let t = Test 3 printfn "%d" (t.GetNum()) t.Next() printfn "%d" (t.GetNum())
// クロージャ let t = test 3 printfn "%d" ((fst t)()) (snd t)() printfn "%d" ((fst t)())
どちらもtの中に状態が保持されて、関数を通して操作できています。後者は名無しで扱いにくいため、名前を付けてみます。
let getNum, next = test 3 printfn "%d" (getNum()) next() printfn "%d" (getNum())
これは呼ぶ側で名前を付けているのでクラスとは使い勝手が違いますが、クロージャでクラスと同じようなことができるのを確認するのが目的なので、これ以上は追求しません。悪しからず。