今回は「自分型アノテーション」と「構造的部分型」について説明します。
自分型アノテーションは、オブジェクトで自分自身を表すキーワード this に別名を付ける機能のことで、基本コンストラクタの先頭で定義することができます。
名前 =>
定義には => を使います。左辺に名前を指定し、右辺には何も書きません。名前は何でもいいのですが、self を使うことが多いようです。
簡単な例を示しましょう。
scala> class Foo { | self => | val x = 1 | def display(): Unit = { println(this); println(self) } | } // defined class Foo scala> val a = new Foo val a: Foo = Foo@75b363c3 scala> a.display() rs$line$1$Foo@75b363c3 rs$line$1$Foo@75b363c3
このように、self は this と同じインスタンス (自分自身) を表していることがわかります。
自分型アノテーションでは、名前のあとに自分以外の別の「型」を指定することができます。
名前: 型1 [with 型2 ...] =>
自分型アノテーションで型 (トレイト) を指定すると、それを継承したことと同じ扱いになります。ただし、インスタンスを生成するときに、指定したトレイトを Mix-In する必要があります。
簡単な例を示しましょう。
scala> trait Foo { | def foo(): Unit = { println("foo") } | } | | class Bar { | self: Foo => | def bar(): Unit = { println("bar") } | } // defined trait Foo // defined class Bar trait Foo class Bar scala> val a = new Bar with Foo val a: Bar & Foo = $anon$1@7cc1f72c scala> a.foo() foo scala> a.bar() bar
クラス Bar の自分型アノテーションでトレイト Foo を指定します。Bar のインスタンスを new で生成するときは、with Foo で Foo を Mix-in してください。これで Foo のメソッド foo を呼び出すことができます。ただし、キーワード super を使って継承したトレイトのメソッドを呼び出すことはできません。ご注意ください。
型は Bar & Foo になりますが、型が Foo や Bar の変数にもインスタンスを格納することができます。
scala> val b: Foo = a val b: Foo = $anon$1@7cc1f72c scala> val c: Bar = a val c: Bar = $anon$1@7cc1f72c scala> b.bar() -- [E008] Not Found Error: -------------------------------- 1 |b.bar() |^^^^^ |value bar is not a member of Foo 1 error found scala> c.bar() bar scala> c.foo() -- [E008] Not Found Error: -------------------------------- 1 |c.foo() |^^^^^ |value foo is not a member of Bar 1 error found
インスタンスを Foo の変数に格納すると、Foo のメソッドしか呼び出すことができません。逆に、Bar の変数に格納すると、呼び出すことができるのは Bar のメソッドだけになります。
自分型アノテーションを使うと、実装 (機能) を切り替えることが簡単にできるようになります。次の例を見てください。
scala> trait Foo { | def foo(): Unit | } | | trait Foo1 extends Foo { | def foo(): Unit = { println("Foo1!") } | } | | trait Foo2 extends Foo { | def foo(): Unit = { println("Foo2!") } | } | | class Bar { | self: Foo => | def bar(): Unit = { println("bar!") } | } // defined trait Foo // defined trait Foo1 // defined trait Foo2 // defined class Bar
Foo は抽象トレイトで、抽象メソッド foo を持っています。トレイト Foo1, Foo2 は Foo を継承して、メソッド foo を具象化しています。クラス Bar では自分型アノテーションに型 Foo を指定することで、Foo1 と Foo2 のどちらでも Mix-in することができます。つまり、メソッド foo の機能をインスタンスの生成時に切り替えることができます。
それでは実行してみましょう。
scala> val a = new Bar with Foo1 val a: Bar & Foo1 = $anon$1@4c5c0306 scala> val b = new Bar with Foo2 val b: Bar & Foo2 = $anon$1@70c99e13 scala> a.foo() Foo1! scala> b.foo() Foo2! scala> a.bar() bar! scala> b.bar() bar!
変数 a のインスタンスは Foo1 を Mix-in しているので、foo を呼び出すと Foo1! と表示されます。変数 b は Foo2 を Mix-in しているので、foo は Foo2 と表示されます。このように、Mix-in するトレイトを変更するだけで、簡単に実装を切り替えることができます。
また、次のように仮想メソッド foo の実装を定義すれば、Foo を Mix-in することもできます。
scala> val c = new Bar with Foo { def foo(): Unit = { println("Foo!") } } val c: Bar & Foo = $anon$1@6be7a271 scala> c.foo() Foo! scala> c.bar() bar!
参考 URL『スケーラブルで関数型でオブジェクト指向なScala入門(11)』によると、『自分型アノテーションを使うことで、DI(依存性の注入)機能をソースファイルだけで実現できます。』 とのことです。
また、依存性の注入は参考 URL『依存性の注入 - Wikipedia』によると、『コンポーネント間の依存関係をプログラムのソースコードから排除し、外部の設定ファイルなどで注入できるようにするソフトウェアパターンである。英語の頭文字からDIと略される。』 とのことです。
M.Hiroi は勉強不足でまだよく理解できていないのですが、Foo という仕様 (抽象トレイト) を使ってプログラムを記述し、あとから実装 (Foo1 や Foo2) を Mix-in する方法は、クラス Bar の独立性を高め、他のクラスとの結合度を低くすることができるように思いました。興味のある方はいろいろ調べてみてください。
Scala の「無名クラス」は、class 文でクラスを定義しなくてもインスタンスを生成することができました。無名クラスは { ... } の中でフィールド変数やメソッドを定義します。Scala 2 の場合、new でインスタンスを生成するとき、{ ... } で定義したフィールド変数やメソッドの型がインスタンスの型に現れます。次の例を見てください。
scala> val a = new { val x = 1; def getX: Int = x } val a: AnyRef{val x: Int; def getX: Int} = $anon$1@53abfc07
ここで表示される型に注目してください。{ ... } の中にフィールド変数やメソッドの型が列挙されていますね。これがオブジェクトの型を表しています。本稿では、これを「シグネチャ (signature)」と呼ぶことにします。シグネチャはオブジェクトの仕様を記述するものと考えてください。
Scala 3 の場合、匿名クラスの型は Object で、シグネチャは自分で定義する必要があります。
scala> val a = new { val x = 1; def getX: Int = x } val a: Object = anon$1@2fa879ed scala> val a1: Object{val x: Int; def getX: Int} = new { val x = 1; def getX: Int = x } val a1: Object{val x: Int; def getX: Int} = anon$1@4fc3529
Scala の場合、変数や引数の型にシグネチャを指定することができます。また、type でシグネチャに名前を付けることもできます。
簡単な例を示しましょう。
scala> import scala.reflect.Selectable.reflectiveSelectable scala> def foo(a: {def getX: Int}): Int = a.getX def foo(a: Object{def getX: Int}): Int
Scala 3 の場合、scala.reflect.Selectable.reflectiveSelectable を import しないとエラーになります。ご注意ください。関数 foo の引数 a の型は {def getX: Int} です。メソッド getX を持つオブジェクトであれば、関数 foo の引数に渡すことができます。実際に、次に示す 3 つのオブジェクトを渡してみましょう。
(1) new {def getX: Int = 10} (2) new {def getX: Int = 10; def getY:Int = 20} (3) new {def getX: Int = 10; def getY:Int = 20; def getZ: Int = 30}
scala> foo(new {def getX: Int = 10}) val res1: Int = 10 scala> foo(new {def getX: Int = 10; def getY: Int = 20}) val res2: Int = 10 scala> foo(new {def getX: Int = 10; def getY: Int = 20; def getZ: Int = 30}) val res3: Int = 10
どのオブジェクトの型もメソッド getX があるので、関数 foo の引数として渡すことができます。Scala の場合、(2) と (3) は (1) の「部分型 (subtyping)」になります。データ型を集合とみなした場合、部分型はある型の部分集合を表していると考えることができます。
関数 foo の場合、引数 a の型は (1) の任意の部分型を表していると考えてください。したがって、(2), (3) のように (1) の部分型のオブジェクトであれば、関数 foo を呼び出すことができるわけです。
一般的なオブジェクト指向では、クラスを「継承」することによって部分型が発生します。継承を宣言することで部分型を生成する方法を「名前的部分型 (nomincal subtyping)」といいます。Scala の場合は継承に関係なく部分型を生成することができます。これを「構造的部分型 (structural subtyping)」といいます。
なお、Scala は静的な型チェックを行うので、getX を持たないオブジェクトを foo に渡すと、コンパイルでエラーとなります。
scala> foo(new {def getY: Int = 20}) -- [E007] Type Mismatch Error: -------------------------------- 1 |foo(new {def getY: Int = 20}) | ^ | Found: Object {...} | Required: Object{def getX: Int} | | longer explanation available when compiling with `-explain` 1 error found
Python や Ruby など動的な型付けを行うプログラミング言語では、同じインターフェースが備わっているオブジェクトは同じデータ型とみなす、という手法 (考え方) があります。これを「ダック・タイピング」といい、よく用いられる手法のようです。ご参考までに、Ruby のプログラムを示します。
リスト : ダック・タイピング (Ruby) class Foo def initialize @a = 10 end def get_a @a end def set_a(x) @a = x end end class Bar def initialize @a = 20 end def get_a @a end def set_a(x) @a = x end end def foo(x) print x.get_a end # # 実行 # foo(Foo.new) # 10 と表示する foo(Bar.new) # 20 と表示する
クラス Foo と Bar は異なるクラスで継承関係もありません。この場合、Ruby は異なるデータ型と判断します。しかし、どちらのクラスにもメソッド get_a が定義されているので、関数 foo にインスタンスを渡して get_a を呼び出すことができます。動的なプログラミング言語では、このような「ダック・タイピング」を簡単に行うことができます。
Scala は静的に強く型付けされる言語ですが、構造的部分型によりダック・タイピングのようなプログラミングスタイルも可能になっています。もちろん、コンパイル時に静的な型チェックが行われるので、エラーを検出することもできます。
Scala のプログラムは次のようになります。
scala> class Foo { | var a = 10 | def getA: Int = a | def setA(x: Int): Unit = a = x | } | | class Bar { | var a = 20 | def getA: Int = a | def setA(x: Int): Unit = a = x | } | | def foo(x: {def getA: Int}): Unit = { println(x.getA) } | // defined class Foo // defined class Bar def foo(x: Object{def getA: Int}): Unit scala> foo(new Foo) 10 scala> foo(new Bar) 20