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

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

Chisel Bootcamp - Module3.1(6) - ScalaのキーワードimplicitとChiselへの応用

スポンサーリンク

前回の記事ではChisel Bootcampの引き続きModule3.1の学習を進め、Moduleクラスの引数を使って作成するモジュールクラスのI/O宣言部分をオプション化する方法を勉強した。

www.tech-diningyo.info

今日も引き続きModule3.1を見ていく。今日は他の言語ではあまり例をみないScalaの機能implicitとそれをChiselのModuleに適用するとどんなことが出来るかを見ていく。

Module 3.1: ジェネレータ:パラメータ

Implicit

ここでは他の言語ではあまり見ることのないScalaの文法implicitについてを見ていく。まずはModule3.1の説明を引用する。

プログラミングを行っていると、提携のコードを多く利用する場面がしばしば発生する。このようなケースをに対応するために、Scalaではimplicitという構文が用意されている。これは一種の構文糖として動作する。ある状況の裏で多くのことが起きるため、この構文はとても不思議なものに見えるだろう。このセクションではいくつかの基本的な例を通して、implicitの説明とどのように使うのが一般的であるかということについてを掘り下げていく。

暗黙の引数

とある引数をトップ階層からみて、何段も下の関数で使うようなケースがある。通常このような引数を下位の関数に引き渡すためには、関数の引数に所望の引数を設定して、使用したい関数までその引数を渡していく必要がある。Scalaではこの作業を手動で行う代わりに、暗黙の引数を使うことが出来る。

例題:暗黙の猫

早速例題を見ていこう。

object CatDog {
  implicit val numberOfCats: Int = 3
  //implicit val numberOfDogs: Int = 5

  def tooManyCats(nDogs: Int)(implicit nCats: Int): Boolean = nCats > nDogs
    
  val imp = tooManyCats(2)    // Argument passed implicitly!
  val exp = tooManyCats(2)(1) // Argument passed explicitly!
}
CatDog.imp
CatDog.exp

コードを見るとわかる通り、object内の変数numberOfCatsにと唯一の関数tooManyCatsの引数にimplicitというキーワードが付与されている。

その関数を使って宣言した2つの変数imp/expを評価するとどのような動きをするか、、というのがこの例で注目スべきポイントだ。

早速実行例を見てみよう。

defined object CatDog
res5_1: Boolean = true
res5_2: Boolean = false

結果は2つの変数の評価はどちらも正常に完了しそれぞれtrue/falseとなった。これだけだと、C++pythonで見かけるデフォルト値の動きに似ている感じではある。

もう少し動きを掘り下げるために、object内で宣言されるimplicit付きの変数の値を変更してみる。

object CatDog {
  //implicit val numberOfCats: Int = 3
  implicit val numberOfDogs: Int = 1

  def tooManyCats(nDogs: Int)(implicit nCats: Int): Boolean = nCats > nDogs
    
  val imp = tooManyCats(2)    // Argument passed implicitly!
  val exp = tooManyCats(2)(1) // Argument passed explicitly!
}
CatDog.imp
CatDog.exp

こちらを先と同様に実行してみよう。

defined object CatDog
res6_1: Boolean = false
res6_2: Boolean = false

先ほどとはことなり、ひとつ目のimpの評価値がfalseになった。

コメントにimpicitlyとあるように、関数呼び出し時の2つ目の引数が省略されているため、何らかの値が渡されていることになるのだが、その実体がobject内に定義されたnumberOfCatsだったりnumberOfDogsだったりするということのようだ。

ではobject内で宣言するInt型の変数を2つにしてみるとどうなるかを実験してみる。

object CatDog {
  implicit val numberOfCats: Int = 3
  implicit val numberOfDogs: Int = 5

  def tooManyCats(nDogs: Int)(implicit nCats: Int): Boolean = nCats > nDogs
    
  val imp = tooManyCats(2)    // Argument passed implicitly!
  val exp = tooManyCats(2)(1) // Argument passed explicitly!
}
CatDog.imp
CatDog.exp

結果は下記のようにエラーとなった。

cmd7.sc:7: ambiguous implicit values:
 both value numberOfDogs in object CatDog of type => Int
 and value numberOfCats in object CatDog of type => Int
 match expected type Int
  val imp = tooManyCats(2)    // Argument passed implicitly!
                       ^Compilation Failed

さらにもうひとつの実験。変数宣言に付いているキーワードimplicitを外してみる。

object CatDog {
  //implicit val numberOfCats: Int = 3
  val numberOfDogs: Int = 1

  def tooManyCats(nDogs: Int)(implicit nCats: Int): Boolean = nCats > nDogs
    
  val imp = tooManyCats(2)    // Argument passed implicitly!
  val exp = tooManyCats(2)(1) // Argument passed explicitly!
}
CatDog.imp
CatDog.exp

これを実行してみると、下記のようにエラーとなった。

cmd12.sc:7: could not find implicit value for parameter nCats: Int
  val imp = tooManyCats(2)    // Argument passed implicitly!
                       ^Compilation Failed

これらのことから、特定のスコープ内で関数の引数をimplicit付き宣言すると、そのスコープ無いからimplicit付きの変数を探して、見つかった場合はその引数を暗黙の引数として処理するということをコンパイラが行ってくれるようだ。

説明を読み進めていくと、どのような条件にするとエラーとなるかが書いてあった:

  • 2つ以上の同一の型の暗黙の変数(implicit付きの変数が)が同じスコープ内にある
  • 関数呼び出し時にコンパイラが暗黙の引数を見つけられない時

これはちょうど、上で実験してエラーとなった場合と同様だ。

例題:暗黙のロギング

