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

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

Chisel Bootcamp - Module3.6(2) - SaclaのunapplyとPartial Function

スポンサーリンク

前回はChisel-Bootcampの学習に戻りModule3.6を見ていった。

www.tech-diningyo.info

今回も引き続きModule3.6を進めていく。今日はScalaunapplyだ。

ジェネレータ:型について

unapply

前回で見てきたようにScalamatchは値や型などさまざまものにマッチさせることが出来る。 ケースクラスを使う場合には、以下のような使い方も可能だ。

case class Something(a: String, b: Int)
val a = Something("A", 3)
a match {
    case Something("A", value) => value
    case Something(str, 3)     => 0
}

では、ここで実際に何が起きているのか??ということだが、その答えがunapplyになる。 Scalaのケースクラスを宣言すると、コンパニオン・オブジェクトが生成され、その中に幾つかのメソッドが自動で定義される仕組みになっており、その生成されるメソッドの一つがunapplyメソッドになる。 Bootcampの説明によると、、

Scalaunapplyメソッドは別形態の構文糖でmatch文に型によるマッチとマッチした型の値を変数に展開するという2つの機能を与える

とある。 とりあえず例で見てみよう。 因みにこの例、Bootcampのコードだと何故かSomeGeneratorParametersが定義されてないので、適当に追加したものを使っている。 自分の使ってたBootcampのデータが古かっただけでした。。ということで今のでデータで更新(ほぼ同じだったけど)

case class SomeGeneratorParameters(
    someWidth: Int,
    someOtherWidth: Int = 10,
    pipelineMe: Boolean = false
) {
    require(someWidth >= 0)
    require(someOtherWidth >= 0)
    val totalWidth = someWidth + someOtherWidth
}

def delay(p: SomeGeneratorParameters): Int = p match {
    case sg @ SomeGeneratorParameters(_, _, true) => sg.totalWidth * 3
    case SomeGeneratorParameters(_, sw, false) => sw * 2
}

println(delay(SomeGeneratorParameters(10, 10)))
println(delay(SomeGeneratorParameters(10, 10, true)))
  • 実行結果
20
60

defined class SomeGeneratorParameters
defined function delay

ここで注目しておきたいのは以下の2点だ。

  • 内部のパラメータを直接参照している(2番目のSomeGeneratorParameters(_, sw, false))のswのこと
  • パラメータの内部の値に直接マッチしている(1番目/2番目のケースのSomeGeneratorParameters.hasSgのこと)

unapplyが呼ばれることによって、マッチ時に内部の変数が展開され(上記のケース文の(_, _, true)(_, sw, true)のこと)、その値に対してのマッチが行われたのち、マッチ後の処理においてもその展開された値を使用することが可能になる。

先ほどBootcampの説明を引用した部分に書いたようにこれはScalaの構文糖なので、前回見てきたmatchの構文を使って書くことももちろん可能だ。 Bootcampで紹介されている例を見てみよう。

まずひとつ目。 これは前回見た型に対するマッチと、それをunapplyメソッドを使う形に直したものなので、やっている事自体は一緒になる。

case p: SomeGeneratorParameters => p.sw * 2      // こっちは普通の型に対するマッチ
case SomeGeneratorParameters(_, sw, _) => sw * 2 // unapplyを使ったマッチ

2番目。これはどちらもunapplyを使う形。 この2つの違いは、マッチしたクラス自体へのリファレンスを保持するかどうかにある。 これもやっていることは全く同じ。

case SomeGeneratorParameters(_, sw, true) => sw
case sg@SomeGeneratorParameters(_, sw, true) => sw // マッチしたSomeGeneratorParametersはsgで参照可能

3番目。これは前回までのmatchで型+値でマッチするようなケースとそれをunapplyで書いたものの比較になる。 書き方は違えど、意味的はこれも全部一緒。 3つ目の例は若干野暮ったい。

case SomeGeneratorParameters(_, sw, false) => sw * 2
case s@SomeGeneratorParameters(_, sw, false) => s.sw * 2
case s: SomeGeneratorParameters if s.pipelineMe => s.sw * 2

先の説明にケースクラス(case class A)を作ると、コンパニオン・オブジェクトが生成されその中にunapplyが生成されると書いたとおり、通常のコンパニオン・オブジェクトを作成しただけではこれらのメソッドは定義されていない。 この場合は自分でそれらのメソッドを定義しておけば使用することが可能になる。

class Boat(val name: String, val length: Int)
object Boat {
    def unapply(b: Boat): Option[(String, Int)] = Some((b.name, b.length))
    def apply(name: String, length: Int): Boat = new Boat(name, length)
}

def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter { b =>
    b match {
        case Boat(_, length) if length < 60 => true
        case Boat(_, _) => false
    }
}

val boats = Seq(Boat("Santa Maria", 62), Boat("Pinta", 56), Boat("Nina", 50))
println(getSmallBoats(boats).map(_.name).mkString(" and ") + " are small boats!")
  • 実行結果
Pinta and Nina are small boats!

