open netshop

Chapter 1. オブジェクト指向プログラミング

Table of Contents

1. クラス
1.1. クラス定義の基本
1.2. フィールド
1.3. メソッド
1.4. プロパティ
1.5. クラスメンバのアクセス制御
1.6. コンストラクタ
1.7. インスタンスの生成
1.8. 静的メンバ
1.9. 可変フィールド
1.10. 可変フィールドに対するプロパティ
1.11. インデックス付きプロパティ
1.12. 継承
1.13. アップキャスト・ダウンキャスト
1.14. 抽象メンバ
1.15. 名前つき引数
1.16. オプション引数
1.17. メソッドのオーバーロード
1.18. 練習問題
2. インターフェイス
2.1. インターフェイスの定義方法
2.2. インターフェイスのメンバ呼び出し
2.3. 練習問題
3. 構造体
3.1. 構造体の定義方法
3.2. 練習問題
4. 列挙型
4.1. 列挙型の定義方法
4.2. 練習問題
5. F#特有の型とクラスの関係
5.1. F#特有の型の内部実装
5.2. 練習問題
6. クラス・インターフェイス・構造体の糖衣構文
6.1. 種推論
6.2. 暗黙的クラス定義
6.3. オブジェクト式
6.4. 型拡張
6.5. 練習問題

F#は関数型言語ですが、オブジェクト指向プログラミングや命令型プログラミングもサポートしています。そもそも、.NET Frameworkはオブジェクト指向の上に成り立つフレームワークであるため、その上で動作する言語も本格的なオブジェクト指向の機能を利用することができます。さらに、F#はオブジェクト指向以外にも複数のプログラミングパラダイムを利用できるため、言語開発設計者であるDon Symeは単なる関数型言語ではなくマルチパラダイム言語と呼んでいます[ref]。

前章で紹介した命令型プログラミングは、関数型プログラミングとは背反するところが大きく、両方を効果的に組み合わせて使うことは困難でした。ところが、オブジェクト指向プログラミングというプログラミングパラダイムは、関数型言語とは背反するものではなく、両方を組み合わせて両方の恩恵を同時に享受することができます。

現在では、オブジェクト指向プログラミングは非常にメジャーなプログラミングパラダイムとなっています。実際、ビジネスの分野で幅広く使われており、それを学ぶための情報源も解説書・セミナー・授業・インターネットなど莫大な数を誇ります。その情報源の大半はJava、C#、C++といった命令型のオブジェクト指向言語を使用しているため、オブジェクト指向は命令型プログラミングの上に成り立つものという強い印象がありますが、関数型プログラミングの上には成り立たないというわけではありません。ここでは、F#におけるオブジェクト指向の機能を紹介するとともに、関数型プログラミングとの融合についても説明していきます。

1. クラス

まずは、F#におけるクラス定義の基本について説明していきます。ここで説明することは、基本的にC#でもおなじみの概念ばかりなので、あまりひっかからずに読めると思います。ですので、ここでは概念よりも、C#とF#の文法の違いを意識しながら読み進めていってください。ただし、ここでもベースとなる考え方は「不変」であることに注意してください。

F#によるクラスの定義方法には大きく分けて2つの種類があります。ひとつめは「明示的クラス定義」という定義方法で、もうひとつは「暗黙的クラス定義」という定義方法です。暗黙的クラス定義は明示的クラス定義の構文をもっと簡潔にしたものですが、基本的にはどちらを使っても全く同じクラスを定義することができます。

F#では暗黙的クラス定義をよく使いますが、その構文にはかなりわかりにくいところがあり、いきなりそれを説明しても混乱をきたすと思います(実際、私はかなり混乱しました)。そこで本サイトでは、クラス定義の基本としてまず明示的クラス定義を使って全体的な説明をし、その後に簡易的な記法として暗黙的クラス定義を説明していくことにします。

1.1. クラス定義の基本

この節では、クラスの例として矩形を表現するRectクラスを作りながらクラス定義の基本を紹介していきます。まず、以下の定義はフィールドもメソッドももたない空のRectクラスの定義です。メソッドやフィールドは、classendに囲まれた部分に追加していきます。

> type Rect =
    class
    end;;

type Rect =
  class
  end

1.2. フィールド

まずは矩形を表現するために、矩形の4つの点を表すフィールドを追加します。

> type Rect =
    class
      val top    : float
      val left   : float
      val bottom : float
      val right  : float
    end;;

type Rect =
  class
    val top: float
    val left: float
    val bottom: float
    val right: float
  end

各フィールドはvalキーワードに続けて宣言します。ここではfloat型のtopleftbottomrightというフィールドを宣言しています。この方法で定義されたフィールドを、明示的フィールド(explicit field)と呼びます。

1.3. メソッド

次に矩形の面積を計算するAreaメソッドを追加します。

> type Rect =
    class
      val top    : float
      val left   : float
      val bottom : float
      val right  : float
      member this.Area() = (this.right - this.left) * (this.bottom  - this.top)
    end;;

type Rect =
  class
    val top: float
    val left: float
    val bottom: float
    val right: float
    member Area : unit -> float
  end

メソッドの定義はmemberキーワードに続けて行います。ここでthisという部分に注目してください。これはC#のthisキーワードに相当するもので、自分自身のインスタンスを表しています。これは自己識別子(self-identifier)と呼びます。

上の例では自己識別子にthisという識別子名を使いましたが、F#ではthisは特別なキーワードではなく、他の識別子名をつけることも可能です。たとえば、thisのかわりにvという識別子名を使う場合は以下のように書きます。

member v.Area() = (v.right - v.left) * (v.bottom - v.top)

この例では、メソッドAreaを修飾している識別子vが自己識別子名として認識され、メソッドの定義の中でもvという名前を使って自身のフィールドにアクセスしています。

F#では、自身のフィールドやメソッドなどにアクセスするときは必ず自己識別子をつなければなりません。C#ではフィールド名がほかの変数名などと競合していないかぎり、thisキーワードを省略することができましたが、F#では自己識別子名で修飾しないとコンパイルエラーとなります。

1.4. プロパティ

次はプロパティを追加してみましょう。ここでは、4つの点を表すTopBottomLeftRightと、幅と高さを表すWidthHeightを追加します。

> type Rect =
    class
      // フィールド
      val top    : float
      val left   : float
      val bottom : float
      val right  : float
      // メソッド
      member this.Area() = this.Width * this.Height
      // プロパティ
      member this.Top    = this.top
      member this.Bottom = this.bottom
      member this.Left   = this.left
      member this.Right  = this.right
      member this.Width  = this.right - this.left
      member this.Height = this.bottom - this.top
    end;;

type Rect =
  class
    val top: float
    val left: float
    val bottom: float
    val right: float
    member Area : unit -> float
    member Bottom : float
    member Height : float
    member Left : float
    member Right : float
    member Top : float
    member Width : float
  end

1.5. クラスメンバのアクセス制御

ここまで紹介したフィールド、メソッド、プロパティにはprivatepublicといったアクセス制御(access control)の指定をしてきませんでした。F#ではアクセス制御の指定がされなかった場合は、基本的にpublicとして扱います。したがって、ここまで紹介したフィールド、メソッド、プロパティはすべてpublicとして扱われます。これらに対してアクセス制御を指定するには、valおよびmemberキーワードの直後にアクセス指定子を書きます。アクセス指定子には以下の種類があります。

アクセス指定子概要
publicどこからでもアクセス可能
privateクラス内のみからアクセス可能
internal同一アセンブリ内からアクセス可能

例としてRectのフィールドをprivateにするには以下のように指定します。

> type Rect =
    class
      val private top    : float
      val private left   : float
      val private bottom : float
      val private right  : float
      member this.Area() = this.Width * this.Height
      member this.Top    = this.top
      member this.Bottom = this.bottom
      member this.Left   = this.left
      member this.Right  = this.right
      member this.Width  = this.right - this.left
      member this.Height = this.bottom - this.top
    end;;

type Rect =
  class
    val private top: float
    val private left: float
    val private bottom: float
    val private right: float
    member Area : unit -> float
    member Bottom : float
    member Height : float
    member Left : float
    member Right : float
    member Top : float
    member Width : float
  end

その他のメソッドやプロパティは何も指定していないので、publicとして扱われます。もちろん、メソッドやプロパティに対して明示的にpublicを指定することもできます。

1.6. コンストラクタ

ここでRectクラスにコンストラクタを定義します。C#ではクラスにコンストラクタがひとつも定義されていないとき、コンパイラが自動的にデフォルトコンストラクタ(引数なしのコンストラクタ)を作ってくれます。しかし、F#では必ず自分でコンストラクタを定義しなければなりません。もし定義されていないと、そのクラスのインスタンスを生成することはできません(ただし、後述する静的メンバならば使うことができます)。ここまで定義してきたRectクラスは、実はまだインスタンスを生成することはできませんでした。ここでコンストラクタを定義することで、はじめてRectクラスのインスタンスを生成することができるようになります。