では「この機能を使うとどのようなことが出来るのか」という問いへの答えがこの例題となる。

sealed trait Verbosity
implicit case object Silent extends Verbosity
case object Verbose extends Verbosity

class ParameterizedWidthAdder(in0Width: Int, in1Width: Int, sumWidth: Int)(implicit verbosity: Verbosity)
extends Module {
  def log(msg: => String): Unit = verbosity match {
    case Silent =>
    case Verbose => println(msg)
  }
  require(in0Width >= 0)
  log(s"in0Width of $in0Width OK")
  require(in1Width >= 0)
  log(s"in1Width of $in1Width OK")
  require(sumWidth >= 0)
  log(s"sumWidth of $sumWidth OK")
  val io = IO(new Bundle {
    val in0 = Input(UInt(in0Width.W))
    val in1 = Input(UInt(in1Width.W))
    val sum = Output(UInt(sumWidth.W))
  })
  log("Made IO")
  io.sum := io.in0 + io.in1
  log("Assigned output")
}

println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)))
println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)(Verbose)))

sealed付きのtraitとして宣言したVerbosityを継承して、以下の2つを宣言されている

  • implicit付きのcase object(よくわかってないが、使われ方からすると、match文のマッチ対象となる型)
  • implicit無しのcase object

これを使った関数logが定義されており、match対象となるverbosityの状況に応じて、文字出力が行われたり行われなかったりするという動作になるようだ。

実行結果

では実行してみよう。

[info] [0.000] Elaborating design...
[info] [0.010] Done elaborating.
Total FIRRTL Compile Time: 14.0 ms
module cmd8HelperParameterizedWidthAdder( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input        io_in0, // @[:@6.4]
  input  [3:0] io_in1, // @[:@6.4]
  output [4:0] io_sum // @[:@6.4]
);
  wire [3:0] _GEN_0; // @[cmd8.sc 23:20:@8.4]
  wire [4:0] _T_11; // @[cmd8.sc 23:20:@8.4]
  wire [3:0] _T_12; // @[cmd8.sc 23:20:@9.4]
  assign _GEN_0 = {{3'd0}, io_in0}; // @[cmd8.sc 23:20:@8.4]
  assign _T_11 = _GEN_0 + io_in1; // @[cmd8.sc 23:20:@8.4]
  assign _T_12 = _T_11[3:0]; // @[cmd8.sc 23:20:@9.4]
  assign io_sum = {{1'd0}, _T_12};
endmodule

[info] [0.000] Elaborating design...
in0Width of 1 OK
in1Width of 4 OK
sumWidth of 5 OK
Made IO
Assigned output
[info] [0.012] Done elaborating.
Total FIRRTL Compile Time: 20.1 ms
module cmd8HelperParameterizedWidthAdder( // @[:@3.2]
  input        clock, // @[:@4.4]
  input        reset, // @[:@5.4]
  input        io_in0, // @[:@6.4]
  input  [3:0] io_in1, // @[:@6.4]
  output [4:0] io_sum // @[:@6.4]
);
  wire [3:0] _GEN_0; // @[cmd8.sc 23:20:@8.4]
  wire [4:0] _T_11; // @[cmd8.sc 23:20:@8.4]
  wire [3:0] _T_12; // @[cmd8.sc 23:20:@9.4]
  assign _GEN_0 = {{3'd0}, io_in0}; // @[cmd8.sc 23:20:@8.4]
  assign _T_11 = _GEN_0 + io_in1; // @[cmd8.sc 23:20:@8.4]
  assign _T_12 = _T_11[3:0]; // @[cmd8.sc 23:20:@9.4]
  assign io_sum = {{1'd0}, _T_12};
endmodule

以下の様に(Verbose)をつけて実行した場合にのみ、requireの結果に応じたログメッセージが出力されている。

println(getVerilog(new ParameterizedWidthAdder(1, 4, 5)(Verbose)))

最初の部分に書いたが、このログ取得機能を他の言語で実装する場合は、ログ関数のスコープをグローバルにしてしまうか、もしくは冒頭で書いたように、全ての関数の引数にログ取得用のオブジェクトを渡すような処理をする必要がある。

一方でScalaではデバッグ時のログレベルに応じたプリント出力を切り替える処理をこんな形で書くことが出来るのか。

暗黙の型変換

例題:暗黙の型変換

implicitのもうひとつの用法に型変換がある。

早速例を見てみる。

class Animal(val name: String, val species: String)
class Human(val name: String)
implicit def human2animal(h: Human): Animal = new Animal(h.name, "Homo sapiens")
val me = new Human("Adam")
println(me.species)

見ての通りで、2つのクラスAnimalHumanが定義されておりその変換を行いそうな関数human2animalが定義されている。

実装を見るとわかる通り、Humanクラスにはspeciesという変数は存在しないため、一見するとエラーになりそうにも見える。

早速実行してみよう。

Homo sapiens

上記のように特にエラーになることもなく、human2animalの実装に記載されているHomo sapiensが結果として得られた。

これがScalaにおけるimplicitを使った型変換処理のようだ。

me.speciesを参照した際に、Humanクラスにはその変数が見当たらないため、コンパイラが変換を行うための関数が存在しているかをチェックして、今回のケースではimplicit def human2animalがあったため、その処理が実行されたということになりそう。

最後にこの機能を使うにあたっての指針がBoocampに記載されているため、それを引用して終わりにしよう。

一般的にはimplicitはコードを複雑にするため、まずは継承や関数のオーバーロードを使ってた後の最終手段として使うことを推奨する。

強力な構文なので、副作用もあるからここぞ!!という時に使ったほうが良さそう。この機能についてはもう少し時間をかけてどんなケースで使うと有効なのか確認しておきたいな。。。