ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

Chisel Bootcamp - Module3.5(2) - Scalaのコンパニオン・オブジェクトとケース・クラス

スポンサーリンク

前回のChiselの記事ではChisel-bootcampのModule3.5に入りScalaオブジェクト指向言語としての特徴からクラスとトレイトについてを見ていった。

www.tech-diningyo.info

今回も引き続きModule3.5に取り組んでいく。前回の最後に記載したとおり今日はScalaのオブジェクト。

オブジェクト指向プログラミング

オブジェクト

以下はChisel-Bootcampに載っているオブジェクトについての説明。

Scalaobjectというシングルトンクラスのための言語仕様を持っている。オブジェクトはインスタンスすることは出来ず(== 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を使わずに使用すると、それはオブジェクトへの参照となる。

例題:コンパニオン・オブジェクト

上記のコンパニオン・オブジェクトを例にしたのが、以下のコードだ。 見てお分かりの通り、classobjectで同じ名前の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. クラスのコンストラクタの前後で実行したいコードがある場合
  3. クラスで複数のコンストラクタを持つ場合 以下の例では、Animalクラスのインスタンスに番号を振っている。また全ての動物はそれぞれ名前を持っており、インスタンスした順番も知る必要がある。最後に名前が与えられなかった場合には、デフォルトの名前を得られるべきである。

上記の"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)
  }
}

他にもobjectapplyをファクトリメソッドとして使う例はかなり多く存在している。というか最終的にハードになる部分はほとんどそうか。まあそれも当たり前といえば当たり前か、ハードウェアの設計なんだし。

ケース・クラス

続いてケース・クラス。これはScalaclassにいくつかの機能を付け足した特別版。

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を使う必要がある。
ScrewNaliと近い形で宣言されているが、引数リストに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でどのようにして活かしていくかを見ていく(ケース・クラスのは少し書いているが。。)。