実は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

詳しくは、

にあるMSDNの「コンストラクターでの副作用の実行」を読むといい。

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.

継承はいらない子、俺もその方向で進んでいきたい。