前回はChisel-Bootcampの学習に戻りModule3.6を見ていった。
今回も引き続きModule3.6を進めていく。今日はScalaのunapply
だ。
ジェネレータ:型について
unapply
前回で見てきたようにScalaのmatch
は値や型などさまざまものにマッチさせることが出来る。
ケースクラスを使う場合には、以下のような使い方も可能だ。
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の説明によると、、
Scalaの
unapply
メソッドは別形態の構文糖でmatch
文に型によるマッチとマッチした型の値を変数に展開するという2つの機能を与える
とある。
とりあえず例で見てみよう。 因みにこの例、Bootcampのコードだと何故か 自分の使ってたBootcampのデータが古かっただけでした。。ということで今のでデータで更新(ほぼ同じだったけど)SomeGeneratorParameters
が定義されてないので、適当に追加したものを使っている。
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"の直訳。なんやねん、それってなったので、調べてみたら以下の記事がとっても分かりやすかったのでそちらを見てもらった方がいいかも。
上記の記事から引用させてもらうと、
これはなにかっていうと「特定の引数に対しては結果を返すけど、結果を返せない引数もあるような中途半端な関数」です
とのこと。
とりあえず、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
という数式に当てはまらない数を入れると値を返さないのでisDefinedAt
がfalse
を返す - partialFunc2は
(i + 2) % 3 == 0
という数式に当てはまらない数を入れると値を返さないのでisDefinedAt
がfalse
を返す - 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
の処理みたいにいろいろ使い途はありそうなので、いろいろ試してみる価値がありそう。
とりあえず今日はここまで。