前回のChiselの記事ではChisel-bootcampのModule3.5に入りScalaのオブジェクト指向言語としての特徴からクラスとトレイトについてを見ていった。
今回も引き続きModule3.5に取り組んでいく。前回の最後に記載したとおり今日はScalaのオブジェクト。
オブジェクト指向プログラミング
オブジェクト
以下はChisel-Bootcampに載っているオブジェクトについての説明。
Scalaは
object
というシングルトンクラスのための言語仕様を持っている。オブジェクトはインスタンスすることは出来ず(==new
を呼ぶ必要が無い)、ただ単に直接参照すればいい。これはJavaのスタティッククラスと似ている。
例題:オブジェクト
早速オブジェクトについての例を見ていこう。
object MyObject { def hi: String = "Hello World!" def apply(msg: String) = msg } println(MyObject.hi) println(MyObject("This message is important!")) // これは"MyObject.apply(msg)" // を実行したのと等価
先に書いたようにobject
で宣言を行っているので、使用する際にnew
を使ってインスタンスせずにobject
内のメソッドにアクセスを行っているのがわかるかと思う。
上記コードを実行した結果は以下となる。
Hello World! This message is important!
コンパニオン・オブジェクト
続いてコンパニオン・オブジェクト。これも説明を見ておこう。
クラスとオブジェクトが同一ファイル内で同じ名前で定義されるとき、そのオブジェクトはコンパニオン・オブジェクトと呼ばれる。クラス/オブジェクト名の前に
new
を使とクラスがインスタンスされ、new
を使わずに使用すると、それはオブジェクトへの参照となる。
例題:コンパニオン・オブジェクト
上記のコンパニオン・オブジェクトを例にしたのが、以下のコードだ。
見てお分かりの通り、class
とobject
で同じ名前のLion
が定義されている。このコードを同一のファイルに収めて実行した場合object Lion
はコンパニオン・オブジェクトとなる。サンプルコードの後半にあるようにnew
を使って呼ぶかどうかでclass Lion
となるかobject Lion
となるかが決定される。
object Lion { def roar(): Unit = println("I'M AN OBJECT!") } class Lion { def roar(): Unit = println("I'M A CLASS!") } new Lion().roar() Lion.roar()
そのため上記のサンプルコードを実行すると、実際に呼び出されるroar
メソッドの所属が異なるため別のメッセージが表示される。
I'M A CLASS! I'M AN OBJECT! defined object Lion defined class Lion
何に使うの??
ではこれはどのようなケースで使われるのだろうか?? このあたりも触れてくれているので、そのまま引用させていただく。
多くの場合コンパニオン・オブジェクトは以下のような理由で使用される:
上記の"1"/"2"を実際に試したのが以下のコードだ。
object Animal
内に呼び出した動物の数を管理するnumberOfAnimals
を準備しておき、それをapply
が呼ばれるたびにインクリメントしている。
object Animal { val defaultName = "Bigfoot" private var numberOfAnimals = 0 def apply(name: String): Animal = { numberOfAnimals += 1 new Animal(name, numberOfAnimals) } def apply(): Animal = apply(defaultName) } class Animal(name: String, order: Int) { def info: String = s"Hi my name is $name, and I'm $order in line!" } val bunny = Animal.apply("Hopper") // Animalクラスのファクトリメソッドを呼び出す println(bunny.info) val cat = Animal("Whiskers") // Animalクラスのファクトリメソッドを呼び出す println(cat.info) val yeti = Animal() // Animalクラスのファクトリメソッドを呼び出す println(yeti.info)
- 実行結果
Animal
の呼び出し時に引数を取らなかった3番目のyeti
はデフォルトの名前である"Bigfoot"
が与えられている。
Hi my name is Hopper, and I'm 1 in line! Hi my name is Whiskers, and I'm 2 in line! Hi my name is Bigfoot, and I'm 3 in line!
因みにこのコンパニオン・オブジェクトはChiselのライブラリで色々使われている。モジュールを作成する際に使うModule
とかCounter
とか。。。
とりあえずコード量がいい感じだったのでCounter
のコードを載せておく。
/** A counter module * * Typically instantiated with apply methods in [[Counter$ object Counter]] * * @example {{{ * val countOn = true.B // increment counter every clock cycle * val (counterValue, counterWrap) = Counter(countOn, 4) * when (counterValue === 3.U) { * ... * } * }}} * * @param n number of counts before the counter resets (or one more than the * maximum output value of the counter), need not be a power of two */ @chiselName class Counter(val n: Int) { require(n >= 0) val value = if (n > 1) RegInit(0.U(log2Ceil(n).W)) else 0.U /** Increment the counter, returning whether the counter currently is at the * maximum and will wrap. The incremented value is registered and will be * visible on the next cycle. */ def inc(): Bool = { if (n > 1) { val wrap = value === (n-1).asUInt value := value + 1.U if (!isPow2(n)) { when (wrap) { value := 0.U } } wrap } else { true.B } } } object Counter { /** Instantiate a [[Counter! counter]] with the specified number of counts. */ def apply(n: Int): Counter = new Counter(n) /** Instantiate a [[Counter! counter]] with the specified number of counts and a gate. * * @param cond condition that controls whether the counter increments this cycle * @param n number of counts before the counter resets * @return tuple of the counter value and whether the counter will wrap (the value is at * maximum and the condition is true). */ @chiselName def apply(cond: Bool, n: Int): (UInt, Bool) = { val c = new Counter(n) var wrap: Bool = null when (cond) { wrap = c.inc() } (c.value, cond && wrap) } }
他にもobject
のapply
をファクトリメソッドとして使う例はかなり多く存在している。というか最終的にハードになる部分はほとんどそうか。まあそれも当たり前といえば当たり前か、ハードウェアの設計なんだし。
ケース・クラス
続いてケース・クラス。これはScalaのclass
にいくつかの機能を付け足した特別版。
class Nail(length: Int) // 通常のクラス val nail = new Nail(10) // 通常のクラスなので`new`が必要 // println(nail.length) // 文法エラー! クラスのコンストラクタのパラメータは // デフォルトでは外部からは見えない。 class Screw(val threadSpace: Int) // `val`を付与することで`threadSpace`は外部に公開される val screw = new Screw(2) // 上記と一緒で`new`が必要 println(screw.threadSpace) case class Staple(isClosed: Boolean) // ケース・クラスのコンストラクタのパラメータ // はデフォルトで外部に公開される。 val staple = Staple(false) // `new`は必要ない println(staple.isClosed)
上記を実行すると以下のようになる。
2 false defined class Nail nail: Nail = ammonite.$sess.cmd0$Helper$Nail@1b26f334 defined class Screw screw: Screw = ammonite.$sess.cmd0$Helper$Screw@277167cd defined class Staple staple: Staple = Staple(false)
ここも説明を引用しておく。
Nali
は通常のクラスで、クラスのパラメータはval
をつけて宣言されていないため外部には見えない。またNali
クラスのインスタンスを作るにはnew
を使う必要がある。
Screw
はNali
と近い形で宣言されているが、引数リストにval
が付与されている。これによりthreadSpace
は外部から見えるようになる。
ケース・クラスを使うことによって、Staple
は全てのパラメータが外部から見えるという利益を得る(これはval
をつけたかどうかは関係しない)
加えてStaple
はケース・クラスとして宣言しているので使用する際にはnew
が必要ない。これはScalaのコンパイラがケース・クラス用のapply
メソッドを含んだコンパニオン・オブジェクトを自動的に作成しているからだ。
ケース・クラスは多くのパラメータを持つジェネレーターにとって良いコンテナになる。コンストラクタは継承したパラメータを定義したり、入力をチェックするためのにちょうどいい場所を提供してくれる。
この最後の部分の”良いコンテナになる”だが、例えば以下の様にケース・クラスを作ってパラメータを定義しておきChiselモジュールのコンストラクタ・パラメータにこれを引き渡すようにしておくことを指している。
case class SomeGeneratorParameters( someWidth: Int, someOtherWidth: Int = 10, pipelineMe: Boolean = false ) { require(someWidth >= 0) require(someOtherWidth >= 0) val totalWidth = someWidth + someOtherWidth }
上記を使う場合はこんな(↓)感じでコンストラクタの引数にSomeGeneratorParameters
を入れておく。説明にもあったとおり、ケース・クラス内部のパラメータ
class MyModule(val params: SomeGeneratorParameters) extends Module { val io = IO(new Bundle { val inData1 = Input(UInt(params.someWidth.W)) val inData2 = Input(UInt(params.someWidth.W)) val outData1 = Output(UInt(params.totalWidth.W)) }) io.outData1 := Cat(io.inData1, io.inData2) }
- 生成されるRTL
module cmd7HelperMyModule( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [9:0] io_inData1, // @[:@6.4] input [9:0] io_inData2, // @[:@6.4] output [19:0] io_outData1 // @[:@6.4] ); wire [19:0] _T_11; // @[Cat.scala 30:58:@8.4] assign _T_11 = {io_inData1,io_inData2}; // @[Cat.scala 30:58:@8.4] assign io_outData1 = _T_11; endmodule
このようにコンストラクタのパラメータリストをケース・クラスのみにしてインターフェースを統一できるため、以降の変更が容易になる。また例に含まれているがrequire
を使ってチェックをかけておくことでChiselのエラボレート時にチェックが行えるため、意図しない設定を行わせないようにすることが可能だ。
さて、ここまでがScalaのオブジェクト指向言語としての側面として紹介されていた項目になる。次回はこれをChiselでどのようにして活かしていくかを見ていく(ケース・クラスのは少し書いているが。。)。