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

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

Chisel Bootcamp - Module3.3 (1) - 高階関数

スポンサーリンク

前回のChisel-Bootcampの記事ではやっと標準ライブラリの紹介を終えたところだった。

www.tech-diningyo.info

ちょっと間が空いたがChisel-Bootcampを進めていこうと思う。今日は高階関数だ。

Module 3.3: 高階関数

モチベーション

これまで通りモチベーションの訳から。

前のモジュールの厄介なforループは冗長であり、関数型プログラミングの目的に反するものだ。このモジュールではあなたのジェネレータがファンクタを獲得する。

2つのFIRフィルタの物語

以前のモジュールでは、FIRフィルタの畳込み処理を以下のように書いた。

val muls = Wire(Vec(length, UInt(8.W)))
for(i <- 0 until length) {
  if(i == 0) muls(i) := io.in * io.consts(i)
  else       muls(i) := regs(i - 1) * io.consts(i)
}

val scan = Wire(Vec(length, UInt(8.W)))
for(i <- 0 until length) {
  if(i == 0) scan(i) := muls(i)
  else scan(i) := muls(i) + scan(i - 1)
}

io.out := scan(length - 1)

これは実装としては全然間違ったものではないが冗長なので、もう少しシンプルに書けないかを考えたほうがいい。Chiselではこの処理は以下のようにシンプルに一行で書くことが可能だ。

io.out := (taps zip io.consts).map { case (a, b) => a * b }.reduce(_ + _)

実はこの構文の解説は以下の記事で既にやっているので、細かい説明はそちらを見ていただきたい。

www.tech-diningyo.info

引数としての関数

上記の構文を理解する上で必要なのがmapreduceといった関数は関数を引数に持っているということだ。 このような関数を高階関数と呼ぶ。

ja.wikipedia.org

Chisel-Bootcampの説明によれば

上記の例で既に明らかなようにこの構文は非常に強力で一般的な計算パターンをカプセル化する。これによって実装者は計算の流れの制御ではなくアプリケーションの論理に集中することができ、結果として簡潔なコードとなる。

引数の指定

上記の例では2つの方法で引数をしているのがわかると思う。それらは以下のものだ。

  • その関数向けに一度宣言された引数は_で参照することが出来る。上記ではreduce引数関数は2つの引数を取り、_ + _で指定されている。

どうも色々便利に使うために難解なルールに基づいて処理されているようで、とりあえず試してみて動かなければ次の方法を試してみるとおいいらしい。

  1. 引数を明示的にリスト化する。reduceの例では直前のブロック式中で(a, b) => a + bと書くことで引数をタプル化して指定している。
  2. タプルのアンパックが必要な場合はcase文を使う。これは2つの要素からなるタプルを引数として受取り、アンパックしてabに格納する。

1.は上記で見てきたio.outの1行が以下のようにも書けるということだ。

io.out := taps.zip(io.consts).map { case (a, b) => a * b }.reduce((a, b) => a + b)

Scalaで練習

高階関数の扱いに慣れるために幾つかの例が紹介されているので見ておこう。

println(List(1, 2, 3, 4).map(x => x + 1))       // 引数を明示した例
println(List(1, 2, 3, 4).map(_ + 1))            // 上記の例で暗黙の引数を使った場合guments
println(List(1, 2, 3, 4).map(_.toString + "a")) // 入力の型を別の型にすることも可能

// タプルをアンパックする。`{}`を使っていることに注意("()"を使うと文法エラー)
println(List((1, 5), (2, 6), (3, 7), (4, 8)).map { case (x, y) => x*y })

// 補足:Scalaは連続する数のリストを作成する文法がある
println(0 to 10)     // toは"以下"を意味する
println(0 until 10)  // untilは"未満"を意味する

// 以下はリストのように振る舞うので、インデックスの生成に役立つ
val myList = List("a", "b", "c", "d")
println((0 until 4).map(myList(_)))

実行すると以下のようになる。

List(2, 3, 4, 5)
List(2, 3, 4, 5)
List(1a, 2a, 3a, 4a)
List(5, 12, 21, 32)
Range(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Range(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
Vector(a, b, c, d)

もう少し例を見ておく。次は要素が2以上になる例。

println(List((1, 5, 3), (2, 6, 3), (3, 7, 3), (4, 8, 3)).map{ case (x, y, z) => x * y * z }) // this unpacks a tuple, note use of curly braces

これは型さえ合っていれば問題にならない。

List(15, 36, 63, 96)

次の例はListに含まれているタプルの要素がマッチしない場合だ。

println(List((1, 5, 3), (2, 6, 3), (3, 7, 3), (4, 8, 3)).map { case (x, y) => x*y })  // this unpacks a tuple, note use of curly braces

以下のように引数の型が合わないのでエラーになる。

cmd5.sc:1: constructor cannot be instantiated to expected type;
 found   : (T1, T2)
 required: (Int, Int, Int)
val res5 = println(List((1, 5, 3), (2, 6, 3), (3, 7, 3), (4, 8, 3)).map { case (x, y) => x*y })  // this unpacks a tuple, note use of curly braces
                                                                               ^Compilation Failed

これは一部の要素が間違っていても一緒。 とりあえず2個以上の引数を使いたいときは_で試してみて駄目ならcase使って明示しておけばなんとかなりそう。 これを自然に使いこなせてこそ、、、と言ったところなのだろうが、まだまだ時間がかかりそう。とりあえず次回は残りの例の紹介と練習問題をこなして行こうと思う。