では、実際にコンストラクタを定義してみましょう。ここでは4つのフィールドを初期化するコンストラクタと、引数をとらないコンストラクタ(デフォルトコンストラクタ)を定義します。

> type Rect =
    class
      // フィールド
      val private top    : float
      val private left   : float
      val private bottom : float
      val private right  : float
      // コンストラクタ
      new (t, l, b, r) = { top = t; left = l; bottom = b; right = r }
      new () = new Rect(0.0, 0.0, 0.0, 0.0)
      // メソッド
      member this.Area() = this.Width * this.Height
      // プロパティ
      member this.Top    = this.top
      member this.Bottom = this.bottom
      member this.Left   = this.left
      member this.Right  = this.right
      member this.Width  = this.right - this.left
      member this.Height = this.bottom - this.top
    end;;

コンストラクタの本体、すなわち=の右側にはオブジェクト初期化式(object initialization expression)を書くことができます。オブジェクト初期化式には、他のコンストラクタの呼び出しか、フィールドの初期化式のどちらかを書くことができます。上の例でいうと、デフォルトコンストラクタは他の(4引数の)コンストラクタ呼び出しを使っており、4引数のコンストラクタはフィールドの初期化式を使っています。

フィールドの初期化式では、以下のようにクラス内の各フィールドに初期値をセットします。このとき、クラス内のすべてのフィールドを何らかの値で初期化する必要があります。もし値がセットされていないフィールドがあると、コンパイルエラーになります。

{ フィールド1 = 1 ; [フィールドn = n ;]* }

オブジェクト初期化式の前後には、条件分岐、前処理、後処理、let束縛をはさむことができます。

; オブジェクト初期化式 // 前処理としてを実行
オブジェクト初期化式;  // 前処理としてを実行
if  then オブジェクト初期化式1 else オブジェクト初期化式2 // の値に応じて実行するオブジェクト初期化式を変える
let 束縛式 in オブジェクト初期化式 // オブジェクト初期化式の中だけで有効な識別子を定義する

1番目と2番目はそれぞれ直前と直後に処理を行い、3番目は条件分岐を行い、4番目はlet束縛を行います(軽量構文の場合は1番目と4番目の式のセミコロンとinは省略できます)。これらの式は入れ子にして組み合わせることもできます。いくつかの例を示します。

new() =
  printf "pre-process" // 前処理
  { top = t; left = l; bottom = b; right = r }
  then printf "post-process" // 後処理

new(size) =
  let zero = 0.0 // let束縛
  if size <= 0.0 // 条件分岐
    else { top = zero; left = zero; bottom = zero; right = zero }
    then { top = zero; left = zero; bottom = size; right = size }

コンストラクタ内で自己識別子が必要になる場合があります。そのときはasキーワードを使って自己識別子を表す識別子を宣言することができます。

new() as this =
  { top = t; left = l; bottom = b; right = r }
  then printf "Area()=%f" (this.Area())

1.7. インスタンスの生成

ではこのクラスのインスタンスを生成して、実際に定義したメソッドやフィールドを呼び出してみましょう。まず、クラスのインスタンスを生成するには、C#と同じようにnewキーワードを使いますが、F#ではnewは書いても書かなくてもどちらでもかまいません。メソッドやフィールドを呼び出すには、C#と同じようにドットのあとにその名前を書きます。

> let r = new Rect(1.0, 1.0, 5.0, 5.0);; // インスタンスの生成
val r : Rect

> r.Top;; // Topプロパティ
val it : float = 1.0

> r.Width;; // Widthプロパティ
val it : float = 4.0

> r.Area();; // Areaメソッド
val it : float = 16.0

> r.top;; // privateなtopフィールドへアクセス

  r.top;;
  ^^^^^^

stdin(40,1): error FS0191: The record field 'top' is not accessible from this code location.

> let r2 = Rect(1.0, 1.0, 5.0, 5.0);; // インスタンスの生成(newキーワードは省略)
val r2 = Rect

1.8. 静的メンバ

静的メソッド、静的プロパティ、静的フィールドといった静的メンバを宣言/定義するには、staticキーワードを使用します。例として、2つの矩形が合同であるか調べるIsCongruentという静的メソッドを追加してみましょう。

> type Rect =
    class
      // フィールド
      val private top    : float
      val private left   : float
      val private bottom : float
      val private right  : float
      // メソッド
      member this.Area() = this.Width * this.Height
      // 静的メソッド
      static member IsCongruent(x : Rect, y : Rect) = (x.Width = y.Width) && (x.Height = y.Height)
      // プロパティ
      member this.Top    = this.top
      member this.Bottom = this.bottom
      member this.Left   = this.left
      member this.Right  = this.right
      member this.Width  = this.right - this.left
      member this.Height = this.bottom - this.top
    end;;

type Rect =
  class
    val private top: float
    val private left: float
    val private bottom: float
    val private right: float
    member Area : unit -> float
    static member IsCongruent : x:Rect * y:Rect -> bool
    member Bottom : float
    member Height : float
    member Left : float
    member Right : float
    member Top : float
    member Width : float
  end

静的メンバへのアクセスは、C#と同じようにクラス名につづけてドットとメンバ名を書きます。

> let x = new Rect(0.0,0.0,10.0,5.0);; // 矩形のインスタンスを生成
val x : Rect

> let y = new Rect(100.0,100.0,110.0,105.0);; // xと合同な矩形のインスタンスを生成
val y : Rect

> Rect.IsCongruent(x,y);; // 静的メソッドの呼び出し
val it : bool = true

> let z = new Rect(0.0,0.0,10.0,10.0);; // xとは合同ではない矩形のインスタンスを生成
val z : Rect

> Rect.IsCongruent(x,z);; // 静的メソッドの呼び出し
val it : bool = false

ちなみに静的フィールドの宣言は少し注意が必要です。静的フィールドは必ずmutableを指定し、かつDefaultValue属性を指定しなければなりません。

type Rect =
  class
    [<DefaultValue(true)>]
    static val mutable v : float
  end

DefaultValue属性にはtruefalseのどちらかを指定することができます。trueにするとその値は必ず0で初期化され、falseにするとその値は初期化されません。

1.9. 可変フィールド

F#では、ここまで紹介してきたように一度定義したら変わらない不変フィールド(immutable field)を基本とします。しかし、命令型プログラミングのところで説明したように、可変なフィールドを用いたほうが適切な場合があります。

可変フィールドは命令型プログラミングのところで説明したように、<-演算子によって値を書き換えることができます。また、可変フィールドも通常のフィールドと同じように、オブジェクト初期化式などで初期化する必要があります。

> type Counter =
    class
      val mutable private counter : int  // 可変フィールドを宣言
      member this.incr() = this.counter <- this.counter + 1 // <-演算子で内容を書き換える
      member this.Value = this.counter
      new() = { counter = 0 } // オブジェクト初期化式で初期化する
    end;;

type Counter =
  class
    val mutable private counter: int
    new : unit -> Counter
    member Value : int
    member incr : unit -> unit
  end

> let counter = new Counter();; // インスタンスを生成
val counter : Counter

> counter.Value;; // 現在の可変フィールドの値を調べる
val it : int = 0

> counter.incr();; // 可変フィールドの値を書き換える
val it : unit = ()

> counter.Value;; // 現在の可変フィールドの値を調べる
val it : int = 1

1.10. 可変フィールドに対するプロパティ

少し前の小節ではプロパティのを説明をしましたが、そこで紹介したものはすべて不変フィールドに対するもので、読み取り専用のプロパティでした。読み取り専用のプロパティは、C#でいうとgetアクセサのみをもつプロパティです。しかし、可変フィールドの場合はsetアクセサも必要となります。ここでは、F#のプロパティに関してもう少し詳しく説明します。

まずCounterクラスのプロパティの定義を振り返ってみましょう。

type Counter =
  class
    val mutable private counter : int
    member this.incr() = this.counter <- this.counter + 1
    member this.Value = this.counter
    new() = { counter = 0 }
  end

このValueプロパティは読み取り専用です。読み取り専用プロパティは以下のように書くこともできます。

member this.Value with get() = this.counter

この書き方だとgetアクセサのみをもつことがわかり、読み取り専用であることがよりはっきりします。

このCounterクラスにsetアクセサを追加すると以下のようになります。

type Counter =
  class
    val mutable private counter : int
    member this.incr() = this.counter <- this.counter + 1
    member this.Value
      with get()  = this.counter
      and  set(v) = this.counter <- v
    new() = { counter = 0 }
  end

このように、getsetを両方もつときは「withand ~ 」の中にそれらを定義します。また、setアクセサのみをもつプロパティを定義することもできます。

1.11. インデックス付きプロパティ

インデックス付きプロパティ(indexed property)は通常のプロパティと同じように定義しますが、getアクセサの引数がunit型ではなくインデックス番号を表す引数になり、setアクセサではインデックス番号を表す引数がひとつ増えます。

