M.Hiroi's Home Page

F# Programming

お気楽 F# プログラミング超入門

[ PrevPage | F# | NextPage ]

インターフェース

前回は継承について説明しました。今回は「インターフェース (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 を行うことが可能です。この Mix-in という方法を言語仕様に取り込んでいるのが Ruby です。Mix-in を図 1 に示します。


        図 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
  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 = ()

演算子のオーバーロード

F# は OCaml と違って演算子をオーバーロード (多重定義) することができます。

●オーバーロードの構文

オーバーロードの構文を以下に示します。

static member (op) (args1: type1, args2: type2) = 式

クラスやレコードなどで演算子を多重定義します。static は必ず付けてください。演算子 op が二項演算子の場合、引数は 2 つ必要になります。単項演算子の場合、op の前に ~ を付けて、引数は 1 つだけになります。

●レコードの場合

レコードは { ... } の後ろで member を定義することができます。

type record_name =
  {
    フィールド名 : 型式
    ...
  }
  member ...
  ...
  static member ...
  ...

簡単な例として、平面座標を表すレコード point を定義し、それをベクトルとみなして和 (+)、差 (-)、スカラー倍 (*)、内積 (*) を計算する演算子を多重定義してみましょう。プログラムは次のようになります。

リスト : レコード point (point.fsx)

type point =
  {
    x: float
    y: float
  }
  // ベクトルの和と差
  static member ( + ) (p1: point, p2: point) =
    {x = p1.x + p2.x; y = p1.y + p2.y }
  static member ( - ) (p1: point, p2: point) =
    {x = p1.x - p2.x; y = p1.y - p2.y }
  // スカラー倍
  static member ( * ) (k: float, p: point) =
    {x = k * p.x; y = k * p.y }
  static member ( * ) (p: point, k: float) =
    {x = k * p.x; y = k * p.y }
  // 内積
  static member ( * ) (p1: point, p2: point) =
    p1.x * p2.x + p1.y * p2.y

簡単な実行例を示します。

> #load "point.fsx";;
... 略 ...

> open Point;;
> let p1 = {x = 1.0; y = 1.0};;
val p1: point = { x = 1.0
                  y = 1.0 }

> let p2 = {x = 2.0; y = 3.0};;
val p2: point = { x = 2.0
                  y = 3.0 }

> p1 + p2;;
val it: point = { x = 3.0
                  y = 4.0 }

> p2 - p1;;
val it: point = { x = 1.0
                  y = 2.0 }

> p1 - p2;;
val it: point = { x = -1.0
                  y = -2.0 }

> 2.0 * p2;;
val it: point = { x = 4.0
                  y = 6.0 }

> p2 * 3.0;;
val it: point = { x = 6.0
                  y = 9.0 }

> p1 * p2;;
val it: float = 5.0

> p2 * p2;;
val it: float = 13.0

●クラスの場合

クラスで定義する場合は次のようになります。

リスト : クラス Point

type Point(x0: float, y0: float) =
  let x = x0
  let y = y0
  member this.X with get() = x
  member this.Y with get() = y
  // ベクトル演算
  static member ( + ) (p1: Point, p2: Point) =
    new Point(p1.X + p2.X, p1.Y + p2.Y)
  static member ( - ) (p1: Point, p2: Point) =
    new Point(p1.X - p2.X, p1.Y - p2.Y)
  // スカラー倍
  static member ( * ) (k: float, p: Point) =
    new Point(k * p.X, k * p.Y)
  static member ( * ) (p: Point, k: float) =
    new Point(p.X * k, p.Y * k)
  // 内積
  static member ( * ) (p1: Point, p2: Point) =
    p1.X * p2.X + p1.Y * p2.Y
> #load "point.fsx";;
... 略 ...

> open Point;;
> let p1 = new Point(1.0, 1.0);;
val p1: Point

> let p2 = new Point(2.0, 3.0);;
val p2: Point

> p1 + p2;;
val it: Point = FSI_0002.Point+Point {X = 3.0;
                                      Y = 4.0;}

> p1 - p2;;
val it: Point = FSI_0002.Point+Point {X = -1.0;
                                      Y = -2.0;}

> p2 - p1;;
val it: Point = FSI_0002.Point+Point {X = 1.0;
                                      Y = 2.0;}

> 2.0 * p2;;
val it: Point = FSI_0002.Point+Point {X = 4.0;
                                      Y = 6.0;}

> p2 * 3.0;;
val it: Point = FSI_0002.Point+Point {X = 6.0;
                                      Y = 9.0;}
> p1 * p2;;
val it: float = 5.0

> p2 * p2;;
val it: float = 13.0

●グローバルな演算子の定義

F# は let でグローバルな演算子を定義することができます。

let [inline] (op) (args1: type1) (args2: type2) = 式

演算子 op が二項演算子の場合、引数は 2 つ必要になります。オーバーロードとは違って、カリー化関数として定義することに注意してください。単項演算子の場合、op の前に ~ を付けて、引数は 1 つだけになります。

なお、この方法で既存の演算子を多重定義することはできません。let の定義が優先されるので、既存の演算子を再定義する、もしくは新しい演算子を定義するときに使用してください。

簡単な例を示しましょう。

> type point = {x: float; y: float};;
type point =
  {
    x: float
    y: float
  }

> let (+/) (p1: point) (p2: point) = {x = p1.x + p2.x; y = p1.y + p2.y};;
val (+/) : p1: point -> p2: point -> point

> let (-/) (p1: point) (p2: point) = {x = p1.x - p2.x; y = p1.y - p2.y};;
val (-/) : p1: point -> p2: point -> point

> let p1 = {x = 1.0; y = 1.0};;
val p1: point = { x = 1.0
                  y = 1.0 }

> let p2 = {x = 2.0; y = 3.0};;
val p2: point = { x = 2.0
                  y = 3.0 }

> p1 +/ p2;;
val it: point = { x = 3.0
                  y = 4.0 }

> p1 -/ p2;;
val it: point = { x = -1.0
                  y = -2.0 }

> p2 -/ p1;;
val it: point = { x = 1.0
                  y = 2.0 }

Copyright (C) 2022 Makoto Hiroi
All rights reserved.

[ PrevPage | F# | NextPage ]