最小のクラス

クラスとはデータと関数の集合で オブジェクト指向プログラミングでは 基本的なデータ構造です。 クラスの簡略化した定義は以下のようになります。より正確な構文はこのページの末尾に載せます。
クラスの定義
// Class definition: type type-name ( parameter-list ) [ as identifier ] = [ class ] [ inherit base-type-name(base-constructor-args) ] [ let-bindings ] [ do-bindings ] member-list ... [ end ]
まずは最小のクラスを宣言してみます。
最小のクラス
type Minimum = class end;;
これは、フィールドもメンバ関数も何も持っていないクラスMinimumの定義です。 最小のクラスの宣言はclassとendキーワードが必要です。 このままでは何の役にもたたないので 次にこれにフィールドを追加していきますが クラスからオブジェクトを作るための コンストラクタの定義に2種類の方法があります。 参考 それぞれプライマリーコンストラクターと追加コンストラクターと呼ばれます。

プライマリーコンストラクター

コンストラクタとは、クラスをインスタンス化してオブジェクトを作る際に 初期化をするために呼ばれる関数のことです。 プライマリーコンストラクターは以前本サイトでは暗黙的なコンストラクタと呼んでいたもので 追加コンストラクターよりも後にF#に追加されました。 プライマリーコンストラクターを使うには、型名の直後にパラメータを指定して使います。
最小のプライマリーコンストラクター
type Min() = class end
let m = new Min()
let n = Min() //newは省略可能
これは最小のクラスにプライマリーコンストラクターを追加したものです。 コンストラクターを追加したのでnewでオブジェクトを生成できるようになっています。 newキーワードはこの例のように省略しても構いません。
プライマリーコンストラクター
type PConSample(x:int) = class
    let a = x
    do
        printfn "side effect %d" a
    end;;
let im = new PConSample(10);;
// エラー。letで宣言した識別子はプライベートなのでアクセスできない
//printfn "%d" im.a
プライマリーコンストラクター(軽量構文)
type PConSample(x:int) =
    let a = x
    do
        printfn "side effect %d" a
let im = new PConSample(10)
// エラー。letで宣言した識別子はプライベートなのでアクセスできない
//printfn "%d" im.a
この例ではint型のxをパラメータとしてもつプライマリーコンストラクターを定義しています。 let aの個所がlet bindingでdo以下がdo bindingになります。 letで宣言した値や関数はプライベートなものとなり クラスの中からしか参照できません。 ここではdo bindingの中で参照しています。 do bindingはクラス作成時に行いたいアクションを記述します。 軽量構文でもdoは省略できません。 クラスの外部からもアクセスできるフィールドを宣言するにはmemberキーワードを用いてプロパティを宣言します
プライマリーコンストラクターとmember
type PC(x:int) = class
    do
        printfn "%d" x
    member this.v = x
    end;;
let p = PC(10);;
printfn "%d" p.v;;
プライマリーコンストラクターとmember(軽量構文)
type PC(x:int) =
    do
        printfn "%d" x
    member this.v = x
let p = PC(10)
printfn "%d" p.v
thisはオブジェクト自身を表すもので、別に名前はxでもselfでもなんでも構いません ただ、慣習的にthisと書く場合が多いです。 その後にあるvがプロパティメンバの名前です。 このコードではvは読み取り専用(immutable)です。 次のようにget,setを指定すること(これをアクセサといいます。getはgetter、setはsetterと呼ばれます)で 値を変更できるようにしてみましょう。
アクセサの利用
type PC(x:int) = class
    let mutable bs = x
    member this.v with get() = bs and set(value) = bs <- value
    end;;
let p = PC(10);
printfn "%d" p.v;
p.v <- 100;
printfn "%d" p.v;;
アクセサの利用(軽量構文)
type PC(x:int) =
    let mutable bs = x
    member this.v with get() = bs and set(value) = bs <- value

let p = PC(10)
printfn "%d" p.v
p.v <- 100
printfn "%d" p.v
プロパティの読み書きを可能にするため、元になるデータbsを宣言しています。 このようにプロパティの元になるデータのことをバッキングストアとも呼びます。 直接member this.v with get() = x and set(value) = this.v <- valueなどとすることは出来ません。(型エラーが出ます) なのでバッキングストアとなるデータを宣言する必要があります。 値を変更可能とするため、バッキングストアとなる識別子にはmutable(変更可能にするキーワード)をつけています。 getter/setterがこの例のようにシンプルなものはmember valキーワードを用いて宣言することで AutoPropertyという機能を用いて自動的に生成することが可能です。
AutoPropertyの利用
type PC(x:int) = class
    member val v=x with get,set
    end;;
let p = PC(10);
printfn "%d" p.v;
p.v <- 100;
printfn "%d" p.v;;
AutoPropertyの利用(軽量構文)
type PC(x:int) = 
    member val v=x with get,set
let p = PC(10)
printfn "%d" p.v
p.v <- 100
printfn "%d" p.v
with get,setの個所がAutoPropertyを利用している個所です。 getter/setterで複雑な処理を行うのでない限り、AutoPropertyを用いて記述するのが簡便です。

追加コンストラクター

追加コンストラクターとは以前は本サイトでは明示的なコンストラクタと呼ばれていたもので、 名前の通り追加のコンストラクタのため、基本的にはプライマリーコンストラクターと セットで使うことになることを想定されていると思います。 ですが、次のようにプライマリーコンストラクターなしでクラスを設計することも可能です。 また、このvalを用いたフィールドの宣言を明示的なフィールドと呼びます。
プライマリーコンストラクターのない追加コンストラクターのサンプル
type AC = class
    val mutable v : int
    new (x:int) = {v=x} then printfn "%d" x
    end;;