以下にインデックス付きプロパティを使ったクラスの定義を示します。このVector3Dクラスは3つの値xyzというをもつ(すなわち3次元のベクトルを表す)クラスです。このクラスでは、xyzのそれぞれの値にアクセスするために、インデックス付きプロパティを使っています。0番目のインデックスはx、1番目のインデックスはy、2番目のインデックスはzを表します。このインデックス付きプロパティ定義のget/setキーワードを見てみると、その直後にindexという引数があるのが確認できます。ちなみに、ここではその場合分けを表現するためにmatch式を使っています。

> type Vector3D=
    class
      val mutable private x : int
      val mutable private y : int
      val mutable private z : int
      member this.Item // インデックス付きプロパティを定義
        with get index = // getアクセサの定義
          match index with // indexに応じて可変フィールドの値を返す
          | 0 -> this.x
          | 1 -> this.y
          | 2 -> this.z
          | _ -> raise (System.IndexOutOfRangeException())
        and  set index v = // setアクセサの定義
          match index with // indexに応じて可変フィールドを書き換える
          | 0 -> this.x <- v
          | 1 -> this.y <- v
          | 2 -> this.z <- v
          | _ -> raise (System.IndexOutOfRangeException())     
      new() = { x = 0; y = 0; z = 0 } // コンストラクタ
    end;;

type Vector3D =
  class
    val mutable private x: int
    val mutable private y: int
    val mutable private z: int
    new : unit -> Vector3D
    member Item : index:int -> int with get
    member Item : index:int -> int with set
  end

> let vec = new Vector3D();; // インスタンスを生成
val vec : Vector3D

> vec.[0];; // xにアクセス
val it : int = 0

> vec.[3];; // 範囲外にアクセス
System.IndexOutOfRangeException: インデックスが配列の境界外です。
   場所 FSI_0041.Vector3D.get_Item(Int32 index)
   場所 <StartupCode$FSI_0045>.$FSI_0045._main()
stopped due to error

> vec.[0] <- 5;; // xの値を書き換え
val it : unit = ()

> vec.[0];; // 書き換えられたことを確認
val it : int = 5

1.12. 継承

クラスを継承する場合は、classキーワードの直後にinherit 継承クラスという形でそのクラスを指定します。ここでは例として、先ほど定義したRectクラスを継承してTransparentRectクラスを定義します。このクラスは新たにTranparencyという透明度(0.0~1.0)を表すプロパティをもちます。

派生クラスのコンストラクタでは、基底クラスの初期化をする必要があります。基底クラスの初期化は、オブジェクト初期化式の先頭で、inheritキーワードに続けて基底クラスのコンストラクタを呼び出します。オブジェクト初期化式内で基底クラスのコンストラクタ呼び出しを省略した場合は、暗黙的に基底クラスのデフォルトコンストラクタが呼び出されます。

実行例は以下のとおりです。ついでに、Rectクラスの定義も下のほうに再掲しておきます。

> type TransparentRect =
    class
      inherit Rect 
      val transparency : float
      member this.Transparency = this.transparency
      new (t, l, b, r, trans) = {
        inherit Rect(t, l, b, r); // 基底クラスのコンストラクタ呼び出し
        transparency = trans
      }
    end;;

type TransparentRect =
  class
    inherit Rect
    val transparency: float
    new : t:float * l:float * b:float * r:float * trans:float -> TransparentRect
    member Transparency : float
  end

> let r = new TransparentRect(0.0, 0.0, 1.0, 1.0, 0.5);; // 透明度0.5の矩形
val r : TransparentRect

> r.Transparency;;
val it : float = 0.5

> r.Width;;
val it : float = 1.0
type Rect =
  class
    // フィールド
    val private top    : float
    val private left   : float
    val private bottom : float
    val private right  : float
    // コンストラクタ
    new (t, l, b, r) = { top = t; left = l; bottom = b; right = r }
    new () = new Rect(0.0, 0.0, 0.0, 0.0)
    // メソッド
    member this.Area() = this.Width * this.Height
    // 静的メソッド
    static member IsCongruent(x : Rect, y : Rect) = (x.Width = y.Width) && (x.Height = y.Height)
    // プロパティ
    member this.Top    = this.top
    member this.Bottom = this.bottom
    member this.Left   = this.left
    member this.Right  = this.right
    member this.Width  = this.right - this.left
    member this.Height = this.bottom - this.top
  end

1.13. アップキャスト・ダウンキャスト

アップキャスト(upcast)は派生クラスから基底クラスへの型変換、ダウンキャスト(downcast)は基底クラスから派生クラスへの型変換です。派生クラスから基底クラスへの型変換はコンパイル時にチェックすることができますが、逆は実行時でないとチェックできません。したがって、F#ではアップキャストを静的型変換(static coercion)、ダウンキャストを動的型変換(dynamic coercion)とも呼びます。以下に例を示します。

> let hello_obj = "hello" :> obj;; // アップキャスト(string -> obj)
val hello_obj : obj

> let hello_string = hello_obj :?> string;; // ダウンキャスト(obj -> strint)
val hello_string : string

> let hello_string = hello_obj :?> int;; // 不正なダウンキャスト(obj -> int)
System.InvalidCastException: 指定されたキャストは有効ではありません。
   場所 <StartupCode$FSI_0013>.$FSI_0013._main()
stopped due to error
val hello_string : int

アップキャストとダウンキャストは、それぞれ:>:?>という演算子によっておこないます。また、かわりにupcastdowncastキーワードを使うこともできます。これらの演算子によって型変換をおこなう式を、静的型変換式(static coercion expression)、動的型変換式(dynamic coercion expression)と呼びます。

> let hello_obj = upcast("hello") : obj;; // アップキャスト(string -> obj)
val hello_obj : obj

> let hello_string = downcast(hello_obj) : string;; // ダウンキャスト(obj -> strint)
val hello_string : string

上の例のように、不正なダウンキャストは実行時エラーとなります。正常にダウンキャストすることができるかをチェックするには、動的型テスト式(dynamic type test expression)を使います。上のhello_objをテストするには以下のように書きます。

> (hello_obj :? string);;
val it : bool = true

> (hello_obj :? int);;
val it : bool = false

1.14. 抽象メンバ

抽象メンバとは名前(メソッド名、プロパティ名)と型だけが宣言され、その本体が定義されていないメンバです。そのようなメンバをもつクラスを抽象クラスといいます。抽象メンバは、抽象クラスにおいて名前と型だけを宣言しておき、その派生クラスで関数の本体の定義をします。抽象メンバの宣言は、abstractキーワードに続いてメンバ名と型を書きます。また、抽象メンバをもつクラスには、AbstractClass属性を指定しなければなりません。

例として、図形を表すFigureクラスがあり、このクラスからRectクラスやCircleクラスを派生させるとします。Figureクラスには、Areaという面積を返す抽象メソッドを用意し、派生クラスでは自身の面積を計算して返すように実装します。

[<AbstractClass>]
type Figure =
  class
    new() = {}
    abstract Area : unit -> float
  end

type Circle =
  class
    inherit Figure
    val radius : float
    new(r) = { radius = r }
    override this.Area() = this.radius * this.radius * 3.14159265
  end

type Rect =
  class
    inherit Figure
    val width  : float
    val height : float
    new(w,h) = { width = w; height = h }
    override this.Area() = this.width * this.height
  end

Figureクラスでは、abstractキーワードによって抽象メンバの宣言をし、派生クラスであるCircleクラスとRectクラスではoverrideキーワードによってそのメンバの本体を実装しています。

抽象メンバは本体が実装されていない抽象的なメンバですが、デフォルトの実装を与えることもできます。もし派生クラスで、その抽象メンバが実装されなかった場合は、かわりにデフォルトの実装が呼びだされます。派生クラスで実装された場合は、その新しい実装で上書きされます。デフォルト実装を与えるには、抽象クラス内でdefaultキーワードを用いてその実装をします。例として、Figureクラスにデフォルト実装を与えてみましょう。

[<AbstractClass>]
type Figure =
  class
    new() = {}
    abstract Area : unit -> float
    default this.Area() = 0.0
  end

この実装では、デフォルト実装として面積0を返すような定義をしました。

Note

C#には仮想メンバ(virtual member)というものがありますが、F#には直接それに相当する言葉はありません。その代わりF#では、抽象メンバ宣言+デフォルト実装という組み合わせでそれと同じものを表します。

Note

実は現在のF#の言語仕様(F# 1.9.6)では、defaultoverrideの実装は中途半端で、コンパイル時にきちんとチェックされていません。現在のコンパイラでは、実はdefaultoverrideは単なる別名として定義されているだけです。したがって、defaultと書いたにもかかわらず、F# Interactiveがoverrideと画面に出力する場合がありますが、あまり気にしないでください。将来的にはきちんと実装するそうです。

1.15. 名前つき引数

