前回は継承について説明しました。今回は「インターフェース (interface)」について説明します。F# のインターフェースはメソッドの仕様 (抽象メソッド) だけを定義した抽象クラスの一種ですが、「多重継承」できるところが他のクラスと異なるところです。
クラスを多重継承するとき複雑な問題を引き起こす場合がありますが、その問題点について把握しておくと、インターフェースの理解が深まると思います。まず最初に、多重継承の問題点について説明します。
一般的なオブジェクト指向の場合、継承によって引き継がれる性質は定義されたデータ (インスタンス変数など) やメソッドになります。これを「実装の継承」と呼びます。また、インスタンス変数を継承することを特別に「属性の継承」と呼ぶ場合があります。
多重継承を行う場合、異なる性質や機能を持つクラスを継承することがあります。たとえば、クラス Foo にはメソッド methodA() があり、クラス Bar にはメソッド methodB() があるとしましょう。この 2 つのメソッドはまったく異なる働きをするとします。ここで、methodA() はインスタンス変数 x を使っていて、methodB() も x を使っていると、多重継承で問題が発生します。
一般的な多重継承で、クラス Foo と Bar を継承してクラス Baz を作成した場合、クラス Baz のインスタンスに変数 x は一つしか存在しません。メソッド methodA() と methodB() は一つしかない x を使うことになります。この場合、どちらかのメソッドは正常に動作しないでしょう。これでは多重継承する意味がありませんね。
また、多重継承ではインスタンス変数だけではなく、メソッド名が衝突する場合もあります。このように、多重継承では名前の衝突が発生する危険性があるのです。それから、多重継承にはもう一つ問題点があります。それはクラスの階層構造が複雑になることです。
単一継承の場合、クラスの階層は木構造になりますが、多重継承ではグラフになります。木構造の場合、クラスの優先順位は簡単に決めることができますが、グラフになると優先順位を決めるためのアルゴリズムは複雑になり、それを理解するのは難しくなります。多重継承は強力な機能ですが、使うときには十分な注意が必要になるのです。
ちなみに C++ の場合、多重継承したクラスに同名のメソッドがある場合、どちらを呼び出すのか明確に指定しないとコンパイルでエラーとなります。また C++ はメンバ変数も継承されるため、変数名の衝突も発生します。この場合も、どちらの変数を使用するのか明確に指定しないとコンパイルエラーとなります。
このほかにも、多重継承ではいろいろな問題が発生するため、それを解決するためにC++ではいろいろな機能が用意されています。ところが、それらの機能が C++ をいっそう複雑な言語にしていると M.Hiroi には思えてなりません。C++ はコンパイラ型の言語で、なによりも効率を重視するため、複雑な言語仕様になるのは避けられないのかもしれません。
これらの問題を回避するため、属性を継承するスーパークラスは一つだけに限定して、あとのスーパークラスは実装だけを継承するという方法があります。これを Mix-in といいます。
具体的には、インスタンス変数を定義せずにメソッドだけを記述したクラスを用意します。属性の継承は単一継承になりますが、実装のみを記述したクラスはいくつ継承してかまいません。一つのクラスに複数の実装を混ぜることから Mix-in と呼ばれています。
なお、Mix-in は特別な機能ではなく、多重継承を使いこなすための方法論にすぎません。多重継承を扱うことができるプログラミング言語であれば Mix-in を行うことが可能です。この Mix-in という方法を言語仕様に取り込んでいるのが Ruby です。Mix-in を図 1 に示します。
A / B Mixin A / \ Mixin B \ / \ / C D 図 1 : Mix-in
クラス C はクラス B を継承していて、そこにクラス Mixin A が Mix-in されています。クラス D もクラス B を継承していますが、Mix-in されているクラスは Mixin B となります。
多重継承の問題点は Mix-in ですべて解決できるわけではありませんが、クラスの階層構造がすっきりとしてわかりやすくなることは間違いありません。Mix-in は多重継承を使いこなす優れた方法だと思います。
F# は単一継承なので Mix-in を使うことはできませんが、そのかわりにインターフェース (interface) という機能が用意されています。F# のインターフェースはメソッドの「仕様」だけを記述した抽象クラスのことで、クラスは複数のインターフェースを継承することができます。このように、メソッドの仕様だけを継承する方法を「仕様の継承」といいます。
属性を継承しないところは Mix-in と同じですが、メソッドの実装さえも継承しないところがインターフェースの特徴です。したがって、メソッドの実体はインターフェースを継承したクラスでプログラムしないといけません。また、インターフェースは抽象クラスの一種と考えられるので、型として使用することができます。つまり、クラスをインターフェースの型に変換することで、ポリモーフィズムを有効に活用することができるわけです。
それでは、具体的にインターフェースの使い方を説明しましょう。インターフェースは type で定義します。
type Interface_name = [interface] inherit Interface1 inherit ... ... abstract member 名前: 型式 abstract member ... ... [end]
type の後ろにインターフェース名 Interface_name を指定します。.NET ではインターフェース名の先頭に英大文字 I を付ける慣習があります。そして、interface ... end の中がインターフェースの本体になります。軽量構文を使うと interface と end を省略することができます。
インターフェースは他のインターフェースを継承することができます。クラスと同様にインターフェースの継承も inherit を使います。インターフェースは多重継承できるので、inherit を複数記述することで、複数のスーバーインターフェースを継承することができます。そして、本体の中でメソッドを宣言します。メソッドの実体を定義することはできません。インターフェースで宣言されたメソッドは暗黙のうちに public が付加されます。
クラスでインターフェースを継承する場合はキーワード interface を使います。
type className<...>(...) = [class] ... interface Interface_name1 with member this.名前1(...) = 式1 member this.名前2(...) = 式2 ... interface Interface_name2 with ... ... [end]
interface の後ろに使用するインターフェース名を指定し、with のあとにメソッドを実装します。
C# や Java など一般的なオブジェクト指向の場合、インターフェースのメソッドはクラスのメソッドと同様に呼び出すことができます。ところが F# の場合、インターフェースにアップキャストしないと、そのメソッドを呼び出すことはできません。ご注意くださいませ。
簡単な例を示しましょう。
> type Ifoo = - abstract member message: unit -> unit;; type Ifoo = abstract message: unit -> unit > type Foo() = - interface Ifoo with - member this.message () = printfn "Ifoo message";; type Foo = interface Ifoo new: unit -> Foo > let a = Foo();; val a: Foo > a.message();; => エラー > (a :> Ifoo).message();; Ifoo message val it: unit = ()
同じメソッド名を持つインターフェースを多重継承することもできます。
> type Ibar = - abstract member message: unit -> unit;; type Ibar = abstract message: unit -> unit > type Bar() = - interface Ifoo with - member this.message () = printfn "Ifoo message" - interface Ibar with - member this.message () = printfn "Ibar message";; type Bar = interface Ibar interface Ifoo new: unit -> Bar > let b = Bar();; val b: Bar > (b :> Ifoo).message();; Ifoo message val it: unit = () > (b :> Ibar).message();; Ibar message val it: unit = ()
このように、アップキャストしたほうのメソッドが呼び出されます。
もう一つ簡単な例として、前回作成した図形のプログラムをインターフェースを使って書き直してみましょう。次のリストを見てください。
リスト : インターフェースの使い方 (figure1.fsx) let pi = 3.14159265 // 図形のインターフェース type IFigure = abstract member kind_of: unit -> string abstract member area: unit -> float abstract member print: unit -> unit // 三角形 type Triangle(altitude: float, base_: float) = member this.getAltitude () = altitude member this.getBase () = base_ interface IFigure with member this.kind_of () = "Triangle" member this.area () = altitude * base_ / 2.0 member this.print () = printfn "%s: area = %f" ((this :> IFigure).kind_of()) ((this :> IFigure).area()) // 四角形 type Rectangle(width: float, height: float) = member this.getWidth () = width member this.getFeight () = height interface IFigure with member this.kind_of () = "Rectangle" member this.area () = width * height member this.print () = printfn "%s: area = %f" ((this :> IFigure).kind_of()) ((this :> IFigure).area()) // 円 type Circle(radius: float) = member this.getRadius () = radius interface IFigure with member this.kind_of () = "Circle" member this.area () = radius * radius * pi member this.print () = printfn "%s: area = %f" ((this :> IFigure).kind_of()) ((this :> IFigure).area()) // 合計値を求める let sum_of_figure xs = List.fold (fun a (x: IFigure) -> a + x.area()) 0.0 xs
最初に図形を操作するインターフェース IFigure を定義します。この中でメソッド kindOf(), area(), print() を宣言します。次に図形を表すクラス Triangle, Rectangle, Circle を定義します。interface を使って IFigure を継承し、そのメソッドを実装します。IFigure のメソッドを呼び出すとき、this を IFigure へアップキャストすることをお忘れなく。
なお、IFigure のメソッドがすべて実装されていないと、そのクラスは抽象クラスとみなされるため、コンパイルでエラーになります。ご注意くださいませ。
面積の合計を求める関数 sum_of_figure は引数に IFigure 型のリストを受け取ります。そして、IFigure に定義されているメソッドを使ってインスタンスを操作することができます。sum_of_figure はメソッド area() を呼び出していますが、ポリモーフィズムの働きにより各クラスのメソッド area() が呼び出されるので図形の面積を正しく求めることができます。
それでは実際に試してみましょう。
> #load "figure1.fsx";; [読み込み中 /home/mhiroi/fsharp/figure1.fsx] namespace FSI_0013.Figure1 val pi: float type IFigure = abstract area: unit -> float abstract kind_of: unit -> string abstract print: unit -> unit type Triangle = interface IFigure new: altitude: float * base_: float -> Triangle member getAltitude: unit -> float member getBase: unit -> float type Rectangle = interface IFigure new: width: float * height: float -> Rectangle member getFeight: unit -> float member getWidth: unit -> float type Circle = interface IFigure new: radius: float -> Circle member getRadius: unit -> float val sum_of_figure: xs: IFigure list -> float > open Figure1;; > let a = new Triangle(2.0, 2.0);; val a: Triangle > let b = new Rectangle(2.0, 2.0);; val b: Rectangle > let c = new Circle(2.0);; val c: Circle > sum_of_figure [a; b; c];; val it: float = 18.5663706
インターフェースはジェネリックを使って定義することもできます。
type Interface_name<型変数, ...> = ...
type class_name<型変数, ...> (...) = interface Interfase_name<型変数, ...> with ... interface Interfase_name<型, ...> with ...
簡単な例を示しましょう。
> type IPair<'a, 'b> = - abstract member fst: unit -> 'a - abstract member snd: unit -> 'b;; type IPair<'a,'b> = abstract fst: unit -> 'a abstract snd: unit -> 'b > type Foo() = - interface IPair<int, float> with - member this.fst () = 1 - member this.snd () = 2.5;; type Foo = interface IPair<int,float> new: unit -> Foo > let a = new Foo() :> IPair<int, float>;; val a: IPair<int,float> > a.fst();; val it: int = 1 > a.snd();; val it: float = 2.5 > type Bar<'a, 'b>(a: 'a, b: 'b) = - interface IPair<'a, 'b> with - member this.fst () = a - member this.snd () = b;; type Bar<'a,'b> = interface IPair<'a,'b> new: a: 'a * b: 'b -> Bar<'a,'b> > let b = new Bar<int, float>(100, 123.25) :> IPair<int, float>;; val b: IPair<int,float> > b.fst();; val it: int = 100 > b.snd();; val it: float = 123.25
インターフェース IPair は先頭要素と二番目の要素を取り出すメソッド fst, snd を宣言しています。先頭要素の型を 'a で、二番目の要素の型を 'b で表しています。IPair を実装したクラスは fst, snd を使って値を取り出すことができます。
クラス Foo は IPair<int,float> を実装しているので、fst を呼び出すと 1 を、snd を呼び出すと 2.5 を返すことなります。クラス Bar の場合、fst は引数 a を、snd は引数 b を返しています。Bar はジェネリッククラスなので、型変数 'a, 'b を使って IPair のメソッドを定義しています。
オブジェクト式は Java の無名クラスとよく似ている機能です。オブジェクト式は、クラスまたはインターフェースからインスタンスを直接生成する方法です。生成されたインスタンスが属するクラスは、F# が自動的に作成する匿名のオブジェクト型になります。オブジェクト式の構文を以下に示します。
1. クラスからの生成 { new class_name<...>(...) with // メソッドの定義 member ... ... interface Interface_name<...> with // インターフェースの実装 member ... ... } 2. インターフェースからの生成 { new Interface_name <...> with // インターフェースの実装 member ... ... }
どちらの方法でも、クラス class_name (またはインターフェース Interface_name) を継承した匿名のオブジェクト型のインスタンスが生成されます。関数やメソッドの中で一時的にクラスを定義したいとき、オブジェクト式はラムダ式のように名前を付けなくてもよいので便利です。
なお、1 の方法でメソッドを定義できるのは、抽象メソッドを実装すること、またはその実装をオーバーライドすることだけです。新しいメソッドを定義することはできません。新しいメソッドを追加したい場合はインターフェースを継承してください。
簡単な例を示しましょう。
> let c = { new IPair<int, float> with - member this.fst() = 1000 - member this.snd() = 1.234 - };; val c: IPair<int,float> > c.fst();; val it: int = 1000 > c.snd();; val it: float = 1.234 > type Baz() = - abstract message: unit -> unit - default this.message () = printfn "hello, Baz!!";; type Baz = new: unit -> Baz override message: unit -> unit + 1 オーバーロード > let d = new Baz();; val d: Baz > d.message();; hello, Baz!! val it: unit = () > let e = { - new Baz() with - member this.message() = printfn "hello, object expression" - };; val e: Baz > e.message();; hello, object expression val it: unit = ()
最初の例はインターフェース IPair からインスタンス (オブジェクト) を生成しています。次の例では、抽象メソッド message を持つクラス Baz を定義します。F# はキーワード default を使って抽象メソッドの「デフォルト実装」を定義することができます。抽象メソッド message の実装が用意されているので、Baz は new でインスタンスを生成することができます。オブジェクト式では message をオーバーライドしています。
もちろん、抽象クラスでもオブジェクト式でインスタンスを生成することができます。
> [<AbstractClass>] - type Oops() = - abstract message: unit -> unit;; type Oops = new: unit -> Oops abstract message: unit -> unit > let f = { new Oops() with - member this.message () = printfn "hello, Oops!!" - };; val f: Oops > f.message();; hello, Oops!! val it: unit = ()