let ac = new AC(10);
ac.v <- 20;
printfn "%d" ac.v;;
プライマリーコンストラクターのない追加コンストラクターのサンプル(軽量構文)
type AC =
    val mutable v : int
    new (x:int) = {v=x} then printfn "%d" x
let ac = new AC(10)
ac.v <- 20
printfn "%d" ac.v
このように、フィールドの宣言にはvalを用います。値を変更可能とするため、ここではmutableもついています。 また、newから始まる行がコンストラクタと呼ばれるものです。 ここは若干複雑なので、簡略化した構文を述べると次のようになります。
簡略化したコンストラクタ定義
new パターン [as 識別子] = {[識別子=式 ;]*} [then 式]
上の例ではxがパラメータでvの値をxで初期化しています。 また、プライマリーコンストラクターがない場合do bindingは使えず 代わりにthenキーワードの後にdo binding相当のことをしています。 new AC(10)というコンストラクタの呼び出しによって newで定義した部分が呼び出されインスタンスが作成されます。 ここでもnewは省略することができます。 アクセスすることが出来ます。 次にプライマリーコンストラクターと追加コンストラクターを共存させるケースについて説明します。 プライマリーコンストラクターがある場合、追加コンストラクターで初期化時に 必ずプライマリーコンストラクターを呼ばなければなりません。 具体的な例は次のようになります。
プライマリーコンストラクターと追加コンストラクターの併用
type PAC(x:int,y:int) = class
    do
        printfn "x=%d,y=%d" x y
    member val px = x with get,set
    member val py = y with get,set
    //プライマリーコンストラクターの呼び出し。引数がない場合は0で初期化する
    new () = PAC(0,0) then printfn "0引数コンストラクタ"
    //プライマリーコンストラクターの呼び出し。引数が1つの場合はpx,pyを同じ値で初期化する
    new (z:int) = PAC(z,z) then printfn "1引数コンストラクタ"
    end;;
let p = PAC(10,10);
let q = PAC();
let r = PAC(15);
p.px <- 20;
p.py <- 30;
printfn "x=%d,y=%d" p.px p.py;;
プライマリーコンストラクターと追加コンストラクターの併用(軽量構文)
type PAC(x:int,y:int) =
    do
        printfn "x=%d,y=%d" x y
    member val px = x with get,set
    member val py = y with get,set
    //プライマリーコンストラクターの呼び出し。引数がない場合は0で初期化する
    new () = PAC(0,0) then printfn "0引数コンストラクタ"
    //プライマリーコンストラクターの呼び出し。引数が1つの場合はpx,pyを同じ値で初期化する
    new (z:int) = PAC(z,z) then printfn "1引数コンストラクタ"
let p = PAC(10,10)
let q = PAC()
let r = PAC(15)
p.px <- 20
p.py <- 30
printfn "x=%d,y=%d" p.px p.py
上のプログラムの実行結果
x=10,y=10
x=0,y=0
0引数コンストラクタ
x=15,y=15
1引数コンストラクタ
x=20,y=30
この例では、プライマリーコンストラクターと2種類の追加コンストラクター(new ()とnew (z:int))を定義しています。 追加コンストラクターでプライマリーコンストラクターを呼び出す際は {}は不要です。(あるとエラーになります) このプログラムを実行するとdo bindingとthenの評価順はdo bindingの方が先になることが確認できます。 明示的なフィールドに空の初期値(0や""など)を与えたい場合 DefaultValueという属性を用いて次のようにします。 また、必ずmutableをつけなければなりません。
明示的なフィールドとDefaultValue
type DV() = class
    []
    val mutable X : int
    end;;
let d = new DV();
printfn "%d" d.X
d.X <- 10
printfn "%d" d.X
余談ですが、継承にまつわる問題として 以下のような記事があるみたいです。 これについて私はあまり理解出来ていないのですが F#ではどうなのでしょうね
参考:Inheritance is not subtyping問題
あるクラスを継承したクラスが
継承元のクラスの部分型にならないことがある、という問題のこと。
OCamlではこういうことが起こりうるようです。
クラスのより正確な構文は以下のようになります。参考
クラスの定義
// Class definition: type [access-modifier] type-name [type-params] [access-modifier] ( parameter-list ) [ as identifier ] = [ class ] [ inherit base-type-name(base-constructor-args) ] [ let-bindings ] [ do-bindings ] member-list ... [ end ] // Mutually recursive class definitions: type [access-modifier] type-name1 ... and [access-modifier] type-name2 ... ...
最後に、メンバの定義の部分の構文についてまとめておきます。
メンバの定義
要素型定義 := | フィールド定義 | メンバ定義 | インターフェース定義 フィールド定義 :=  | [static] [mutable] 識別子 : 型名 メンバ定義 := | [ static ] member メンバ束縛 | (override|default) メンバ束縛 | コンストラクタ定義 | abstract シグニチャ定義 | val [mutable] 値、メンバのシグニチャ コンストラクタ定義 := | new パターン [as 識別子] = コンストラクタ式 コンストラクタ式 := | 文 ';' コンストラクタ式 | コンストラクタ式 then 式 | if 式 then コンストラクタ式 else コンストラクタ式 | let 変数宣言 in コンストラクタ式 | オブジェクトの初期化式 オブジェクトの初期化式 := | '{' [ inherit 式; ] フィールド束縛 '}' | new 型 式