通常、メソッドを呼び出すときの実引数は、定義された仮引数の順番どおりに並べる必要があります。しかし、名前つき引数(named arguments)を使うと、呼び出す際の実引数の順番を入れ替えることができます。当然、ただ並び替えるだけでは、どの実引数がどの仮引数に対応するのかわからなくなってしまうので、名前つき引数を使う場合には必ず仮引数の名前を指定します。

> type Point =
    class
      val mutable private x : float
      val mutable private y : float
      member this.X with get() = this.x and set(x) = this.x <- x
      member this.Y with get() = this.y and set(y) = this.y <- y
      new(x_val,y_val) = { x = x_val; y = y_val }
    end;;

type Point =
  class
    val mutable private x: float
    val mutable private y: float
    new : x_val:float * y_val:float -> Point
    member X : float
    member Y : float
    member X : float with set
    member Y : float with set
  end

> let p = new Point(1.0,3.14);; // 通常のメソッド呼び出し
val p : Point

> p;;
val it : Point = FSI_0166+Point {X = 1.0;
                                 Y = 3.14;}

> let p = new Point(y_val=3.14, x_val=1.0);; // 名前つき引数によるメソッド呼び出し
val p : Point

> p;;
val it : Point = FSI_0182+Point {X = 1.0;
                                 Y = 3.14;}

名前つき引数は、単に仮引数の名前を指定して呼び出すことができるだけでなく、同時にプロパティの名を指定して、呼出し後にそのプロパティに値をセットするという機能もあります。

> type NamedPoint = // 名前をもつ点を表現するクラス
    class
      val mutable private x : float
      val mutable private y : float
      val mutable private name : string
      member this.X with get() = this.x and set(x) = this.x <- x
      member this.Y with get() = this.y and set(y) = this.y <- y
      member this.Name with get() = this.name and set(name) = this.name <- name
      new(x_val,y_val) = { x = x_val; y = y_val; name = "" }
    end;;

type NamedPoint =
  class
    val mutable private x: float
    val mutable private y: float
    val mutable private name: string
    new : x_val:float * y_val:float -> NamedPoint
    member Name : string
    member X : float
    member Y : float
    member Name : string with set
    member X : float with set
    member Y : float with set
  end

> let p = new NamedPoint(x_val=1.0,y_val=3.14);; // 仮引数の名前を指定して呼び出し
val p : NamedPoint

> p;;
val it : NamedPoint = FSI_0190+NamedPoint {Name = "";
                                           X = 1.0;
                                           Y = 3.14;}

> let p = new NamedPoint(x_val=1.0,y_val=3.14,Name="Point1");; // 仮引数と同時にプロパティ名を指定
val p : NamedPoint

> p;;
val it : NamedPoint = FSI_0190+NamedPoint {Name = "Point1";
                                           X = 1.0;
                                           Y = 3.14;}

このプロパティを同時にセットする機能は、.NET Frameworkのクラスを利用するときによく使います。たとえば、System.Windows.Forms.Formクラスはウィンドウの基本となるクラスで、非常によく使われますが、このクラスはデフォルトコンストラクタしかもちません。つまり、引数なしのコンストラクタで一度インスタンスを生成して、そのあとに改めて各種プロパティをセットしなければなりません。このようなときに非常に役に立ちます。

1.16. オプション引数

オプション引数(optional arguments)を使うと、メソッド呼び出しのときの実引数を一部省略することができます。メソッドの定義時に、引数名の前に?をつけると、その引数はオプション引数として省略可能となります。オプション引数として指定された引数は、option型に包まれます。たとえばint型の引数に?をつけると、その引数はint option型となります。そして、呼び出し時にその引数が省略された場合はNoneが渡され、省略されない場合はSomeにその値が包まれて渡されます。

例として、先ほどのNamedPointクラスのコンストラクタにオプション引数を使ってみます。このコンストラクタはオプション引数として、第3引数に名前を表す文字列を受け取ります。もしその引数が省略された場合は、名前は空の文字列に初期化されます。

> let default_string_arg arg = // option<string>型を処理する補助関数
    match arg with
    | Some x -> x    // Someのときは包まれている文字列を取り出して返す
    | None   -> "";; // Noneのときは空の文字列を返す

val default_arg : string option -> string

> type NamedPoint =
      class
        val mutable private x : float
        val mutable private y : float
        val mutable private name : string
        member this.X with get() = this.x and set(x) = this.x <- x
        member this.Y with get() = this.y and set(y) = this.y <- y
        member this.Name with get() = this.name and set(name) = this.name <- name
        new(x_val,y_val,?name) = // オプション引数を用いたコンストラクタ
          { x = x_val; y = y_val;
            name = default_string_arg name }
      end;;

type NamedPoint =
  class
    val mutable private x: float
    val mutable private y: float
    val mutable private name: string
    new : x_val:float * y_val:float * ?name:string -> NamedPoint
    member Name : string
    member X : float
    member Y : float
    member Name : string with set
    member X : float with set
    member Y : float with set
  end

> let p = new NamedPoint(0.0,0.0);;

val p : NamedPoint

> p;;
val it : NamedPoint = FSI_0002+NamedPoint {Name = "";
                                           X = 0.0;
                                           Y = 0.0;}
> let p = new NamedPoint(0.0,0.0,"Point1");;

val p : NamedPoint

> p;;
val it : NamedPoint = FSI_0002+NamedPoint {Name = "Point1";
                                           X = 0.0;
                                           Y = 0.0;}

まず、クラスを定義する前に補助関数としてdefault_string_argというものを定義しています。この関数はoption型の値を受け取り、それが値を持っていた場合はその値を返し、持っていなければ空文字列を返します。そして、この関数を用いてコンストラクタを定義しています。

1.17. メソッドのオーバーロード

C#ではメソッドのオーバーロード定義がサポートされています。メソッドのオーバーロード定義とは、名前が同じで引数の数と型が異なるようなメソッドを複数個定義することをいいます。F#におけるメソッドのオーバーロードは、少々特別な形で定義しなければなりません。具体的には、オーバーロード定義された複数のメソッドには、必ずそれぞれに対してOverloadID属性を付加する必要があります。例を見てみましょう。

> type Comparator =
    class
      [<OverloadID("1")>] // OverloadID属性を付加
      static member Compare(x:int, y:int) = x = y
      [<OverloadID("2")>] // OverloadID属性を付加
      static member Compare(x:float, y:float) = abs(x - y) < 0.00001
    end;;

type Comparator =
  class
    static member Compare : x:int * y:int -> bool
    static member Compare : x:float * y:float -> bool
  end

> type Comparator = // 同一名のメソッドにOverloadID属性を付加しないとエラーになる
    class
      static member Compare(x:int, y:int) = x = y
      static member Compare(x:float, y:float) = abs(x - y) < 0.00001
    end;;

      static member Compare(x:float, y:float) = abs(x - y) < 0.00001
  ------------------^^^^^^^

stdin(32,19): error FS0037: Two members called 'Compare' have the same argument counts and/or signatures.
To overload methods with identical argument counts use an 'OverloadID' attribute.

ここでは、静的メソッドCompareをもつComparatorクラスを定義しました。このクラスはCompareという名前のメソッドを2つもち、それぞれ引数の型が異なります。この場合、それぞれのメソッド定義の直前に[<OverloadID("属性値")>]という形でOverloadID属性を付加しなければなりません。この属性値には任意の文字列を与えることができますが、メソッド間で重複してはいけません。ちなみに、この例では静的メソッドを用いて説明しましたが、インスタンスメソッドでも同じ方法でオーバーロード定義を行うことができます。

Note

F#における属性の扱いについては後の章で詳しく説明するので、ここでは[<OverloadID("属性値")>]という形だけ覚えておいてください。

