Scala変態技法最速マスター
Java変態文法最速マスターなんてのがかなりブクマされてるみたいだが、変態さならJavaなんてScalaの足元にも及ばないぜ!!ということで、Scala版を書いてみました。しかし、実はあまり変態ではないかもしれません。元ネタと違って、これを読めば何かがわかる、という意味の実用性はあまり無いでしょう。
implicit conversion編
基本
Scalaのimplicit conversionは使いようによってはScalaの強力な型システムを台無しにしてしまう力を持っています。たとえば、
implicit def string2int(x: String): Int = Integer.parseInt(x)
というimplicit conversionを定義してやると、"300" / 3
が100になるなどというキモい挙動を実現することができます。また、さらにカスタマイズして、16進数や8進数に対応することもできます:
scala> implicit def string2int(x: String): Int = { if(x.startsWith("0x")) Integer.parseInt(x.substring(2), 16) else | if(x.startsWith("0")) Integer.parseInt(x.substring(1), 8) else | Integer.parseInt(x) | } string2int: (x: String)Int scala> "0x64" / 3 res2: Int = 33 scala> "010" / 2 res3: Int = 4
C言語が好きな方は、次のようなimplicit conversionを定義してやることで幸せになれるかもしれません。
scala> object CLike { | implicit def int2Boolean(x: Int): Boolean = x != 0 | implicit def ref2Boolean(x: AnyRef): Boolean = x != null | } defined module CLike scala> import CLike._ import CLike._ scala> { var i = 5; while(i) { println(i); i -= 1 } } 5 4 3 2 1 scala> val map = new java.util.HashMap[String, String] map: java.util.HashMap[String,String] = {} scala> map put ("A", "B") res5: String = null scala> val b = map get "A" b: String = B scala> if(b) println(b) B
また、PHPが好きな方は、次のようなimplicit conversionを定義することで幸せになれるかもしれません。
object PHPLike { implicit def byte2Boolean(x: Byte): Boolean = x != 0 implicit def short2Boolean(x: Short): Boolean = x != 0 implicit def int2Boolean(x: Int): Boolean = x != 0 implicit def long2Boolean(x: Long): Boolean = x != 0 implicit def char2Boolean(x: Char): Boolean = x != 0 implicit def float2Boolean(x: Float): Boolean = x != 0.0f implicit def double2Boolean(x: Double): Boolean = x != 0.0f implicit def ref2Boolean(x: AnyRef): Boolean = x != null implicit def array2Boolean[A](x: Array[A]): Boolean = { ref2Boolean(x) && x.length != 0 } implicit def list2Boolean[A](x: List[A]): Boolean = x != Nil }
scala> :load PHPLike.scala Loading PHPLike.scala... defined module PHPLike scala> import PHPLike._ import PHPLike._ scala> def trueOrFalse(x: Boolean) = x trueOrFalse: (x: Boolean)Boolean scala> trueOrFalse(1) res1: Boolean = true scala> trueOrFalse(1.0f) res2: Boolean = true scala> trueOrFalse(0.0f) res3: Boolean = false scala> trueOrFalse(0.0d) res4: Boolean = false scala> trueOrFalse(0.5d) res5: Boolean = true scala> trueOrFalse('a') res6: Boolean = true scala> trueOrFalse('\0') res7: Boolean = false scala> trueOrFalse(null) //<- list2booleanとarray2booleanが同じ優先度で適用できてしまうためエラー <console>:10: error: type mismatch; found : Null(null) required: Boolean Note that implicit conversions are not applicable because they a both method list2Boolean in object PHPLike of type [A](x: List[ and method array2Boolean in object PHPLike of type [A](x: Array are possible conversion functions from Null(null) to Boolean trueOrFalse(null) ^ scala> val x: String = null x: String = null scala> trueOrFalse(x) // <- 特定の型の変数に入っていれば大丈夫 res9: Boolean = false scala> x = "foo" <console>:9: error: reassignment to val x = "foo" ^ scala> val x: String = "foo" x: String = foo scala> trueOrFalse(x) res10: Boolean = true scala> trueOrFalse(Nil) res11: Boolean = false scala> trueOrFalse(List(1)) res12: Boolean = true scala> trueOrFalse(Array(1)) res13: Boolean = true scala> trueOrFalse(Array()) // <- Array()がArray[Nothing]とみなされて、ref2booleanの方が適用されてしまうため res14: Boolean = true scala> trueOrFalse(Array[Int]()) res15: Boolean = false
specialized implicit conversion
implicit conversionは上のような例を別にすれば、一般的には、既存のクラスにメソッドを追加したように見せかけるために使われることが多い機能ですが、このとき、特定の型パラメータを持つ型のみにメソッドを追加することができます。たとえば、以下のようにすることで、Int型を要素に持つListにのみaverageメソッドを追加することができます。
scala> implicit def extendIntList(x: List[Int]) = new { | def average: Double = (0.0/:x)(_+_) / x.size | } extendIntList: (x: List[Int])java.lang.Object{def average: Double} scala> List(1, 2, 3, 4).average res13: Double = 2.5 scala> List("A", "B", "C").average <console>:19: error: value average is not a member of List[java.lang.String] List("A", "B", "C").average
call-by-name implicit conversion
call-by-name implicit conversionというのは今さっき考えた適当な名前です。Scalaでは、implicit conversionの引数自体をcall-by-nameにすることで、メソッド呼び出しのレシーバの評価を遅延することができます。これを利用することで、以下のように、Rubyの後置制御構文のようなものもユーザが定義することができます:
scala> implicit def addPostfixWhile[A](x: => A) = new { | def while_(cond: => Boolean): Unit = while(cond) x | } addPostfixWhile: [A](x: => A)java.lang.Object{def while_(cond: => Boolean): Unit } scala> var i = 10 i: Int = 10 scala> { println(i); i -= 1 } while_(i >= 0) 10 9 8 7 6 5 4 3 2 1 0
infix notation編
infix type notation
Scalaでは2つの型パラメータを持つジェネリックな型は中置記法で書くことができるというシンタックスシュガーが導入されています。これを使うと、たとえばList型を次のように表記できます。[]
が減った分ちょっとかっこいい、かもしれません。
scala> type %[G[_], A] = G[A] defined type alias $percent scala> val x: List%String = List() x: %[List,String] = List()
また、Map型は2つの型パラメータを取るので、次のように書くことができます。
scala> val map: Int Map (Int Map Int) = Map()
map: Map[Int,Map[Int,Int]] = Map()
ちなみに、型名の最後に:が付くと右結合になるので、Mapの別名として=>:などを導入すると、なんかMap型が関数っぽく見えて嬉しい、かもしれません。
scala> type =>: [K, V] = Map[K, V] defined type alias $eq$greater$colon scala> val map: Int =>: Int =>: Int = Map() map: =>:[Int,=>:[Int,Int]] = Map()
infix operation pattern
Scalaで次のような算術式の構文木を処理することを考えてみます:
abstract sealed class Node case class Add(lhs: Node, rhs: Node) extends Node case class Sub(lhs: Node, rhs: Node) extends Node case class Mul(lhs: Node, rhs: Node) extends Node case class Div(lhs: Node, rhs: Node) extends Node case class Num(value: Int) extends Node
このようなとき、通常は、
node match { case Add(lhs, rhs) => ... case Sub(lhs, rhs) => ... case Mul(lhs, rhs) => ... case Div(lhs, rhs) => ... case Num(value) => }
のように書くことができますが、なんかダサくないでしょうか?Scalaではこのような場合、次のようにパターンマッチを書くことができます。
node match { case lhs Add rhs => ... case lhs Sub rhs => ... case lhs Mul rhs => ... case lhs Div rhs => ... case Num(value) => }
さらに、構文木のノードの名前を、%+,%-,%*,%/のように変えてやれば…
node match { case lhs %+ rhs => ... case lhs %- rhs => ... case lhs %* rhs => ... case lhs %/ rhs => ... case Num(value) => ... }
数式に対してパターンマッチしているという気分がより強く出せているのではないでしょうか(?)
type alias編
annotation alias
Java Beansとしても使えるようにしたいクラスを作りたいとき、Scalaでは@BeanPropertyアノテーションを使って、次のように記述できます:
scala> class Point(@BeanProperty var x: Int, @BeanProperty var y: Int) defined class Point
しかし、BeanPropertyってなんか長ったらしくて嫌じゃないでしょーか?そんなとき、Scalaではアノテーションに別名をつけることができます。基本的に、普通の型の別名付けと同様、type宣言を使うだけなので簡単です。ためしに、@beanという別名を付けてみましょう:
scala> type bean = scala.reflect.BeanProperty defined type alias bean scala> class Point(@bean var x: Int, @bean var y: Int) defined class Point
ほら、すっきり(?)
higher-kind alias
名前は今考えました。というか、別に名前はどうでもいいんですけど、なんとなく。
上の方でもちょろっと出てきたのですが、ScalaではGenericsのパラメータとして型だけでなく、型コンストラクタ(≒ジェネリックなクラス)を引数に取ることができます。これを利用すると、型パラメータ一つを取る任意のジェネリックなクラスについて使える次のようなtype aliasが作れます。
type _3D[G[_], E] = G[G[G[E]]]
このtype aliasは次のようにして使います:
scala> val xs: List _3D Int = List() xs: _3D[List,Int] = List() // List[List[List[Int]]] scala> val ys: Array _3D Int = new (Array _3D Int)(3) ys: _3D[Array,Int] = Array(null, null, null) // Array[Array[Array[Int]]]
コンパイラオプション編
-Xsource-reader -ある種のリーダーマクロ-
scalacのコンパイラオプションには-Xsource-readerというオプションがあり、これを使うとソースを読み込む際のリーダーを乗っ取ることができてしまいます。これを使えば何でもありで、プリプロセッサマクロのようにシンボルを適当なものに置き換えたり、scalaの構文を変えてしまうなども自由にできます。たとえば、以下のHelloWorldSourceReaderはあらゆるソースファイルをhello, world!を出力するMyHelloWorld objectに変えてしまいます。
package hello import scala.tools.nsc.io.SourceReader import java.nio.charset.CharsetDecoder import scala.tools.nsc.reporters._ class HelloWorldSourceReader(decoder: CharsetDecoder, reporter: Reporter) extends SourceReader(decoder, reporter) { def this(){ this(null, null) } override def read(file: java.io.File): Array[Char] = { """ object MyHelloWorld { def main(args: Array[String]) { println("Hello, World!") } } """.toCharArray } }
これを使うには、コンパイルした結果を適当なjarファイルに固めて、SCALA_HOME/libディレクトリに放り込んだ上で、
scalac -Xsource-reader hello.HelloWorldSourceReader Hoge.scala
のようにすればOKです。
-Yrecursion -型レベルプログラミングのおともに-
このオプションを指定することで、型が再帰的に展開されるような場合に、再帰を何段まで許すかを指定することができます(ここに関しては厳密には正しく無いかもしれないので、識者のツッコミをお待ちしております)。普段は使う機会がありませんが、Scalaで型レベル計算をする際にはほぼ必須のオプションです。
Scala 2.8コレクションライブラリ編
Scala 2.8ではコレクションライブラリが全面的に書き直され、大幅に利便性が上がるとともに、変態的な挙動も追加されました。たとえば、次のようなMapの宣言があったとします。
scala> val map = Map("x" -> 1, "y" -> 2) map: scala.collection.immutable.Map[java.lang.String,Int] = Map(x -> 1, y -> 2)
このようなmapに対してmapを行ってみます。
scala> map map { case (k, v) => (v, k) } res0: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> x, 2 -> y)
…Mapが返ってきました。まあ、これはいいとします。では、mapに渡す関数がペアの内片方の値しか返さない場合どうなるでしょうか?
scala> map map { case (k, v) => k } res1: scala.collection.immutable.Iterable[java.lang.String] = List(x, y)
何故かIterableに変わってます。このような不思議な挙動は一体どのようにして実現されているのでしょうか?とりあえず、Map#mapのドキュメントを見てみます。
def map[B, That](f: ( (A, B) ) ⇒ B)(implicit bf: CanBuildFrom[Map[A, B], B, That]): That
Builds a new collection by applying a function to all elements of this map
メソッドの型パラメータがThatで返り値の型もThatってなってますが、Thatって何だよThatって。メソッドに明示的に渡した引数のどこにもThatの型は使われていないのに、一体どうやってThatは推論されたのでしょうか?つーか、こんなん見てわかるわけないですよね。というわけで、もうちょっとシンプルな例で説明してみます。まず、準備として次のようなコードを用意します。
ここで、Mapping[A, B]はA型からB型へ変換を行う方法を提供するtraitです。
object ImplicitMagic { trait Mapping[A, B] { def apply(a: A): B } implicit object Int2StringMapping extends Mapping[Int, String] { def apply(a: Int): String = (a + 1).toString } implicit object Str2TupleMapping extends Mapping[String, (String, String)] { def apply(a: String): (String, String) = (a, a) } }
このImplicitMagicをimportした、次のようなコードを作成します。
import ImplicitMagic._ object ImplicitUser { def convert[A, That](a: A)(implicit m: Mapping[A, That]): That = m(a) }
このコードを実行してみます。
scala> :load ImplicitMagic.scala Loading ImplicitMagic.scala... defined module ImplicitMagic scala> :load ImplicitUser.scala Loading ImplicitUser.scala... import ImplicitMagic._ defined module ImplicitUser scala> ImplicitUser.convert(1) res0: String = 2 scala> ImplicitUser.convert("A") res1: (String, String) = (A,A)
ImplicitMagicの中に定義したInt2StringMappingやStr2TupleMappingがimplicitな引数としてconvertにわたされていることがわかります。ここで、convertの呼び出し時には、Mappingが返す型であるThatが明示的に渡されていないことに注意してください。scala 2.8のコンパイラはこの時点で、Mapping[Int, ?]やMapping[String, ?]がimplicitなパラメータとして期待されていることしかわかりませんが、implicitな値(Int2StringMappingやStr2TupleMapping)がそれぞれMapping[Int, String],Mapping[String, (String, String)]であることから、Thatの型をそれぞれString, (String, String)と推論することができるわけです。
実は、scala 2.7までのscalacはこのような場合にimplicit parameterの型をうまく推論することができず、単にThat = Nothingとなっていたのですが、scala 2.8ではimplicitに関する推論方法が変更になり、このような事が可能になったのです。