Scala

Scala のラムダ式は trait に自動変換できる

More than 1 year has passed since last update.

ぼくが昨日知った Scala の変態記法をお知らせします。

scala> trait A { def greeting(name: String): String }
defined trait A

// これは普通
scala> val a1: A = new A { def greeting(name: String): String = "Hello, " + name }
a1: A = $anon$1@5c0f79f0

scala> a1.greeting("Serval")
res0: String = Hello, Serval

// !!!?!?!?!??!?!??!!?!?
scala> val a2: A = { name: String => "Hello, " + name }
a2: A = $$Lambda$1050/812031404@629adce

scala> a2.greeting("Arai-san")
res1: String = Hello, Arai-san

// なにこれ!すごーい!
scala> val a3: A = "Hello, " + _
a3: A = $$Lambda$1051/777970377@55a609dd

scala> a3.greeting("Fennec")
res2: String = Hello, Fennec

abstract method が一つだけ定義された trait は、ラムダ式から変換できるらしい。 @xuwei_k 先生曰く、2.12 から入った機能とのこと。 SAM conversion と呼ばれている。

SAM Conversion

SAM (single abstract method) conversion の仕様は言語仕様に記述されている。適当に訳してみる。

http://www.scala-lang.org/files/archive/spec/2.12/06-expressions.html#sam-conversion

SAM conversion

(T1, ..., TN) => T の式 (p1, ..., pN) => body は以下を満たすとき、期待される型 S に SAM変換可能である。

  • S のクラス C がシグネチャ (p1: A1, ..., pN: AN): R の抽象メソッド m を一つ宣言している
  • Cm の他に抽象値メンバーを宣言・継承していない
  • S のサブタイプ U であって、式 new U { final def m(p1: A1, ..., pN: AN): R = body } が正しく型付けされる(期待される型 S に合致する)こと
  • スコーピングのために、 m が static メンバーとして扱われること(U のメンバーが body のスコープ内に現れないこと)
  • (A1, ..., AN) => R(T1, ..., TN) => T のサブタイプであること(この条件を満たすことで S の未知の型パラメータの型推論を行う)

SAM の対象となる関数リテラルは、上記のインスタンス生成式にコンパイルされるとは限らないことに注意せよ。これはプラットフォーム依存である。

ここから以下のことが言える。

  • もしクラス C がコンストラクタを定義しているなら、それはアクセス可能であり、唯一の空の引数リストを定義しなければならない
  • Cfinalsealed であってはならない(単純のため、SAM変換がその sealed class と同じコンパイル単位で行えるかどうかを問わないことにする)
  • m はポリモーフィックであってはならない
  • C の未知の型パラメータの推論を行うことで、 S から完全型 U を導出できなくてはならない

最後に、いくつかの実装上の都合による制約を課す(これらは将来のリリースで撤廃されるかもしれない)。

  • C はネストしていたり、ローカルであってはならない(0引数コンストラクタとなるよう、環境をキャプチャしてはならない)
  • C のコンストラクタは implicit 引数リストを持ってはならない(これは型推論を簡略化する)
  • C は自分型を宣言してはならない(これは型推論を簡略化する)
  • C@specialized であってはならない

抽象メソッドが一つだけであれば、他にメソッドが生えていてもいいらしい。抽象メソッドの引数も1つである必要もない。

scala> trait B {
     |   def greeting(place: String, species: String, name: String): String
     |   def speak: String = "みゃみゃみゃみゃみゃ"
     | }
defined trait B

scala> val b: B = { (p: String, s: String, n: String) => s"ここは${p}!わたしは${s}の${n}だよ"}
b: B = $anonfun$1@ed2f2f6

scala> b.greeting("ジャパリパーク", "サーバルキャット", "サーバル")
res7: String = ここはジャパリパークわたしはサーバルキャットのサーバルだよ

scala> b.speak
res8: String = みゃみゃみゃみゃみゃ

しかし、SAM変換可能なのは関数リテラル (p1, ..., pN) => body だけであることに注意。一般の FunctionN インスタンスをSAM変換することはできない。

scala> trait C { def speak(): String }
defined trait C

scala> val c1: C = { () => "アライさんに任せるのだ!" }
c1: C = $$Lambda$1135/2085013955@7197b07f

scala> val fn: Function0[String] = { () => "ふえぇぇぇぇぇ!?" }
fn: () => String = $$Lambda$1136/1695195255@7ce29a2d

scala> val c2: C = fn
<console>:13: error: type mismatch;
 found   : () => String
 required: C
       val c2: C = fn

リリースノートによれば、これは Java 8 のライブラリを便利に呼び出すのに使えるそうだ。SAM変換によって、 Runnable のインスタンスを簡単に作ることができる。

参考文献