1.18. 練習問題

  1. 個人の情報を表現するPersonクラスを、次に述べるように定義してください。このクラスは、名前(name : string)、住所(address : string)、生年月日(birthday : DateTime)、性別(sex : sex_type)というprivateな不変フィールドをもち、それぞれに対応するNameAddressBirthDaySexというpublicな読み取り専用プロパティをもちます。それらのフィールドは、構築時にコンストラクタに渡される引数によって初期化されます。sex_typeは、FemaleおよびMaleというメンバをもつDiscriminated unionとして定義してください。

  2. 前問で定義したPersonクラスにAge() : intというpublicなメソッドを追加し、このメソッドが現在の日時でのその人の年齢を返すようにしてください。

  3. 前問で定義したPersonAgeメソッドがオプション引数としてdate : DateTimeを受け取るように書き換えてください。もしこの引数が省略されて呼び出された場合は、現在の日時における年齢を返すようにし、省略されずに渡された場合は、その渡された日時における年齢を返すようにしてください。

  4. 前問で定義したPersonクラスのインスタンスを生成して返す静的メソッドCreate(name : string, address : string, birthday : DateTime, sex : sex_type)を追加してください。また同時に、コンストラクタをprivateにすることで、外部から直接コンストラクタを呼び出せないようにしてください。

  5. 前問で定義した静的Createメソッドを名前つき引数を使って呼び出して、適当なPersonクラスのインスタンスを生成してください。

  6. 以下のC#によるクラス定義をF#で書き直してください。そして書き終わったら、クラス定義の見易さやコードの行数などを見比べてみてください。

    このプログラムは、年月日の日付を表す文字列を出力するクラスを定義しています。JapaneseDateStringは日本の日付表記である「年/月/日」という順番の文字列を生成するのに対し、AmericanDateStringはアメリカの日付表記である「月/日/年」という順番の文字列を生成します。どちらのクラスも共通するDateString抽象クラスを継承しており、そこで宣言されているGetDateString抽象メソッドを実装しています。このメソッドを呼び出すことで、それぞれの日付を表す文字列が生成されます。

    public abstract class DateString
    {
        private readonly int year;
        private readonly int month;
        private readonly int day;
    
        public DateString(int year, int month, int day)
        {
            this.year = year;
            this.month = month;
            this.day = day;
        }
    
        public int Year {
            get { return this.year; }
        }
    
        public int Month {
            get { return this.month; }
        }
    
        public int Day {
            get { return this.day; }
        }
    
        abstract public void GetDateString();
    }
    public class JapaneseDateString : DateString {
        JapaneseDateString(int year, int month, int day)
            : base(year, month, day)
        {
        }
        public override string GetDateString()
        {
            return System.String.Format("{0}/{1}/{2}", Year, Month, Day);
        }
    }
    public class AmericanDateString : DateString
    {
        AmericanDateString(int year, int month, int day)
            : base(year, month, day)
        {
        }
        public override string GetDateString()
        {
            return System.String.Format("{0}/{1}/{2}", Month, Day, Year);
        }
    }

2. インターフェイス

2.1. インターフェイスの定義方法

インターフェイスは抽象メンバのみをもつ特殊なクラス型です。また、インターフェイスはほかのインターフェイスを継承することもできます。定義方法の基本は以下の形になります。

type インターフェイス名
  interface
    [基底インターフェイス名]*
    [メンバ定義]*
  end

まず他のインターフェイスを継承する場合には、基底インターフェイス名にそのインターフェイスを書きます。そして、その後のメンバ定義に自身のメンバの定義を書きます。インターフェイスを実装するには、クラス定義の中で以下のように書きます。ここで、自己識別子には自己識別子、インターフェイスメンバには実装するメンバ、メンバ定義にはその実装を書きます。

interface インターフェイス名 with
  member 自己識別子.インターフェイスメンバ = メンバ定義

例としてICountableインターフェイスというものを定義して実装してみます。このインターフェイスは、カウンタをひとつ増やすCountメソッドと、現在のカウンタ値を表すCounterValueプロパティを定義しています。そして、このインターフェイスをCountクラスで実装します。

> type ICountable = // インターフェイスを定義
    interface
      abstract Count : unit -> unit
      abstract CounterValue : int
    end;;

type ICountable =
  interface
    abstract member Count : unit -> unit
    abstract member CounterValue : int
  end

> type Counter = // インターフェイスを実装
  class
    val mutable counter : int
    new() = { counter = 0 }
    interface ICountable with
      member this.Count() = this.counter <- this.counter + 1
      member this.CounterValue = this.counter
  end;;

type Counter =
  class
    val mutable counter: int
    interface ICountable
    new : unit -> Counter
  end

2.2. インターフェイスのメンバ呼び出し

C#の場合、あるインターフェイスを実装しているクラスのインスタンスから、直接そのインターフェイスのメンバにアクセスすることができます。たとえば、前小節で定義したICountableインターフェイスとそれを実装するCounterクラスの同等な定義をC#で以下のように書いたとします。

interface ICountable
{
    void Count();
    int CountValue { get; }
}
class Counter : ICountable
{
    private int counter = 0;
    public void Count() { counter++; }
    public int CountValue { get { return counter; } }
}

C#ではこのクラスを以下のように利用できます。

Counter c = new Counter();
c.Count();
Console.Out.WriteLine("Counter={0}", c.CountValue);

ここでは、まずCounterクラスのインスタンスを生成してcという変数に代入します。そして、c.Count()c.CountValueと書くことで、Counterが実装しているインターフェイスのメンバにアクセスしています。これはC#ではごく当たり前の動作です。

ところがF#では、クラスのインスタンスから、そのクラスが実装しているインターフェイスのメンバを直接呼び出すことができません。インターフェイスのメンバを呼び出すには、一度そのインターフェイスの型にアップキャストする必要があります。実際に例を見てみましょう。

> let counter = new Counter();; // Counterクラスのインスタンスを生成
val counter : Counter

> counter.CounterValue;; // ICountable.CounterValueプロパティへアクセス(アクセスできない)
  counter.CounterValue;;
  --------^^^^^^^^^^^^

stdin(4,9): error FS0039: The field, constructor or member 'CounterValue' is not defined.
> counter.Count();; // ICountable.Count()メソッドへアクセス(アクセスできない)
  counter.Count();;
  --------^^^^^

stdin(5,9): error FS0039: The field, constructor or member 'Count' is not defined.
> let c = counter :> ICountable;; // ICountableへアップキャスト
val c : ICountable

> c.CounterValue;; // ICountable.CounterValueプロパティへアクセス
val it : int = 0

> c.Count();; // ICountable.Count()メソッドへアクセス
val it : unit = ()

> c.CounterValue;; // ICountable.CounterValueプロパティへアクセス
val it : int = 1

> (counter :> ICountable).CounterValue;; // ICountable.CounterValueプロパティへアクセス
val it : int = 1

> (counter :> ICountable).Count();; // ICountable.Count()メソッドへアクセス
val it : unit = ()

> (counter :> ICountable).CounterValue;; // ICountable.CounterValueプロパティへアクセス
val it : int = 2

まず1番目の入力でCounterクラスのインスタンスを生成し、2番目と3番目の入力ではCounterクラスのインスタンスcounterを通じてインターフェイスのメンバにアクセスしようとして失敗しています。4番目の入力ではICountableにアップキャストしたものにcという名前を束縛し、5番目から7番目の入力ではそれを経由することでインターフェイスのメンバに正しくアクセスしています。8番目以降では、アップキャストとメンバへのアクセスをそれぞれ1行で行っています。

2.3. 練習問題

  1. 次の2つのクラスにあるメンバのうち、共通するメンバPushedHitTestIClickableインターフェイスとして抽出してください。さらに、この2つのクラスがそのインターフェイスを実装するように書き換えてください。

    type Rect =
      class
        val pos : float * float
        val size : float * float
        val mutable pushed : bool
        new (x,y,width,height) = { pos = (x,y); size = (width,height); pushed = false }
        member t.Position = t.pos
        member t.Size = t.size
        member t.Pushed
          with get() = t.pushed
          and set(st) = t.pushed <- st
        member t.HitTest x y =
          let (pos_x,pos_y) = t.pos
          let (size_x,size_y) = t.size
          (pos_x <= x) && (x <= pos_x + size_x) && (pos_y <= y) && (y <= pos_y + size_y)
      end
    
    type Circle =
      class
        val c : float * float
        val r : float
        val mutable pushed : bool
        new (x,y,radius) = { c = (x,y); r = radius; pushed = false }
        member t.Center = t.c
        member t.Radius = t.r
        member t.Pushed
          with get() = t.pushed
          and set(st) = t.pushed <- st
        member t.HitTest x y =
          let (cx,cy) = t.c
          (cx - x)**2.0 + (cy - y)**2.0 <= t.r**2.0    
      end
    

3. 構造体

3.1. 構造体の定義方法

.NET Frameworkの共通言語ランタイムにはさまざまな型が定義されていますが、それらは大きく分けて2つの種類に分類されます。それは、値型(value type)と参照型(reference type)です。ここまで扱ってきたクラスは参照型に分類され、これから扱う構造体は値型に分類されます。これらには以下のような特徴があります。

 値型 参照型 
確保される領域スタックヒープ
メンバへのアクセス
インスタンスのコピー
継承×

値型のインスタンスはスタック領域上に確保され、参照型のインスタンスはヒープ領域上に確保されます。メンバへのアクセスでは、値型は参照はがしを必要としないため非常に高速ですが、参照型は参照はがしを必要とするため少々遅くなります。インスタンスのコピーでは、値型はすべてのメンバーのコピーが発生するためメンバ数が増えると遅くなりますが、参照型は参照のみをコピーするだけなのでメンバの数に関係なく常に高速です。

一般的に、継承の必要のない小さいデータ型を定義するときは構造体を使い、そうでない場合は参照型を使います。構造体の定義のしかたはクラスとほとんど一緒です。ただし、構造体では継承を使うことができません。例として、2次元平面上の点を表現するPointクラスを定義してみます。

