前回の記事ではChisel Bootcampの引き続きModule3.1の学習を進め、Moduleクラスの引数を使って作成するモジュールクラスのI/O宣言部分をオプション化する方法を勉強した。
今日も引き続き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つのクラスAnimal
とHuman
が定義されておりその変換を行いそうな関数human2animal
が定義されている。
実装を見るとわかる通り、Human
クラスにはspecies
という変数は存在しないため、一見するとエラーになりそうにも見える。
早速実行してみよう。
Homo sapiens
上記のように特にエラーになることもなく、human2animal
の実装に記載されているHomo sapiens
が結果として得られた。
これがScalaにおけるimplicit
を使った型変換処理のようだ。
me.species
を参照した際に、Human
クラスにはその変数が見当たらないため、コンパイラが変換を行うための関数が存在しているかをチェックして、今回のケースではimplicit def human2animal
があったため、その処理が実行されたということになりそう。
最後にこの機能を使うにあたっての指針がBoocampに記載されているため、それを引用して終わりにしよう。
一般的には
implicit
はコードを複雑にするため、まずは継承や関数のオーバーロードを使ってた後の最終手段として使うことを推奨する。
強力な構文なので、副作用もあるからここぞ!!という時に使ったほうが良さそう。この機能についてはもう少し時間をかけてどんなケースで使うと有効なのか確認しておきたいな。。。