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

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

Chisel Bootcamp - Module3.4(1) - 関数型言語

スポンサーリンク

前回のChiselの記事ではChisel-bootcampのModule3.3の高階関数を使った設計の仕上げとして練習問題に取り組んだ。

www.tech-diningyo.info

今日からModule3.4に入る。Module3.4は関数型言語についてだ。

関数型言語

モチベーション

これまでどおりモチベーションから。

あなたはこれまでのモジュールで多くのカンスを目にしてきた。今こそ自分で関数を作ってそれらを効果的に使う時だ。

Scala関数プログラミング

例題:Scalaの関数

ここはこれまでのおさらいの様なものなのでサクッと例だけ見ていく。

// No inputs or outputs (two versions).
def hello1(): Unit = print("Hello!")
def hello2 = print("Hello again!")

// Math operation: one input and one output.
def times2(x: Int): Int = 2 * x

// Inputs can have default values, and explicitly specifying the return type is optional.
// Note that we recommend specifying the return types to avoid surprises/bugs.
def timesN(x: Int, n: Int = 2) = n * x

// Call the functions listed above.
hello1()
hello2
times2(4)
timesN(4)         // no need to specify n to use the default value
timesN(4, 3)      // argument order is the same as the order where the function was defined
timesN(n=7, x=2)  // arguments may be reordered and assigned to explicitly

なんの変哲もないScalaの関数だ。実行すると以下の出力が得られる。

Hello!Hello again!

defined function hello1
defined function hello2
defined function times2
defined function timesN
res2_6: Int = 8
res2_7: Int = 8
res2_8: Int = 12
res2_9: Int = 14

オブジェクトとしての関数

Scalaでは関数は第一級のオブジェクトである(らしい)。イマイチきちんと定義を把握していないのでwikipediaさんの説明をペタリ。