> type Point =
    struct
      val mutable private x : float
      val mutable private y : float
      member this.X with get() = this.x and set(x) = this.x <- x
      member this.Y with get() = this.y and set(y) = this.y <- y
    end;;

type Point =
  struct
    val mutable private x: float
    val mutable private y: float
    member X : float
    member Y : float
    member X : float with set
    member Y : float with set
  end

> let p = new Point();; // インスタンスを生成
val p : Point

> p.X;; // Xプロパティの値を取得
val it : float = 0.0

> p.X <- 3.0;; // Xプロパティの値を書き換え
  p.X <- 3.0;;
  ^^

stdin(16,1): error FS0191: A value must be local and mutable in order to mutate the contents of a value type, e.g. 'let mutable x = ...'.

> let mutable p = new Point();; // mutableなインスタンスを生成
val mutable p : Point

> p.X <- 3.0;; // Xプロパティの値を書き換え
val it : unit = ()

> p.X;; // Xプロパティの値を取得
val it : float = 3.0

構造体の場合、コンストラクタの定義を省略すると自動的にすべてのフィールドをゼロで初期化するコンストラクタが生成されます。もちろん、自分でコンストラクタを定義することができますが、.NET Frameworkの制限によりデフォルトコンストラクタは定義することができません。

もうひとつ注意しなければならないのが、mutableなフィールドをもつ構造体を使う場合です。mutableなフィールドをもつ構造体の場合、それを束縛する識別子もmutableになっていないと、読み取りはできても値を書き換えることができません。

3.2. 練習問題

  1. 上のPoint構造体の定義内のstruct/endclass/endに書き換えると、Point構造体がPointクラスになります。しかし、そのPointクラスのインスタンスを生成しようとすると、エラーが発生します。それはどのようなエラーでしょうか。また、エラーが出ないようにするにはどうすればよいでしょうか。実際にエラーが出ないようにそのPointクラスの定義を修正してください。

4. 列挙型

4.1. 列挙型の定義方法

列挙型を使うと、いくつかの同じ型の定数値にそれぞれ名前をつけ、ひとつの型としてまとめることができます。定数には以下の型を使用することができます。

sbyte byte int16 int32 uint64 uint16 uint32 uint64 char

列挙型の定義は以下のようにおこないます。

type 列挙型名 =
  [|] 識別子 = 定数値
  [| 識別子 = 定数値 ]*

一番最初の|は省略可能です。また自分で定義した列挙型を使うには、列挙型名.識別子というように記述します。以下に例を示します。

> type Color = // 1,2,5にRed,Black,Whiteという名前をつける
    | Red   = 1
    | Black = 2
    | White = 5;;

type Color =
  |  Red  =  1
  |  Black  =  2
  |  White  =  5

> Color.Black;; // 値を表示
val it : Color = Black

> type Color = // すべてのメンバが同じ型を持たなければならない
    | Red   = 1u
    | Black = 2u
    | White = 5;;

      | White = 5;;
  ------^^^^^^^^^^

stdin(22,7): error FS0001: This expression has type
  int
but is here used with type
  uint32

4.2. 練習問題

  1. 次の列挙型の定義がエラーになる原因は何でしょうか。

    type ColorName =
      | Red = "Red"
      | Black = "Black"
      | White = "White"
    
  2. discriminated unionは列挙型と似ており、どちらも同じように複数の値のうち同時に1つの値しかとらないような型を定義することができますが、両者にはどのような違いがあるかを答えてください。

5. F#特有の型とクラスの関係

5.1. F#特有の型の内部実装

以前紹介したように、F#にはタプル、リスト、レコード、discriminated unionといった特有の型があります。これらの型は、実際にはある特別なクラスとしてコンパイルされます。

具体的には、タプルとリストは内部的にSystem.Tupleクラス(.NET 4.0以降)とMicrosoft.FSharp.Collections.Listクラスとしてコンパイルされます。そしてレコードとdiscriminated unionは、コンパイラによって自動生成された、以下のメソッドとインターフェイスの実装もつクラスとしてコンパイルされます。

// 自動生成されるメソッド('aには自身の型名が入る)
Equals : 'a -> bool
CompareTo : 'a -> int
GetHashCode : int

// 自動生成されるインターフェイス実装
System.Collections.IStructuralEquatableインターフェイス
  Equals : obj * System.Collections.IEqualityComparer -> bool
  GetHashCode : System.Collections.IEqualityComparer -> int

System.Collections.IStructuralComparableインターフェイス
  CompareTo : obj *  System.Collections.IComparer -> int

System.IComparableインターフェイス
  CompareTo : obj -> int

IStructuralEquatableインターフェイスとIStructuralComparableインターフェイスは、ジェネリックのところで説明した構造的比較演算を実装するためのインターフェイスです。また、上に出てくるIEqualityComparerインターフェイスを受け取るメソッドは、比較演算やハッシュ関数のカスタマイズしたい場合に利用します。自分でIEqualityComparerインターフェイスを実装して、それを渡すことで独自の挙動をさせることができます。

Note

.NET 4.0からは、タプルを表現するSystem.Tupleクラスや、構造的比較演算を表現するSystem.Collections.IStructuralEquatableおよびSystem.Collections.IStructuralComparableインターフェイスがクラスライブラリに追加され、F#はこれらを使用するようになりました。

しかし、それ以前のバージョンの.NETアセンブリとしてコンパイルする場合は、それを利用することはできないので、F#はFSharp.Core.dllにある自前の実装であるMicrosoft.FSharp.Core.TupleクラスやMicrosoft.FSharp.Core.IStructuralEquatableインターフェイスおよびMicrosoft.FSharp.Core.IStructuralComparableインターフェイスをその代用として使用します。

さらに、レコードとdiscriminated unionも結局はひとつのクラスなので、独自のメソッドを定義することができます。

> type Point = // レコードにMoveメソッドを定義
  { mutable x : float
    mutable y : float }
  member this.Move(dx,dy) =
    this.x <- this.x + dx
    this.y <- this.y + dy;;

type Point =
  {mutable x: float;
   mutable y: float;}
  with
    member Move : dx:float * dy:float -> unit
  end

> let p = { x = 0.0; y = 0.0 };;

val p : Point

> p.Move(10.0,0.0);;
val it : unit = ()

> p;;
val it : Point = {x = 10.0;
                  y = 0.0;}
> type Maybe = // Discriminated unionにTextプロパティを定義
    | None
    | Just of string
    member this.Text =
      match this with
      | None -> "None"
      | Just x -> x;;

type Maybe =
  | None
  | Just of string
  with
    member Text : string
  end

> let v = None;;
val v : Maybe

> v.Text;;
val it : string = "None"

> let v = Just "Hello";;
val v : Maybe

> v.Text;;
val it : string = "Hello"

5.2. 練習問題

  1. 通常のクラス定義によって、上記のPointレコード型と同等の機能をもつクラス(すなわちxyの2つのプロパティを持ち、Moveという1つのメソッドをもつクラス)を定義してください。定義できたら、上記のレコード型を用いた定義と比較してみてください。

  2. 上記のMaybeというdiscriminated union型の値であるNoneを、IComparableインターフェイス型にアップキャストし、それにcompという識別子を束縛してください。そしてそのCompareメソッドに対して、さまざまなMaybe型の値を渡して呼び出してその戻り値を調べてみてください。

    次にcompを再びMaybeへダウンキャストしてmという識別子を束縛し、そのTextプロパティを呼び出してください。

  3. Maybeにメソッドto_int : unit -> intを追加してください。このメソッドは、自身がJustの値を持つときはその文字列をintに変換して返し、Noneの値を持つときは0を返すようにしてください。

6. クラス・インターフェイス・構造体の糖衣構文

6.1. 種推論

ここまでのクラス、インターフェイス、構造体の説明では、定義時にそれぞれclass/endinterface/endstruct/endで囲むと説明してきましたが、実は軽量構文ではこれらのキーワードを省略することができます。しかしちょっと考えればわかるとおり、そのキーワードが省略されてしまうと、それがクラスなのかインターフェイスなのか構造体なのかがわからなくなってしまいます。そこでF#では、これらのキーワードが省略された場合、以下の規則によってそれがクラス、インターフェイス、構造体のどれなのかを自動的に推論します。これを種推論(type kind inference)といいます。

  1. 定義した型が抽象メンバしかもたない場合、インターフェイスとして推論される。

  2. それ以外の場合、クラスとして推論される。

この規則からわかるように、ほとんどの場合はクラスとして推論されます。ただし、ClassAttributeInterfaceAttributeStructAttributeといった属性を明示的に指定した場合は、その指定した属性どおりに認識されます。これらの属性には、Class、Interface、Structという省略名も定義されており、そちらを使うこともできます。以下に例を示します。

> type MyPoint = // class/endを省略してクラスを定義
    val mutable private x : float
    val mutable private y : float
    member this.X with get() = this.x and set(x) = this.x <- x
    member this.Y with get() = this.y and set(y) = this.y <- y
    new(x_val,y_val) = { x = x_val; y = y_val };;

