F#は関数型言語ですが、オブジェクト指向プログラミングや命令型プログラミングもサポートしています。そもそも、.NET Frameworkはオブジェクト指向の上に成り立つフレームワークであるため、その上で動作する言語も本格的なオブジェクト指向の機能を利用することができます。さらに、F#はオブジェクト指向以外にも複数のプログラミングパラダイムを利用できるため、言語開発設計者であるDon Symeは単なる関数型言語ではなくマルチパラダイム言語と呼んでいます[ref]。
前章で紹介した命令型プログラミングは、関数型プログラミングとは背反するところが大きく、両方を効果的に組み合わせて使うことは困難でした。ところが、オブジェクト指向プログラミングというプログラミングパラダイムは、関数型言語とは背反するものではなく、両方を組み合わせて両方の恩恵を同時に享受することができます。
現在では、オブジェクト指向プログラミングは非常にメジャーなプログラミングパラダイムとなっています。実際、ビジネスの分野で幅広く使われており、それを学ぶための情報源も解説書・セミナー・授業・インターネットなど莫大な数を誇ります。その情報源の大半はJava、C#、C++といった命令型のオブジェクト指向言語を使用しているため、オブジェクト指向は命令型プログラミングの上に成り立つものという強い印象がありますが、関数型プログラミングの上には成り立たないというわけではありません。ここでは、F#におけるオブジェクト指向の機能を紹介するとともに、関数型プログラミングとの融合についても説明していきます。
まずは、F#におけるクラス定義の基本について説明していきます。ここで説明することは、基本的にC#でもおなじみの概念ばかりなので、あまりひっかからずに読めると思います。ですので、ここでは概念よりも、C#とF#の文法の違いを意識しながら読み進めていってください。ただし、ここでもベースとなる考え方は「不変」であることに注意してください。
F#によるクラスの定義方法には大きく分けて2つの種類があります。ひとつめは「明示的クラス定義」という定義方法で、もうひとつは「暗黙的クラス定義」という定義方法です。暗黙的クラス定義は明示的クラス定義の構文をもっと簡潔にしたものですが、基本的にはどちらを使っても全く同じクラスを定義することができます。
F#では暗黙的クラス定義をよく使いますが、その構文にはかなりわかりにくいところがあり、いきなりそれを説明しても混乱をきたすと思います(実際、私はかなり混乱しました)。そこで本サイトでは、クラス定義の基本としてまず明示的クラス定義を使って全体的な説明をし、その後に簡易的な記法として暗黙的クラス定義を説明していくことにします。
この節では、クラスの例として矩形を表現するRect
クラスを作りながらクラス定義の基本を紹介していきます。まず、以下の定義はフィールドもメソッドももたない空のRect
クラスの定義です。メソッドやフィールドは、class
とend
に囲まれた部分に追加していきます。
> type Rect =
class
end;;
type Rect =
class
end
まずは矩形を表現するために、矩形の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
型のtop
、left
、bottom
、right
というフィールドを宣言しています。この方法で定義されたフィールドを、明示的フィールド(explicit
field)と呼びます。
次に矩形の面積を計算する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#では自己識別子名で修飾しないとコンパイルエラーとなります。
次はプロパティを追加してみましょう。ここでは、4つの点を表すTop
、Bottom
、Left
、Right
と、幅と高さを表すWidth
とHeight
を追加します。
> 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
ここまで紹介したフィールド、メソッド、プロパティにはprivate
やpublic
といったアクセス制御(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
を指定することもできます。
ここで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())
ではこのクラスのインスタンスを生成して、実際に定義したメソッドやフィールドを呼び出してみましょう。まず、クラスのインスタンスを生成するには、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
静的メソッド、静的プロパティ、静的フィールドといった静的メンバを宣言/定義するには、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
属性にはtrue
かfalse
のどちらかを指定することができます。true
にするとその値は必ず0で初期化され、false
にするとその値は初期化されません。
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
少し前の小節ではプロパティのを説明をしましたが、そこで紹介したものはすべて不変フィールドに対するもので、読み取り専用のプロパティでした。読み取り専用のプロパティは、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
このように、get
とset
を両方もつときは「with
~ and
~
」の中にそれらを定義します。また、set
アクセサのみをもつプロパティを定義することもできます。
インデックス付きプロパティ(indexed
property)は通常のプロパティと同じように定義しますが、get
アクセサの引数がunit
型ではなくインデックス番号を表す引数になり、set
アクセサではインデックス番号を表す引数がひとつ増えます。
以下にインデックス付きプロパティを使ったクラスの定義を示します。このVector3D
クラスは3つの値x
、y
、z
というをもつ(すなわち3次元のベクトルを表す)クラスです。このクラスでは、x
、y
、z
のそれぞれの値にアクセスするために、インデックス付きプロパティを使っています。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
クラスを継承する場合は、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
アップキャスト(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
アップキャストとダウンキャストは、それぞれ:>
と:?>
という演算子によっておこないます。また、かわりにupcast
とdowncast
キーワードを使うこともできます。これらの演算子によって型変換をおこなう式を、静的型変換式(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
抽象メンバとは名前(メソッド名、プロパティ名)と型だけが宣言され、その本体が定義されていないメンバです。そのようなメンバをもつクラスを抽象クラスといいます。抽象メンバは、抽象クラスにおいて名前と型だけを宣言しておき、その派生クラスで関数の本体の定義をします。抽象メンバの宣言は、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を返すような定義をしました。
C#には仮想メンバ(virtual member)というものがありますが、F#には直接それに相当する言葉はありません。その代わりF#では、抽象メンバ宣言+デフォルト実装という組み合わせでそれと同じものを表します。
実は現在のF#の言語仕様(F#
1.9.6)では、default
とoverride
の実装は中途半端で、コンパイル時にきちんとチェックされていません。現在のコンパイラでは、実はdefault
とoverride
は単なる別名として定義されているだけです。したがって、default
と書いたにもかかわらず、F#
Interactiveがoverride
と画面に出力する場合がありますが、あまり気にしないでください。将来的にはきちんと実装するそうです。
通常、メソッドを呼び出すときの実引数は、定義された仮引数の順番どおりに並べる必要があります。しかし、名前つき引数(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
クラスはウィンドウの基本となるクラスで、非常によく使われますが、このクラスはデフォルトコンストラクタしかもちません。つまり、引数なしのコンストラクタで一度インスタンスを生成して、そのあとに改めて各種プロパティをセットしなければなりません。このようなときに非常に役に立ちます。
オプション引数(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
型の値を受け取り、それが値を持っていた場合はその値を返し、持っていなければ空文字列を返します。そして、この関数を用いてコンストラクタを定義しています。
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属性を付加しなければなりません。この属性値
")>]属性値
には任意の文字列を与えることができますが、メソッド間で重複してはいけません。ちなみに、この例では静的メソッドを用いて説明しましたが、インスタンスメソッドでも同じ方法でオーバーロード定義を行うことができます。
F#における属性の扱いについては後の章で詳しく説明するので、ここでは[<OverloadID("
という形だけ覚えておいてください。属性値
")>]
個人の情報を表現するPerson
クラスを、次に述べるように定義してください。このクラスは、名前(name
: string
)、住所(address :
string
)、生年月日(birthday : DateTime
)、性別(sex :
sex_type
)というprivateな不変フィールドをもち、それぞれに対応するName
、Address
、BirthDay
、Sex
というpublicな読み取り専用プロパティをもちます。それらのフィールドは、構築時にコンストラクタに渡される引数によって初期化されます。sex_type
は、Female
およびMale
というメンバをもつDiscriminated
unionとして定義してください。
前問で定義したPersonクラスにAge() :
int
というpublicなメソッドを追加し、このメソッドが現在の日時でのその人の年齢を返すようにしてください。
前問で定義した
PersonAge
メソッドがオプション引数としてdate
:
DateTime
を受け取るように書き換えてください。もしこの引数が省略されて呼び出された場合は、現在の日時における年齢を返すようにし、省略されずに渡された場合は、その渡された日時における年齢を返すようにしてください。
前問で定義した
Person
クラスのインスタンスを生成して返す静的メソッドCreate(name
: string, address : string, birthday : DateTime, sex :
sex_type)
を追加してください。また同時に、コンストラクタをprivateにすることで、外部から直接コンストラクタを呼び出せないようにしてください。
前問で定義した静的Create
メソッドを名前つき引数を使って呼び出して、適当なPerson
クラスのインスタンスを生成してください。
以下の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); } }
インターフェイスは抽象メンバのみをもつ特殊なクラス型です。また、インターフェイスはほかのインターフェイスを継承することもできます。定義方法の基本は以下の形になります。
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
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つのクラスにあるメンバのうち、共通するメンバPushed
とHitTest
をIClickable
インターフェイスとして抽出してください。さらに、この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
.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
になっていないと、読み取りはできても値を書き換えることができません。
列挙型を使うと、いくつかの同じ型の定数値にそれぞれ名前をつけ、ひとつの型としてまとめることができます。定数には以下の型を使用することができます。
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
以前紹介したように、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
インターフェイスを実装して、それを渡すことで独自の挙動をさせることができます。
.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"
通常のクラス定義によって、上記のPoint
レコード型と同等の機能をもつクラス(すなわちx
とy
の2つのプロパティを持ち、Move
という1つのメソッドをもつクラス)を定義してください。定義できたら、上記のレコード型を用いた定義と比較してみてください。
上記のMaybe
というdiscriminated
union型の値であるNone
を、IComparable
インターフェイス型にアップキャストし、それにcomp
という識別子を束縛してください。そしてそのCompare
メソッドに対して、さまざまなMaybe
型の値を渡して呼び出してその戻り値を調べてみてください。
次にcomp
を再びMaybe
へダウンキャストしてm
という識別子を束縛し、そのText
プロパティを呼び出してください。
Maybe
にメソッドto_int : unit ->
int
を追加してください。このメソッドは、自身がJust
の値を持つときはその文字列をint
に変換して返し、None
の値を持つときは0
を返すようにしてください。
ここまでのクラス、インターフェイス、構造体の説明では、定義時にそれぞれclass
/end
、interface
/end
、struct
/end
で囲むと説明してきましたが、実は軽量構文ではこれらのキーワードを省略することができます。しかしちょっと考えればわかるとおり、そのキーワードが省略されてしまうと、それがクラスなのかインターフェイスなのか構造体なのかがわからなくなってしまいます。そこでF#では、これらのキーワードが省略された場合、以下の規則によってそれがクラス、インターフェイス、構造体のどれなのかを自動的に推論します。これを種推論(type
kind inference)といいます。
定義した型が抽象メンバしかもたない場合、インターフェイスとして推論される。
それ以外の場合、クラスとして推論される。
この規則からわかるように、ほとんどの場合はクラスとして推論されます。ただし、ClassAttribute
、
InterfaceAttribute
、StructAttribute
といった属性を明示的に指定した場合は、その指定した属性どおりに認識されます。これらの属性には、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
種推論の「種(しゅ)」とは、プログラミング言語論および型理論におけるひとつの用語です。
まず、「型」とは値の集合と考えることができます。たとえばsbyte
という型は、-128y
から127y
までの256個の値の集合になります。それに対して、「種」とは型の集合を意味します。種推論は、定義された型がクラス、インターフェイス、構造体のどのグループに属するか、すなわちどの「種」に属するかを推論するため、そのような名前がついています。
この章ではこれまで説明のために、クラス、インターフェイス、構造体などをいちいちclass
/end
などで囲っていましたが、F#では種推論を積極的に利用して、それらのキーワードを省略することが推奨されています。本サイトでも、以降の説明では必要なとき以外はそれらを省略することにします。
ここまでは、クラス定義の基本として明示的クラス定義によるクラスの定義方法を説明してきました。明示的コンストラクタを使うと、フィールドやメンバを明確に定義することができますが、その構文は冗長で書くのが少々面倒です。そこで暗黙的クラス定義を使うと、よりシンプルにそれらを定義することができます。
ではこれから、暗黙的クラス定義によるクラスの定義方法を説明していきますが、ここでも明示的クラス定義のときの説明にならって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
束縛を任意の順番で書くことができる。
あるクラスのメンバをオーバーライドしたり、あるインターフェイスを実装するために、新しいクラスを定義することは少々面倒な作業です。しかし、オブジェクト式(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の実装
型拡張(type extension)は、あるクラスに対して後からメンバを追加定義できる機能です。これは、C# 3.0から導入された拡張メソッドに相当するものです。型拡張には2つの種類が存在します。ひとつは内在的拡張(intrinsic extension)といい、もうひとつは外在的拡張(optional extension)といいますが、どちらの場合も構文的な違いはありません。ただし、外在的拡張の場合は拡張されるクラスは完全修飾名で表記する必要があります。
ここでは適切な訳語が見当たらなかったために、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 = ()
本章のクラスの説明の最後で出題した練習問題を、暗黙的クラス定義を用いて解いてください。
以下のDistance
クラスは、2次元座標の原点からの距離を表現するクラスです。このクラスに対して内在的拡張を用いて、原点からの直線距離を計算するLength
プロパティを追加定義してください。原点からの直線距離はで計算することができます。
type Distance(x,y) = member v.X : float = x member v.Y : float = y
Distance
クラスの適当なインスタンスをオブジェクト式により生成すると同時に、ToString
メソッドをオーバーライドして(3.5,5.7)
のような文字列表現(2つの座標値を括弧で囲った文字列表現)を返すようにしてください。
前問と同様、オブジェクト式によってDistance
クラスのインスタンスを生成すると同時に、ToStringメソッドをオーバーライドしてください。ただし、今度はDistance
クラスが保持している座標の原点からの直線距離を返すようにしてください。
2問目と同様にして、今度は.NET
FrameworkのクラスであるSystem.Drawing.PointF
に対して、外在的拡張を用いてLength
プロパティを追加定義してください。(外在的拡張をする場合は、拡張されるクラスを完全修飾名で記述することに注意してください)
F#インタプリタ上では、最初からSystem.Drawing.PointF
を利用することはできません。そのクラスを利用するには、System.Drawing
アセンブリへの参照を追加する必要があります。F#インタプリタから、このアセンブリへの参照を追加するには、以下のコマンドを使用します。
> #r "System.Drawing.dll";; --> Referenced 'C:\Windows\Microsoft.NET\Framework\v4.0.20506\System.Drawing.dll'