kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

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で型レベル計算をする際にはほぼ必須のオプションです。

-Xprint: -コンパイラの動作を知りたいときに

scalaでプログラムを書いているときに、「なんでここでコンパイルエラーになるんだ?」という場面に時折遭遇します。-Xprintオプションはそんなときに重宝します。-Xprintオプションによって、コンパイラのあるフェーズ後にプログラムがどのように変換されているかを知ることができます。たとえば、scalac -Xprint:typerとすることで、型付けされた段階でscalacが内部的にどのようなコードに変換しているかがわかります。に書けるフェーズ一覧は、-Xshow-phasesを使うことで知ることができます。

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に関する推論方法が変更になり、このような事が可能になったのです。

コメント編

Scalaのコメントは基本的にJavaのそれと同じなのですが、/* Javaと /* 違って コメントを /* ネスト */ する */ ことができます。*/

他にも色々あるのですが、ちょっと調べるのに疲れたのでとりあえずこの辺で。あとで何か思い出したら追記するかもしれません。


infix operation patternのコードのミスを修正。@thincaさん
ありがとうございます。


Scala 2.8コレクションライブラリ編のコードのミスを修正。@thincaさんありがとうございます。