実はF#のオブジェクト指向ってしっくりこないんです!
頭出し
ネタが古い上に、C++に毒されているせいか、F#のOOP、つーかクラスの書き方がどうもしっくりこないので、まとめる。
七誌さんの
がとても良かった。
言い訳すると「えーマジクラス!?キモーイ。クラスが許されるのはC#までだよね。キャハハハハハ」なんて思っていた時期があって、各種F#本のクラスの所はほぼ丸飛ばししていた。マルチパラダイム?知らない子ですね。あの頃の俺は尖っていたんだ。
コンストラクタ or 初期化周り
空のクラス
空のクラスを作るには以下のように書いておく。
class ... endは省略したい子なんだけど、空のクラスの場合はないとだめっぽい。ぐぬぬ。
> type Hoge = class end;; type Hoge
このクラスはコンストラクタがないので、newすることはできない。
> let x = new Hoge();; let x = new Hoge();; --------^^^^^^^^^^ stdin(10,9): error FS1133: No constructors are available for the type 'Hoge'
また、newを外して書いてもだめなんだが、ちょっとエラーメッセージが違う。
newのあるなしは本質的な違いがないもんかと思っていたが、何か違うってことか。調べないといけない。
> let x = Hoge();; let x = Hoge();; --------^^^^ stdin(2,9): error FS0039: The value or constructor 'Hoge' is not defined
こんなnewすら出来ないこんなクラスを何に使うのかはよくわかんねぇけど、C++ならtag dispatch的に活用されるので、そういう用途もあるのかもしれない、ないかもしれない。ただその用途なら、F#では判別共有体使えばいいのでないと思う。
コンストラクタのあるクラス
クラスの定義時に括弧()をつけると、プライマリコンストラクターつう、コンストラクタが自動で定義される。
> type Hoge() = class end;; type Hoge = class new : unit -> Hoge end
なんで、これで晴れてnewできるようになるわけですわ。
> let x = new Hoge();; val x : Hoge > let x = Hoge();; val x : Hoge
以下のように書いても、出力されるクラスの定義はまったく同じに見えるので、これでもいいのか。プライマリーコンストラクタに対して、これはその通りの”追加のコンストラクター”と呼ばれる。
> type Hoge = new() = {};; type Hoge = class new : unit -> Hoge end
プライマリコンストラクター内では、let, do 束縛でコードの実行が可能。
以下はdo束縛の例。
> type Hoge()= do printfn("Hello, Hoge");; type Hoge = class new : unit -> Hoge end > let x = new Hoge();; Hello, Hoge val x : Hoge
一方、”追加のコンストラクター”でコードを実行する必要がある場合、do束縛じゃなくて、thenを使用する。お、おおう。既に混乱してくる…
> type Hoge = new() = {} then printfn("Hello, Hoge");; type Hoge = class new : unit -> Hoge end > let x = new Hoge();; Hello, Hoge val x : Hoge >
「プライマリーコンストラクタが存在して、かつ追加のコンストラクターがある」場合、その中でプライマリーコンストラクタを呼ばなければならない。仕様です。
> type Hoge()= do printfn("Hello, Hoge") new(a:int) = {} then printfn("Hello, Hoge2");; new(a:int) = {} then -----------------^^ stdin(34,18): error FS0762: Constructors for the type 'Hoge' must directly or indirectly call its implicit object constructor. Use a call to the implicit object constructor instead of a record expression. > type Hoge()= do printfn("Hello, Hoge") new(a:int) = Hoge() then printfn("Hello, Hoge2");; type Hoge = class new : unit -> Hoge new : a:int -> Hoge end > let x = Hoge(3);; Hello, Hoge Hello, Hoge2 val x : Hoge
逆にプライマリーコンストラクタがない場合は呼ばなくていい。
> type Hoge= new() = {} then printfn("Hello, Hoge") new(a:int) = {} then printfn("Hello, Hoge2");; type Hoge = class new : unit -> Hoge new : a:int -> Hoge end > let x = new Hoge(21);; Hello, Hoge2 val x : Hoge
詳しくは、
privateなフィールドと関数
によると、privateなフィールドはlet束縛で書ける。んで、上述のようにlet束縛はプライマリーコンストラクタ内での使用がMUST。
> type Hoge() = let mutable x = 0 member this.showx() = printfn "%d" x;; type Hoge = class new : unit -> Hoge member showx : unit -> unit end > Hoge().showx();; 0 val it : unit = ()
privateなんでクラスの定義からはxの定義が見えていない。
memberはクラスのpublicな部分を作るために使うもんで指定しない限りpublicになる。
publicなフィールド
初期化されていないpublicなフィールドが必要な場合は、val キーワードを使用。
プライマリーコンストラクタ内では[
> type Hoge() = [<DefaultValue>] val mutable x:int;; type Hoge = class new : unit -> Hoge val mutable x: int end
また、classをフィールドとして使いたい場合には[
一方、プライマリコンストラクタがない場合は、[
> type Hoge = [<DefaultValue>] val mutable x:int new(a:int) = {x=a};; new(a:int) = {x=a};; -----------------^^^^^ stdin(3,18): error FS0765: Extraneous fields have been given values > type Hoge = val mutable x:int new(a:int) = {x=a};; type Hoge = class new : a:int -> Hoge val mutable x: int end
全部publicにして丸出しするなら、クラスじゃなくてレコードや構造体でいい気がするなぁ。
またF#の構造体は
によると、値渡しがデフォっぽいので、あまりいらない子な気がする。
プロパティ
Readオンリーがデフォルトになっている。
> type Hoge(x:int) = member this.X = x;; type Hoge = class new : x:int -> Hoge member X : int end > let x = Hoge 100;; val x : Hoge > x.X;; val it : int = 100 > x.X <- 10000;; x.X <- 10000;; ^^^ stdin(27,1): error FS0810: Property 'X' cannot be set
また、Visual Studio2012から自動プロパティ使えるようになったんで、積極的に使っていく。
以下はRead & Writeが可能なプロパティXを定義している例。
> type Hoge(x:int) = member val X = x with get,set;; type Hoge = class new : x:int -> Hoge member X : int member X : int with set end > let x = Hoge 10;; val x : Hoge > x.X <- 100;; val it : unit = () > x.X;; val it : int = 100
ただし、こいつには注意が必要で、
から例を拝借すると
> type MyClass() = let random = new System.Random() member val AutoProperty = random.Next() with get, set member this.ExplicitProperty = random.Next();; type MyClass = class new : unit -> MyClass member AutoProperty : int member ExplicitProperty : int member AutoProperty : int with set end > printfn "class1.AutoProperty = %d" class1.AutoProperty;; class1.AutoProperty = 517235997 val it : unit = () > printfn "class1.AutoProperty = %d" class1.AutoProperty;; class1.AutoProperty = 517235997 val it : unit = () > printfn "class1.ExplicitProperty = %d" class1.ExplicitProperty;; class1.ExplicitProperty = 70874627 val it : unit = () > printfn "class1.ExplicitProperty = %d" class1.ExplicitProperty;; class1.ExplicitProperty = 2067650569 val it : unit = ()
となっていることからわかるように、AutoProperty(自動プロパティ)の方は一度しか評価されない。
メソッド
これはあまり悩まない。member付けてself-identifierつけて関数書けばいい。
> type Hoge() = member this.plus10(x:int) = x+10;; type Hoge = class new : unit -> Hoge member plus10 : x:int -> int end > Hoge().plus10 100;; val it : int = 110
また、再帰関数の時、recは付けなくてもいい模様。
> type Hoge() = member this.factorial(x:int) = match x with | 0|1-> 1 | _ -> x*this.factorial (x-1);; type Hoge = class new : unit -> Hoge member factorial : x:int -> int end > Hoge().factorial(5);; val it : int = 120
abstractやstaticは必要になったら都度調べる方向で。
まとめ
ごちゃごちゃ考えるのが嫌なので、以下の方針で行くことにしたい。
- let束縛使いたいので、プライマリーコンストラクタはとりあえず書いとく
- val束縛の設定がめんどいので、追加のコンストラクタはあんま書きたくない
- 外からのアクセスはプロパティを通す&プロパティは簡単な記法で書く
ここの発想はC++/C#あたりと同じかんじだな。
といわけで、典型的な?クラスは以下のように書くことにしよう。
> type Hoge(x:int) = let twice(x) = 2*x member val X = x with get, set member this.Twice x = twice x;; type Hoge = class new : x:int -> Hoge member Twice : x:int -> int member X : int member X : int with set end > let x = Hoge(10);; val x : Hoge > x.Twice 4;; val it : int = 8 > x.X;; val it : int = 10 > x.X <- 10000;; val it : unit = () > x.X;; val it : int = 10000
また、valとletがゴチャるので、まとめておく。
機能 | プライマリーコンストラクタでの使用 | 追加のコンストラクタでの使用 | 使いどころ |
---|---|---|---|
let束縛 | ○ | × | privateな関数・フィールドを作成したい時にプライマリーコンストラクタで使用 |
val束縛 | ○ | ○ | publicな関数・フィールドを作成したい時、あるいは初期化されないフィールドが欲しい時に使用(private用途でも使用も可) |
do束縛 | ○ | × | プライマリーコンストラクタで副作用のある処理をかく |
then(束縛?) | × | ○ | 追加のコンストラクタで副作用のある処理をかく |
参考
三年前にクラスの学習記録メモを作っていたらしい。
なにはともあれのMSDN
「F# for Fun and Profit」のクラスに関する記事。かなり詳しくて助かる。
「OOP without classes in F#」という記事
コメント欄より
As for inheritance it is simply a bug. There are ways to get similar code reuse without breaking the type system. Unfortunately F# allows inheritance to be compatible with .NET.
継承はいらない子、俺もその方向で進んでいきたい。