type MyPoint =
  class
    new : x_val:float * y_val:float -> MyPoint
    val mutable private x: float
    val mutable private y: float
    member X : float
    member Y : float
    member X : float with set
    member Y : float with set
  end

> [<StructAttribute>] // 明示的にStructAttribute属性を指定することで構造体として認識させる
  type MyPoint =
    val mutable private x : float
    val mutable private y : float
    member this.X with get() = this.x and set(x) = this.x <- x
    member this.Y with get() = this.y and set(y) = this.y <- y
    new(x_val,y_val) = { x = x_val; y = y_val };;

type MyPoint =
  struct
    new : x_val:float * y_val:float -> MyPoint
    val mutable private x: float
    val mutable private y: float
    member X : float
    member Y : float
    member X : float with set
    member Y : float with set
  end

> type MyPoint = // 抽象メンバのみをもつのでインターフェイスとして推論される
    abstract X : float
    abstract Y : float;;

type MyPoint =
  interface
    abstract member X : float
    abstract member Y : float
  end

> [<Interface>] // 明示的に属性を指定してもよい(InterfaceAttributeの省略名を使用)
  type MyPoint =
    abstract X : float
    abstract Y : float;;

type MyPoint =
  interface
    abstract member X : float
    abstract member Y : float
  end

Note

種推論の「種(しゅ)」とは、プログラミング言語論および型理論におけるひとつの用語です。

まず、「型」とは値の集合と考えることができます。たとえばsbyteという型は、-128yから127yまでの256個の値の集合になります。それに対して、「種」とは型の集合を意味します。種推論は、定義された型がクラス、インターフェイス、構造体のどのグループに属するか、すなわちどの「種」に属するかを推論するため、そのような名前がついています。

この章ではこれまで説明のために、クラス、インターフェイス、構造体などをいちいちclass/endなどで囲っていましたが、F#では種推論を積極的に利用して、それらのキーワードを省略することが推奨されています。本サイトでも、以降の説明では必要なとき以外はそれらを省略することにします。

6.2. 暗黙的クラス定義

ここまでは、クラス定義の基本として明示的クラス定義によるクラスの定義方法を説明してきました。明示的コンストラクタを使うと、フィールドやメンバを明確に定義することができますが、その構文は冗長で書くのが少々面倒です。そこで暗黙的クラス定義を使うと、よりシンプルにそれらを定義することができます。

ではこれから、暗黙的クラス定義によるクラスの定義方法を説明していきますが、ここでも明示的クラス定義のときの説明にならってRectクラスを徐々に作り上げながら説明をすることにします。ただし、明示的クラス定義とは構文がかなり異なるので、一旦明示的クラス定義の構文は忘れて、新たに説明を聞くつもりで聞いてください。

暗黙的クラス定義では、まず最初にフィールドとそれを初期化するコンストラクタを定義します。Rectクラスの場合は、上下左右の4点の座標を保持する4つのフィールドと、それを初期化するために4引数のコンストラクタをもつのでそれを定義します。以下にそのコードを示します。

type Rect(t,l,b,r) =
  // フィールド
  let top    : float = t
  let left   : float = l
  let bottom : float = b
  let right  : float = r

暗黙的クラス定義では、このようにクラス名の隣にいきなりコンストラクタの引数のリストを書きます。もしクラス名の横に引数のリストがなければ、それは明示的クラス定義となります。クラスの本体では、コンストラクタの引数を利用してlet束縛により識別子を定義しています。ここで束縛したこれらの識別子は、Rectクラス内だけで利用できるフィールドとなります。ちなみにこのクラス定義では、種推論を利用してclass/endを省略していますが、当然省略しないで書くこともできます。

では次に、コンストラクタに前処理や後処理などの処理を追加してみましょう。

type Rect(t,l,b,r) =
  do printfn "before" // 前処理
  // フィールド
  let top    : float = t
  let left   : float = l
  let bottom : float = b
  let right  : float = r
  do printfn "after" // 後処理

コンストラクタ内の処理は、このようにdoキーワードに続けて書きます。doキーワードの後にはunit型を返す任意の式を書くことができ、これらをまとめてdo束縛(do binding)といいます。コンストラクタ内では、let束縛とdo束縛を好きな順番で書くことができます。

次にプロパティを定義してみましょう。

type Rect(t,l,b,r) =
  do printfn "before" // 前処理
  // フィールド
  let top    : float = t
  let left   : float = l
  let bottom : float = b
  let right  : float = r
  do printfn "after" // 後処理
  // メソッド
  member this.Top    = top
  member this.Bottom = bottom
  member this.Left   = left
  member this.Right  = right
  member this.Width  = left - right
  member this.Height = bottom - top

暗黙的クラス定義におけるプロパティおよびメソッドの定義は、必ずdo束縛とlet束縛の直後に書きます。プロパティおよびメソッドの定義方法は明示的クラス定義と同じですが、暗黙的クラス定義内ではlet束縛で定義したフィールドは自己識別子をつけないでアクセスするという違いがあります。

次は前節で定義したFigure抽象クラスを継承して、Area()抽象メソッドを実装してみましょう。

type Rect(t,l,b,r) =
  inherit Figure() // 基底クラスの宣言と初期化
  do printfn "before" // 前処理
  // フィールド
  let top    : float = t
  let left   : float = l
  let bottom : float = b
  let right  : float = r
  do printfn "after" // 後処理
  // メソッド
  override this.Area() = this.Width * this.Height
  member this.Top    = top
  member this.Bottom = bottom
  member this.Left   = left
  member this.Right  = right
  member this.Width  = left - right
  member this.Height = bottom - top

継承は明示的クラス定義と同じように必ずクラス定義の先頭に書きます。明示的クラス定義では、ここでは単に基底クラス名だけを宣言しておいて、基底クラスのコンストラクタ呼び出しは、後で自身のコンストラクタ内で行っていました。しかし暗黙的コンストラクタでは、宣言と同時にコンストラクタの呼び出しも行います。一方、メソッドやプロパティの実装は明示的クラス定義の場合と同じです。

次にこのクラスに追加でもうひとつコンストラクタを定義してみましょう。

type Rect(t,l,b,r) =
  inherit Figure()
  do printfn "before" // 前処理
  // フィールド
  let top    : float = t
  let left   : float = l
  let bottom : float = b
  let right  : float = r
  do printfn "after" // 後処理
  // (追加の)コンストラクタ
  new () = new Rect(0.0,0.0,0.0,0.0)
  // メソッド
  override this.Area() = this.Width * this.Height
  member this.Top    = top
  member this.Bottom = bottom
  member this.Left   = left
  member this.Right  = right
  member this.Width  = left - right
  member this.Height = bottom - top

追加で定義したコンストラクタは、はじめに定義した4引数のコンストラクタを利用してクラスを初期化します。この追加したコンストラクタのほうは、明示的クラス定義の時と同じ方法で定義します。このように、暗黙的クラス定義においてコンストラクタを複数定義するときは、そのうちどれかひとつだけが代表となります。この代表となるコンストラクタをプライマリコンストラクタ(primary constructor)と呼びます。このクラス定義の場合は、4引数のコンストラクタがプライマリコンストラクタとなっています。

プライマリコンストラクタ以外のコンストラクタは、追加コンストラクタ(additional constructor)と呼ばれます。追加コンストラクタのほうは、明示的クラス定義におけるコンストラクタと同じ方法で定義します。どのコンストラクタをプライマリコンストラクタにするか追加コンストラクタにするかは自由です。したがって、このRectクラスで引数なしのコンストラクタをプライマリコンストラクタにして、4引数のコンストラクタを追加コンストラクタとすることも可能です。

これでひとまずRectクラスに新たなメンバを追加するのは終わりにしますが、このクラス定義はもう少し短くすることができます。まず、4つのフィールドにおける型注釈ですが、これは型推論が働くので省略できます。この型推論は、追加コンストラクタで4つの引数に0.0というfloat値を渡しているところから推論させることができます。次にプライマリコンストラクタの4つの引数の識別子ですが、これらはそのままフィールドとして使うことができます。このようにして、より短くしたクラス定義は以下のようになります。

type Rect(top,left,bottom,right) =
  inherit Figure() // 基底クラスの宣言と初期化
  do printfn "before" // 前処理
  // コンストラクタの引数の識別子をそのままフィールドとして使う
  do printfn "after" // 後処理
  // 追加コンストラクタ
  new () = new Rect(0.0,0.0,0.0,0.0)
  // メソッド
  override this.Area() = this.Width * this.Height
  member this.Top    = top
  member this.Bottom = bottom
  member this.Left   = left
  member this.Right  = right
  member this.Width  = left - right
  member this.Height = bottom - top

これでRectクラスの完成とします。いかがでしょうか。フィールドの定義がかなり短くなったことがわかると思います。その他のメンバは、明示的クラス定義と同じ方法で定義することができます。