defined class Boat
defined object Boat
defined function getSmallBoats
boats: Seq[Boat] = List(
  ammonite.$sess.cmd6$Helper$Boat@15523510,
  ammonite.$sess.cmd6$Helper$Boat@1e7b86dd,
  ammonite.$sess.cmd6$Helper$Boat@315d6815
)

部分関数

部分関数は"Partial Function"の直訳。なんやねん、それってなったので、調べてみたら以下の記事がとっても分かりやすかったのでそちらを見てもらった方がいいかも。

yuroyoro.hatenablog.com

上記の記事から引用させてもらうと、

これはなにかっていうと「特定の引数に対しては結果を返すけど、結果を返せない引数もあるような中途半端な関数」です

とのこと。

とりあえず、Bootcampの例を見ていこう。

// このセルのめんどくささを軽減するヘルパー関数
def printAndAssert(cmd: String, result: Boolean, expected: Boolean): Unit = {
  println(s"$cmd = $result")
  assert(result == expected)
}

// -1, 2, 5, 他のための定義
val partialFunc1: PartialFunction[Int, String] = {
  case i if (i + 1) % 3 == 0 => "Something"
}
printAndAssert("partialFunc1.isDefinedAt(2)", partialFunc1.isDefinedAt(2), true)
printAndAssert("partialFunc1.isDefinedAt(5)", partialFunc1.isDefinedAt(5), true)
printAndAssert("partialFunc1.isDefinedAt(1)", partialFunc1.isDefinedAt(1), false)
printAndAssert("partialFunc1.isDefinedAt(0)", partialFunc1.isDefinedAt(0), false)
println(s"partialFunc1(2) = ${partialFunc1(2)}")
try {
  println(partialFunc1(0))
} catch {
  case e: scala.MatchError => println("partialFunc1(0) = can't apply PartialFunctions where they are not defined")
}

// 1, 4, 7, 他のための定義
val partialFunc2: PartialFunction[Int, String] = {
  case i if (i + 2) % 3 == 0 => "Something else"
}
printAndAssert("partialFunc2.isDefinedAt(1)", partialFunc2.isDefinedAt(1), true)
printAndAssert("partialFunc2.isDefinedAt(0)", partialFunc2.isDefinedAt(0), false)
println(s"partialFunc2(1) = ${partialFunc2(1)}")
try {
  println(partialFunc2(0))
} catch {
  case e: scala.MatchError => println("partialFunc2(0) = can't apply PartialFunctions where they are not defined")
}

val partialFunc3 = partialFunc1 orElse partialFunc2
printAndAssert("partialFunc3.isDefinedAt(0)", partialFunc3.isDefinedAt(0), false)
printAndAssert("partialFunc3.isDefinedAt(1)", partialFunc3.isDefinedAt(1), true)
printAndAssert("partialFunc3.isDefinedAt(2)", partialFunc3.isDefinedAt(2), true)
printAndAssert("partialFunc3.isDefinedAt(3)", partialFunc3.isDefinedAt(3), false)
println(s"partialFunc3(1) = ${partialFunc3(1)}")
println(s"partialFunc3(2) = ${partialFunc3(2)}")
  • 実行結果 とりあえず先に実行結果。 以下のようになります。
partialFunc1.isDefinedAt(2) = true
partialFunc1.isDefinedAt(5) = true
partialFunc1.isDefinedAt(1) = false
partialFunc1.isDefinedAt(0) = false
partialFunc1(2) = Something
partialFunc1(0) = can't apply PartialFunctions where they are not defined
partialFunc2.isDefinedAt(1) = true
partialFunc2.isDefinedAt(0) = false
partialFunc2(1) = Something else
partialFunc2(0) = can't apply PartialFunctions where they are not defined
partialFunc3.isDefinedAt(0) = false
partialFunc3.isDefinedAt(1) = true
partialFunc3.isDefinedAt(2) = true
partialFunc3.isDefinedAt(3) = false
partialFunc3(1) = Something else
partialFunc3(2) = Something

ざっくりまとめると

  • partialFunc1は(i + 1) % 3 == 0という数式に当てはまらない数を入れると値を返さないのでisDefinedAtfalseを返す
  • partialFunc2は(i + 2) % 3 == 0という数式に当てはまらない数を入れると値を返さないのでisDefinedAtfalseを返す
  • partialFunc3はpartialFunc1もしくはpartialFunc2に当てはまらないとfalseになる。

上記の"partialFunc"と名のついたものが部分関数になる。 中身は何らかのcaseを使ったマッチ処理が存在していて、なおかつ、それにマッチしない場合は値が戻らないような作りになっているものだ。

val partialFunc1: PartialFunction[Int, String] = {
  case i if (i + 1) % 3 == 0 => "Something"
}

で、これだけじゃなくてorElseを使うと、関数同士を合成して新しい関数も作れる、、と。

val partialFunc3 = partialFunc1 orElse partialFunc2

関数を合成できるってのは面白い。 先に示したリンク先にあるMapの処理みたいにいろいろ使い途はありそうなので、いろいろ試してみる価値がありそう。

とりあえず今日はここまで。