前回のChiselの記事ではChisel-bootcampのModule3.4の練習問題でニューラルネットワークのニューロン回路を実装するという問題に取り組んだ。
今回はModule3.5に入っていく。ここで扱うトピックはScalaの関数型言語とは別に存在するもうひとつの大きな特徴であるオブジェクト指向の仕組みについてだ。
オブジェクト指向プログラミング
モチベーション
これまで通りモチベーションから確認していく。
ScalaとChiselはオブジェクト指向言語であり、これはコードをオブジェクトに区分化しても良いことを意味する。Javaの上に構築されているScalaは多くのJavaのオブジェクト指向の特徴を継承している。しかしながら、これから見ていくようにそこにはいくつかの違いがある。ChiselのハードウェアモジュールはVerilogのモジュールと似ており、ひとつのまたは複数のモジュールをインスタンスし配線することが可能だ。
オブジェクト指向プログラミング
これまでにもScalaのクラスは使用してきているが、それ以外にもScalaのオブジェクト指向の特徴には以下のようなものがある。
- 抽象クラス
- トレイト
- オブジェクト
- コンパニオンクラス
- ケース・クラス
ということでこのモジュールでは上記の5つについて、改めて例を使って見ていくようだ。
抽象クラス
抽象クラスは他のオブジェクト指向言語と同様の概念で、クラスの中に未実装の値やメソッドを定義することが出来る。 これらの未実装の値/メソッドは抽象クラスを継承したサブクラス側に実装を強制することが出来る。また未実装の値/メソッドが存在する関係もあり抽象クラス自体はインスタンスできない。 どのようなオブジェクトであってもひとつの抽象クラスのみを継承できる。
例題:抽象クラス
早速抽象クラスの例を見てみよう。抽象クラスはclass
の定義時にキーワードabstract
を付与することにより定義できる。
以下の例のMyAbstractClass
を見るとわかる通りで、値やメソッドを宣言したのみであっても文法エラーにはならない。
一方でMyAbstractClass
を継承したConcreteClass
では抽象クラスで宣言のみを行ったmyFunction
とmyValue
の実装が行われている。
abstract class MyAbstractClass { def myFunction(i: Int): Int val myValue: String } class ConcreteClass extends MyAbstractClass { def myFunction(i: Int): Int = i + 1 val myValue = "Hello World!" } // 以下のコメント文をテストのために有効にしてみよう // val abstractClass = new MyAbstractClass() // エラー! 抽象クラスのインスタンスは作れない val concreteClass = new ConcreteClass() // 文法的に正しい!
まずは上記をそのまま実行した結果が以下になる。正常にクラスが定義されconcreteClass
にインスタンスされたConcreteClass
が格納される。
defined class MyAbstractClass defined class ConcreteClass concreteClass: ConcreteClass = ammonite.$sess.cmd2$Helper$ConcreteClass@552c2904
では、上記のサンプルのコメントにあるとおりコメントを外して抽象クラスをインスタンスしようとしてみよう。結果は以下のようになる。
cmd3.sc:10: class MyAbstractClass is abstract; cannot be instantiated val abstractClass = new MyAbstractClass() // エラー! 抽象クラスのインスタンスは作れない ^Compilation Failed
更にもうひとつ、最初に書いたとおり抽象クラスで定義された未実装の値/メソッドは継承先のクラスで実装が強制されることを確認しておこう。
下記のコードはConcreteClass
のボディの部分をなくして、継承しただけのものだ。
abstract class MyAbstractClass { def myFunction(i: Int): Int val myValue: String } class ConcreteClass extends MyAbstractClass { } // 以下のコメント文をテストのために有効にしてみよう // val abstractClass = new MyAbstractClass() // エラー! 抽象クラスのインスタンスは作れない val concreteClass = new ConcreteClass() // 文法的に正しい!
これを実行すると以下のようにエラーが発生する。
cmd3.sc:5: class ConcreteClass needs to be abstract, since: it has 2 unimplemented members. /** As seen from class ConcreteClass, the missing signatures are as follows. * For convenience, these are usable as stub implementations. */ def myFunction(i: Int): Int = ??? val myValue: String = ??? class ConcreteClass extends MyAbstractClass { ^Compilation Failed
トレイト
次はトレイト。これは抽象クラスと似ている概念で未定義の値を定義することが出来る。しかし抽象クラスと比較すると以下のような違いが存在する。
- クラスは複数のトレイトを継承できる
- トレイトはコンストラクタ引数を持てない
例題:トレイトと複数の継承
早速、上記に上げた抽象クラスとの違いを確認するためのコードを見てみよう。
以下のコードでは作成したHasFunction
とHasValue
の中にそれぞれ未実装な部分が存在している。
それをMyClass
で継承して使用するというサンプルになっている。
MyClass
の宣言を見てのとおりだが、トレイトも最初に継承する際にはextends
キーワードを使用し、2個目以降はwith
キーワードを使用する。
trait HasFunction { def myFunction(i: Int): Int } trait HasValue { val myValue: String val myOtherValue = 100 } class MyClass extends HasFunction with HasValue { override def myFunction(i: Int): Int = i + 1 val myValue = "Hello World!" } // 以下のコメント文をテストのために有効にしてみよう // val myTraitFunction = new HasFunction() // 文法エラー! トレイトはインスタンス出来ない。 // val myTraitValue = new HasValue() // 文法エラー! トレイトはインスタンス出来ない。 val myClass = new MyClass() // 文法的に正しい!
上記のコードを実行すると、以下のように問題なくコンパイルされる。
defined trait HasFunction defined trait HasValue defined class MyClass myClass: MyClass = ammonite.$sess.cmd3$Helper$MyClass@5e5d3ff7
コード中のコメントにあるようにコメントを削除して、トレイトをインスタンスしようとすると以下のようなエラーが発生する。
cmd5.sc:13: trait HasFunction is abstract; cannot be instantiated val myTraitFunction = new HasFunction() // 文法エラー! トレイトはインスタンス出来ない。 ^cmd5.sc:14: trait HasValue is abstract; cannot be instantiated val myTraitValue = new HasValue() // 文法エラー! トレイトはインスタンス出来ない。 ^Compilation Faile
Chisel-Bootcampの説明によれば
一般的には、抽象クラスの単一継承による制限の効果を得たい場合に限り抽象クラスを使用し、それ以外の場合はトレイトを使用する
とのことだ。
こちらに使い分けについての指針を書いてくださっている方がいるので参考になるかも。
次はオブジェクトになるのだが、コンパニオンクラスの説明がいい感じに長いのでここで一区切り。 そんなわけで次回はオブジェクトの説明から再開。