最後にまとめとして、明示的クラス定義と暗黙的クラス定義の違いを比較してみましょう。まず、このRectクラスの明示的クラス定義による(ほぼ)同等の定義を示します。

type Rect =
  inherit Figure // 基底クラスの宣言
  // フィールド
  val private top    : float
  val private left   : float
  val private bottom : float
  val private right  : float
  // コンストラクタ
  new (t, l, b, r) =
    printfn "before"
    { inherit Figure(); // 基底クラスのコンストラクタ呼び出し
      top = t; left = l; bottom = b; right = r } then
    printfn "after"
  new () = new Rect(0.0, 0.0, 0.0, 0.0)
  // メソッド
  override this.Area() = this.Width * this.Height
  // プロパティ
  member this.Top    = this.top
  member this.Bottom = this.bottom
  member this.Left   = this.left
  member this.Right  = this.right
  member this.Width  = this.right - this.left
  member this.Height = this.bottom - this.top

この2つのクラスの定義方法の違いをまとめると、以下のようになります。

  • 暗黙的クラス定義では、明示的クラス定義におけるコンストラクタの1つを、プライマリコンストラクタとしてtypeキーワードの直後にもってくる。

  • 明示的クラス定義では、クラス定義の先頭で基底クラスの宣言をして、コンストラクタ内で基底クラスの初期化をするが、暗黙的クラス定義では、クラス定義の先頭において基底クラスの宣言と同時に初期化をする。

  • 明示的クラス定義では、privateなフィールドはvalキーワードで宣言し、コンストラクタ内で初期化するが、暗黙的クラス定義では、let束縛によってそれらを同時に行う。

  • 暗黙的クラス定義では、プライマリコンストラクタの引数を、クラス内で有効なprivateなフィールドとして扱うことができる。

  • 明示的クラス定義では、コンストラクタの前後に処理をはさみたいときは、前処理、後処理、条件分岐、let束縛の(複雑な)4つの構文を組み合わせて書くが、暗黙的クラス定義では、メンバ定義の直前の位置にdo束縛とlet束縛を任意の順番で書くことができる。

6.3. オブジェクト式

あるクラスのメンバをオーバーライドしたり、あるインターフェイスを実装するために、新しいクラスを定義することは少々面倒な作業です。しかし、オブジェクト式(object expression)を使うと特に新たなクラス定義を書かなくても簡単に派生クラスのインスタンスを生成することができます。

オブジェクト式には2つの構文があります。ひとつめは、既存のクラスを拡張したインスタンスを作る場合の構文で、その既存クラスの特定のメンバをオーバーライドしたり、インターフェイスを実装させたりを手軽にできます。もうひとつは、既存のインターフェイスの実装与えたインスタンスを手軽に作るための構文です。

// クラス型用の構文
{ new クラス型 [型引数] コンストラクタ引数 with
    メンバ定義
  [ インターフェイスの追加実装 ]* }

// インターフェイス型または非クラス型用の構文
{ new インターフェイス型or非クラス型 [型引数] with
    メンバ定義
  [ インターフェイスの追加実装 ]* }

どちらの構文でも、メンバ定義にはクラス型またはインターフェイス型or非クラス型のメンバのうち、オーバーライドや実装したいものがあれば、それらの定義を書きます。インターフェイスの追加実装には追加で実装するインターフェイスを書きます。

型引数には、その型がジェネリック型だった場合に型を渡します。また、クラス型用の構文では、コンストラクタを定義しなければならないので、コンストラクタ引数にその引数を書きます。

以下にいくつかの例を示します。

type IPrintable =
    abstract print : unit -> unit

let printer = // IPrintableの実装をしたクラスのインスタンス
  { new IPrintable with
      member this.print() = printf "I am Printer.\n" } // IPrintable.printの実装

let printer = // IPrintableの実装をして、Object.ToStringメソッドをオーバーライドしたクラスのインスタンス
  { new System.Object() with
      member this.ToString() = "Printer Object" // Object.ToStringメソッドのオーバーライド
    interface IPrintable with
      member this.print() = printf "I am Printer.\n" } // IPrintable.printの実装

6.4. 型拡張

型拡張(type extension)は、あるクラスに対して後からメンバを追加定義できる機能です。これは、C# 3.0から導入された拡張メソッドに相当するものです。型拡張には2つの種類が存在します。ひとつは内在的拡張(intrinsic extension)といい、もうひとつは外在的拡張(optional extension)といいますが、どちらの場合も構文的な違いはありません。ただし、外在的拡張の場合は拡張されるクラスは完全修飾名で表記する必要があります。

Note

ここでは適切な訳語が見当たらなかったために、intrinsic extensionに「内在的拡張」、optional extensionに「外在的拡張」という日本語訳を割り当てました。ただし、これはこのサイトにおける独特の訳語なので注意してください。

型拡張を行うには以下の構文を用います。

type 被拡張クラス名 with
  [メンバ定義]*
end

被拡張クラス名には拡張されるクラスを書き、メンバ定義に追加定義を書きます。軽量構文のときはendは省略可能です。

内在的拡張は、同一のコンパイル単位にあるクラスにメンバを追加するもので、コンパイル後は実際にそのクラスのメンバになります。

> type MyClass() = // クラスの定義
    member this.f() = printf "f()\n";;

type MyClass =
  class
    new : unit -> MyClass
    member f : unit -> unit
  end

> type MyClass with // MyClassにgというメソッドを追加定義
    member this.g() = printf "g()\n";;
> let m = new MyClass();;

val m : MyClass

> m.f();;
f()
val it : unit = ()
> m.g();; // 追加されたメソッドを呼び出す
g()
val it : unit = ()

discriminated unionに対してメソッドを定義する場合、内在的拡張を使うとすっきりと書くことができます(discriminated unionは本質的には単なるクラスであることを思い出してください)。

> type Figure = // discriminated unionの定義
    | Rectangle of float * float
    | Triangle of float * float
    | Circle of float

  type Figure with // 内在的拡張を用いてメンバを追加定義する
    member this.print() =
      match this with
      | Rectangle (w,h) -> printf "Rectangle (%f,%f)\n" w h
      | Triangle (w,h)  -> printf "Triangle (%f,%f)\n" w h
      | Circle r        -> printf "Circle (%f)\n" r;;

type Figure =
  | Rectangle of float * float
  | Triangle of float * float
  | Circle of float
  with
    member print : unit -> unit
  end

> let f = Rectangle (1.0,3.0);;
val f : Figure

> f.print();;
Rectangle (1.000000,3.000000)
val it : unit = ()

> let f = Circle 3.0;;
val f : Figure

> f.print();;
Circle (3.000000)
val it : unit = ()

外在的拡張は、異なるコンパイル単位にある既存のクラスを拡張するものです。内在的拡張は実際のクラスにメンバを追加しましたが、こちらは実際には静的メソッドとしてコンパイルされます。

外在的拡張を使うと、組み込みの文字列型であるSystem.Stringに対して、あたかも新たなメンバが追加されたように見えます。

> type System.String with // 外在的拡張によりprint_lengthメソッドを追加
    member this.print_length() = printf "length is %d" this.Length;;

> "hello".print_length();; // 追加されたメソッドを呼び出す
length is 5
val it : unit = ()

6.5. 練習問題

  1. 本章のクラスの説明の最後で出題した練習問題を、暗黙的クラス定義を用いて解いてください。

  2. 以下のDistanceクラスは、2次元座標の原点からの距離を表現するクラスです。このクラスに対して内在的拡張を用いて、原点からの直線距離を計算するLengthプロパティを追加定義してください。原点からの直線距離はで計算することができます。

    type Distance(x,y) =
      member v.X : float = x
      member v.Y : float = y
  3. Distanceクラスの適当なインスタンスをオブジェクト式により生成すると同時に、ToStringメソッドをオーバーライドして(3.5,5.7)のような文字列表現(2つの座標値を括弧で囲った文字列表現)を返すようにしてください。

  4. 前問と同様、オブジェクト式によってDistanceクラスのインスタンスを生成すると同時に、ToStringメソッドをオーバーライドしてください。ただし、今度はDistanceクラスが保持している座標の原点からの直線距離を返すようにしてください。

  5. 2問目と同様にして、今度は.NET FrameworkのクラスであるSystem.Drawing.PointFに対して、外在的拡張を用いてLengthプロパティを追加定義してください。(外在的拡張をする場合は、拡張されるクラスを完全修飾名で記述することに注意してください)

    Note

    F#インタプリタ上では、最初からSystem.Drawing.PointFを利用することはできません。そのクラスを利用するには、System.Drawingアセンブリへの参照を追加する必要があります。F#インタプリタから、このアセンブリへの参照を追加するには、以下のコマンドを使用します。

    > #r "System.Drawing.dll";;
    
    --> Referenced 'C:\Windows\Microsoft.NET\Framework\v4.0.20506\System.Drawing.dll'
inserted by FC2 system