F#のクラス(の主に定義する部分)についてまとめ
この記事はF# Advent Calendar 2015の5日目の記事です。 4日目は@n_enotの「F# でTDDした話 前編?」でした。
※業務連絡。F# Advent Calendar 2015の参加者の皆さん、今年は登録順ではなく、申請順になってしまっているようなので、イベントページに登録はしたけどまだ書く日付が決まっていない方は、イベントページを編集して書きたい日に自分の名前を書いておくようにしましょう。
関数型の何かと思った?
残念、クラスベースオブジェクト指向プログラミングの話でしたー。
今までF#のクラス定義に関する機能がよくわかっていなかった(まともに使ってこなかった、といってもいい)ので、ちょっとまとめつつ勉強してみました。 基本的に、言語仕様の「8.6 Class Type Definitions」をまとめた内容になっています。 より実践的な方向から再構成しているので、仕様書よりも読みやすく意義も伝わりやすいかな、と思います*1。
クラス定義の基本
F#でのクラス定義は次のようになっています。
type 型名 = class クラス本体 end
F#では、クラスは何もしなくてもシリアライズ可能です。
シリアライズ不可能にする場合、AutoSerializable(false)
属性を与える必要があります。
class/endの省略
通常、class
/end
は省いて定義します。
Class属性によるclass/endの省略
Class
属性をつけると、class
/end
は不要です。
[<Class>] type 型名 = クラス本体
Class
のほかに、Interface
属性やStruct
属性もあります。
列挙型は構文が全く違うためか、Enum
属性はありません。
ただ、これから説明する方法によるclass
/end
の方が便利なので、Class
属性は個人的にはあまり使いません。
クラス固有の要素によるclass/endの省略
下記の要素を持つ場合、class
/end
は不要です。
- プライマリコンストラクタ(primary constructors)
- 追加オブジェクトコンストラクタ(additional object constructors)
- 関数定義(function definitions)
- 値定義(value definitions)
- 非抽象メンバー(non-abstract members)
- 引数付きの継承宣言(inherit declarations that have arguments)
これらの詳しい説明は、それぞれの要素の説明の際に合わせてします(関数定義は引数があるかどうかだけなので、この記事では値定義としてまとめます)。
とりあえずは、「ほとんどの場合でclass
/end
は省略でき、コンパイルエラーになったらclass
/end
を書くかClass
属性をつける」と覚えてしまって大丈夫です。
コンストラクタの定義
F#には、コンストラクタは次の2種類があります。
プライマリコンストラクタ
プライマリコンストラクタは、型名の後ろに引数リストを書くことで定義します。
type 型名 (引数リスト) = クラス本体
この引数リストには、識別子か、型指定した識別子しか書けません。 例えば、
type SomeClass (x: int, y) = ...
はOKですが、
type SomeClass ((x1, y1), (x2, y2)) = ...
のように、複雑なパターンは書けません(コンパイルエラーになる)。 複雑なパターンが書きたい場合は、値定義(value definition)を使います。
type SomeClass (p1, p2) = let (x1, y1) = p1 let (x2, y2) = p2 ...
クラスの本体部分に書かれたメンバー定義(追加オブジェクトコンストラクタ含む)以前の部分は、プライマリコンストラクタの本体部分とみなせます。
これらのコードはプライマリコンストラクタの呼び出し時に実行されます。
let
のほかにdo
も使えます。
type SomeClass () = let x = 10 do printfn "%d" x
この例では、SomeClass
を生成するたびに10と表示されます。
他の部分のdo束縛と違い、ここでのdoは省略できない点に注意してください。
プライマリコンストラクタの補佐としての追加オブジェクトコンストラクタ
基本的にはプライマリコンストラクタを使えばいいですが、主に2つ以上のコンストラクタが提供したい場合には追加オブジェクトコンストラクタを定義します。
例えば、int
の値を2つ保持するクラスAが定義したいとします。
この場合、プライマリコンストラクタを使って
type SomeClass (x: int, y: int) = ...
とすることになりますが、y
の方は大体のケースで0
固定だ、という場合、プライマリコンストラクタ以外にコンストラクタが欲しくなります。
こういう場合に追加オブジェクトコンストラクタを使います。
type SomeClass (x: int, y: int) = new (x) = SomeClass(x, 0) ...
このように、new
キーワードを使うことで追加オブジェクトコンストラクタを定義できます。
追加オブジェクトコンストラクタでは、本体の最後の式としてコンストラクタを呼び出す必要があります*2。
コンストラクタの呼び出し後に何か副作用のあるような処理がしたい場合、then
を使います。
type SomeClass (x: int, y: int) = new (x) = printfn "プライマリコンストラクタを呼び出します。" SomeClass(x, 0) then printfn "プライマリコンストラクタが呼び出されました。"
それ以外の役割の追加オブジェクトコンストラクタ
継承が絡んでくるので、まず先に継承についてを説明します。
継承
継承はinherit
キーワードを使い、値定義よりも前に宣言します。
type SomeBaseClass (x: int) = do () type SomeSubClass () = inherit SomeBaseClass(10) let someValue = 20
クラスがプライマリコンストラクタを持つ場合、継承するクラス名に続けてそのクラスのコンストラクタに渡す引数を書きます。
上の例では、SomeBaseClass
のコンストラクタを引数10
で呼び出しています。
値定義など、継承宣言よりも後ろに書く必要のある変数等は使えないので注意してください。
また、継承宣言を省略すると、暗黙的にobj
が継承されます。
明示的にobj
を継承した場合とbase
(基底クラスのメンバーへの参照)の挙動が微妙に異なりますが、気にする場面は出てこないでしょう。
基底クラスの異なるコンストラクタが呼びたい場合に使う追加オブジェクトコンストラクタ
プライマリコンストラクタを持つ場合、すべてのコンストラクタは継承宣言で同時に呼び出される単一の基底クラスのコンストラクタが呼び出されます。 これでは困ったことになる場合があります。 例えば、基底クラスがシリアル化コンストラクタを提供しており、派生クラスもシリアル化可能にしたい独自の状態を持っている場合などです。 この場合、派生クラスは通常のコンストラクタとシリアル化コンストラクタで別々の基底クラスのコンストラクタを呼ぶ必要があります。
type ParseException (row: int, col: int) = inherit FormatException("failed to parse") new (info: SerializationInfo, context: StreamingContext) = (* ここで基底クラスのシリアル化コンストラクタを呼びたいけど呼べない! *)
プライマリコンストラクタを提供せずに、すべてを追加オブジェクトコンストラクタにすることで、これが実現できます。
まず、プライマリコンストラクタを提供しない場合、inherit
宣言に引数リストを付けれなくなります。
type ParseException = inherit FormatException ...
次に、プライマリコンストラクタが無くなったことによって保持できなくなったrow
とcol
をval
としてフィールド化します。
val
は値定義のlet
と比べられることも多いですが、let
はあくまでプライマリコンストラクタに付属するものに対して、val
はメンバー(フィールド)です。
そのため、プライマリコンストラクタを定義しないとlet
は使えません(今回の例ではlet
は使えない、ということです)。
let
はフィールドになるとは限りませんが、val
は常にフィールドになりますし、値定義ではなくメンバー定義のため、すべての値定義(do
含む)よりも後ろに書く必要がある点に注意してください。
val
は宣言と同時に初期値を持たせることはできませんが、(DefaultValue
属性の付かない)val
は、そのすべてをすべてのコンストラクタで初期化することが求められます*3。
これは、プライマリコンストラクタを持つ場合はDefaultValue
属性の付かないval
は定義できないことを意味します*4。
type ParseException = inherit FormatException val Row: int val Col: int ...
val
は既定ではpublic
なので、先頭を大文字にしてみました。
さて、本題の追加オブジェクトコンストラクタです。 これまでは追加オブジェクトコンストラクタでは他のコンストラクタを呼び出していました(delegate construction)が、基底クラスを呼び出すためには明示的構築(explicit construction)を行う必要があります。
type ParseException = inherit FormatException val Row: int val Col: int new (row, col) = { inherit FormatException("failed to parse"); Row = row; Col = col } new (info: SerializationInfo, context: StreamingContext) = let r = info.GetInt32("Row") let c = info.GetInt32("Col") (* F#ではセミコロンは改行に置き換え可能 *) { inherit FormatException(info, context) Row = r Col = c } (* ISerializableの実装は後で *)
このように、追加オブジェクトコンストラクタを使うことで、基底クラスの異なるコンストラクタが呼び出せるようになります。
ちなみに、オブジェクト構築後に副作用のある処理を実行したい場合は、then
を使います。
インターフェイスの実装
F#には空のインターフェイスの指定か、インターフェイスの明示的実装しかありません。
空のインターフェイスの指定
空のインターフェイスの指定は
type SomeClass () = interface IEmptyInterface
のように書きます。
インターフェイスの明示的実装
インターフェイスを実装するには、クラス名に続けてwith
を書き、その後にインターフェイスの持つメンバーの実装を書きます。
type ParseException = inherit FormatException val Row: int val Col: int new (row, col) = { inherit FormatException("failed to parse"); Row = row; Col = col } new (info: SerializationInfo, context: StreamingContext) = let r = info.GetInt32("Row") let c = info.GetInt32("Col") { inherit FormatException(info, context); Row = r; Col = c } interface ISerializable with member x.GetObjectData(info, context) = base.GetObjectData(info, context) info.AddValue("Row", x.Row) info.AddValue("Col", x.Col)
インターフェイスの実装では、すでにインターフェイスで型が明示されるため、すべてのメンバーで型を指定する必要がないのも特徴です。
可視性
F#では至る所で不変が既定になっているため、可視性が設定できるものはpublic
が既定と思ってかまいません(一部の例外の方を覚えればよい)。
もちろん、可視性はカスタマイズ可能ですので、可視性を狭めることは可能です。
また、追加オブジェクトコンストラクタの項でも書きましたが、let
はプライマリコンストラクタの一部であるため、let
には可視性を設定できません。
コンストラクタ内で作ったローカル変数くらいに思っておいた方が無難でしょう。
クラス自身の可視性
クラス自身の可視性は、type
とクラス名の間に書きます。
(* 型をinternalに制限 *) type internal SomeClass () = do ()
プライマリコンストラクタの可視性
プライマリコンストラクタの可視性は、クラス名と(
の間に書きます。
(* 型はpublicのまま、プライマリコンストラクタをinternalに制限 *) type SomeClass internal () = do ()
クラス自身とプライマリコンストラクタの両方に可視性を設定すると、一行に2つの可視性が書かれることになります。
(* 型はinternalに、プライマリコンストラクタはprivateに制限 *) type internal SomeClass private () = do ()
追加オブジェクトコンストラクタの可視性
追加オブジェクトコンストラクタの可視性は、new
と(
の間に書きます。
type SomeClass (x: int) = new internal () = SomeClass(0)
自己参照
プライマリコンストラクタでの自己参照
プライマリコンストラクタで自分自身(いわゆるthis
)のメンバーを使いたい場合、プライマリコンストラクタの引数リストの閉じかっこと=
の間にas
を使って書きます。
(* 名前はthisでもxでもなんでもいい(ここではself) *) type SomeClass () as self = ...
ただ、これを実際に使ったことがあるのは、型プロバイダーを実装する時くらいでしょうか・・・
追加オブジェクトコンストラクタでの自己参照
追加オブジェクトコンストラクタで自分自身のメンバーを使いたい場合、引数リストの閉じかっこと=
の間にas
を使って書きます。
type SomeClass (x: int) = (* 名前はthisでもxでもなんでもいい(ここではself) *) new () as self = ...
こちらは実際に使ったことはないかもしれません・・・
Tips
有効な値を持たないクラス定義
Phantom Typeで使うだけの、タグとして型が必要な場合があります。 その場合は値が不要ですので、値を持たない型で十分です。
type Hoge = class end
と定義すると、F#ではデフォルトでnull
が無効なため、有効な値がない型が簡単に作れます*5。
書けていないトピック
- 抽象クラスの定義
- 構造体の定義
- インターフェイスの定義
- 列挙型の定義(不要かも)
- メンバーの定義
いつか書く。
まとめ
オブジェクト指向プログラミング的な機能をあまり使ってこなかったため、今回初めて知ったことがかなりありました。 この記事だけを読むといろいろ面倒に見えますが、実際に書いてみると、よくあるケースを簡潔に書けるようにするためにいろいろと考えられていることがわかりました。 それでも、レコードや判別共用体を使った方が楽な面も多いですから、クラスはF#では基本的には脇役になってしまいますが、知っておくといつか役に立つことがあるかもしれません。 ということで今回はこの辺で。