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
  ...

次に、プライマリコンストラクタが無くなったことによって保持できなくなったrowcolvalとしてフィールド化します。 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#では基本的には脇役になってしまいますが、知っておくといつか役に立つことがあるかもしれません。 ということで今回はこの辺で。

*1:仕様書の一部分、書いてる人が意義を分かっていなさそうな部分がある・・・

*2:たまに、プライマリコンストラクタを呼び出す必要がある、としている説明を見かけますが、誤りです。ほかの追加オブジェクトコンストラクタでもいいですし、自分自身を呼び出しても構いません

*3:そうしない場合はコンパイルエラー

*4:さらに、mutableも必要になってきます。というか、mutableもつけないと常にゼロ値を表すフィールドになってしまって、意味がないので禁止されているのだと思います。エラーメッセージだけからはわかりにくいですが・・・

*5:Unchecked.defaultofは考えないものとします