第一級オブジェクト(ファーストクラスオブジェクト、first-class object)は、あるプログラミング言語において、たとえば生成、代入、演算、(引数・戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる対象のことである。ここで「オブジェクト」とは広く対象物・客体を意味し、必ずしもオブジェクト指向プログラミングにおけるオブジェクトを意味しない。第一級オブジェクトは「第一級データ型に属す」という。 https://ja.wikipedia.org/wiki/%E7%AC%AC%E4%B8%80%E7%B4%9A%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88

うん、、なんとなくわかるような、、、でもしっくりこないような??と言った感じなのでこれも例を見ていこう。

例題:関数オブジェクト

例題は以下になる。plus1val/times2valvalに関数が入っているのがわかるかと思う。

// 以下は通常の関数
def plus1funct(x: Int): Int = x + 1
def times2funct(x: Int): Int = x * 2

// 以下は変数としての関数
// 最初の例では明示的に戻り値の型を指定している
val plus1val: Int => Int = x => x + 1
val times2val = (x: Int) => x * 2

// 呼び出し方は一緒
plus1funct(4)
plus1val(4)
plus1funct(x=4)
//plus1val(x=4) // これは動作しない。

ここはきちんとテキストを訳してみておこう。

なぜdefの代わりにvalを使うのだろう? 以下の例に示すようにvalを使うと関数を他の関数に渡すことができる。また同様に関数の引数に関数を指定できるような関数を作成することも可能だ。このように関数を受け取ったり関数を生成するような関数を一般的に高階関数という。あなたはすでにModule3.3でこれを勉強してきた。今こそ自分の高階関数を作ってみよう。

例題:高階関数

// 関数を作る
val plus1 = (x: Int) => x + 1
val times2 = (x: Int) => x * 2

// Listの関数mapに上の関数を渡す
val myList = List(1, 2, 5, 9)
val myListPlus = myList.map(plus1)
val myListTimes = myList.map(times2)

// opに指定した処理をN回繰り返すような関数を作成
def opN(x: Int, n: Int, op: Int => Int): Int = {
  if (n <= 0) { x }
  else { opN(op(x), n-1, op) }
}

opN(7, 3, plus1)
opN(7, 3, times2)

ここで新しいのは新規で追加されたopN関数だ。引数opを見るとわかる通り関数を渡すようになっている。 そのようにして渡された引数opを内部で引数nの状況に従って以下の2つのうちのいずれかを実行するようになっている。

条件 処理
n <= 0の時 xを返却
それ以外    opN再帰呼出し
plus1: Int => Int = <function1>
times2: Int => Int = <function1>
myList: List[Int] = List(1, 2, 5, 9)
myListPlus: List[Int] = List(2, 3, 6, 10)
myListTimes: List[Int] = List(2, 4, 10, 18)
defined function opN
res6_6: Int = 10
res6_7: Int = 56

上記の様に変数に格納した関数を引数として渡すことでopNを一切変更せずに引数だけで異なる処理が実行できている。

例題:関数 vs オブジェクト

続いて関数とオブジェクトの使い分けについて。特に引数が無いようなケースを考えてみる。下記の例ではvalに格納したRandom.nextIntと通常の関数が定義されている。 このようなケースで挙動に違いがあるのかを試してみよう。

import scala.util.Random

// x/yともにRandom.nextIntを読んでいる。 xはすぐに評価される一方でyは関数となる。
val x = Random.nextInt
def y = Random.nextInt

// xは宣言時に既に評価済みとなるため定数となる。
println(s"x = $x")
println(s"x = $x")

// yは関数なので呼び出すたびに値が更新される
println(s"y = $y")
println(s"y = $y")

まあ上のコメントの通りでval/defでは評価されるタイミングが異なるため以下のようにxは同じが値/yは異なる値が得られる。

x = 1164950650
x = 1164950650
y = -1334763489
y = -1772365788

無名関数

名称そのままで関数名が存在しない関数も作ることが出来る。pythonだとlambda使うやつ。

例題:無名関数

早速例を見ていく。とは言っても実は既に使っていたりするものを改めて説明する感じになる。

val myList = List(5, 6, 7, 8)

// 無名関数を使ってリストの全ての要素に"1"を加算する
// 引数は"_"に渡される
// 以下の2つは同じことを実行している
myList.map( (x:Int) => x + 1 )
myList.map(_ + 1)

// 無名関数内でcase文を使う処理が一般的
val myAnyList = List(1, 2, "3", 4L, myList)
myAnyList.map {
  case (_:Int|_:Long) => "Number"
  case _:String => "String"
  case _ => "error"
}

コードサンプルの通りで、前回の高階関数で使ったmapを使った処理の際に引数に渡しているものが無名関数になる。実行結果は以下の通り。

myList: List[Int] = List(5, 6, 7, 8)
res1_1: List[Int] = List(6, 7, 8, 9)
res1_2: List[Int] = List(6, 7, 8, 9)
myAnyList: List[Any] = List(1, 2, "3", 4L, List(5, 6, 7, 8))
res1_4: List[String] = List("Number", "Number", "String", "Number", "error")

練習問題:シーケンスの操作

ここは問題文を訳したものを載せておく。

高階関数の共通セットとしてscanLeft/scanRight, reduceLeft/reduceRight、そしてfoldLeft/foldRightがある。これらの操作がどのように処理され、そして使うかを理解しておくことが大切だ。scan/reduce/foldのデフォルトの処理の方向は左に向かってとなるが、全てのケースで保証されているわけではない。

問題としては以下のスケルトンコード中のassertをパスできるように???を実装するというものだ。

val exList = List(1, 5, 7, 100)

// 2つの数字を足す関数を作成し、それからreduceをexListの要素の和を求めよう
def add(a: Int, b: Int): Int = ???
val sum = ???

// 無名関数を使ってexListの要素の和を求めよう (ヒント: 既に見てきたはずだ!)
val anon_sum = ???

// exListを右から左に処理した場合の移動平均を求めよう; 結果の(ma2)はDoubleのリストとなる。
def avg(a: Int, b: Double): Double = ???
val ma2 = ???

assert(add(88, 88) == 176)
assert(sum == 113)

assert(anon_sum == 113)

assert(avg(100, 100.0) == 100.0)
assert(ma2 == List(8.875, 16.75, 28.5, 50.0, 0.0))

ここまでを踏まえて実装した自分の解答 - クリックすると開くので、見たくない場合は開かないように注意。

def add(a: Int, b: Int): Int = a + b
val sum = exList.reduce(add)

val anon_sum = exList.reduce(_ + _)

def avg(a: Int, b: Double): Double = (a + b)/2.0
val ma2 = exList.scanRight(0.0)(avg)

今日はここまで。次回は今回Scalaで試した処理をChiselに適用することを考えていく。