Scalaプログラマレベル(アプリケーション/ライブラリ)[翻訳]
Scalaの難易度とか学習曲線について、最近また議論されてるようですが、公式にはこのようなレベル分けになってますよ。
アプリケーションプログラマとライブラリプログラマに別れてます。アプリケーションプログラマLv2とライブラリプログラマLv1が大体同じくらいの習熟度だそうです。
なんか、これ見ると中級アプリケーションプログラマくらいならすぐになれますよね。
Level A1: 初級アプリケーションプログラマ(Beginning application programmer)
Level A2: 中級アプリケーションプログラマ(Intermediate application programmer)
Level A3: 特級アプリケーションプログラマ(Expert application programmer)
- 畳み込み関数(fold)。例えば、foldLeft, foldRightなど
- Streamとその他遅延データ構造を適切に扱える
- Actors
- パーサコンビネータ
Level L1: 下級ライブラリ設計者(Junior library designer0
- 型パラメータ
- Trait
- 遅延評価(lazy val)
- カリー化を利用した抽象化された制御構造
- 名前渡しパラメータ(By-name parameters)
Level L2: 上級ライブラリ設計者(Senior library designer)
Level L3: 特級イブラリ設計者(Expert library designer)
- 事前定義(Early initializers)
- 抽象型(Abstract types)
- 暗黙の定義(implicit conversion/implicit parameter)
- 高階型引数(Higher-kinded types)
ほとんど使われていないマニアックな機能「事前定義 (Early Definitions)」- Scala Advent Calendar jp 2011 Day 5
このエントリは Scala Advent Calendar jp 2011 の5日目です。
Scalaやってる人なら一度はScala言語仕様に目を通したことがあると思います無いとは言わせない。
この言語仕様を見ると、たまに思いもよらない発見があったりしますが、その中でほとんど利用されているところを見たことがない不遇な機能「5.1.6 事前定義 (Early Definitions)」について書こうと思います*1。
事前定義 (Early Definitions)とは?
テンプレートを事前フィールド定義(early field definition)節で始めることができ、それにより スーパー型のコンストラクタがコールされる前に、ある特定のフィールド値を定義できます。
次のテンプレート中で{ val p1 : T1 = e1 ...
val pn : Tn = en
} with sc with mt1 with mtn {stats}p1, ..., pn 定義の最初のパターンは事前定義(early difinition)と呼ばれます。 それらはテンプ レートの一部をなすフィールドを定義します。 すべての事前定義は、少なくとも 1 つの変数を 定義していなくてはなりません。
Scala言語仕様
つまり、クラス宣言で、withなどでtraitを宣言する前に、特殊な初期化ブロックを記述することが許されている、というワケですが、 なんだかわかりませんね。実例をみましょう。
よくあるoverride and lazy val問題
ある程度Scalaやるとハマる問題の一つとして、スーパークラスの初期化ブロックから参照されているフィールド、メソッドを初期化ブロック内でオーバーライドするとnullになってしまう問題があります。実例を見ましょう。
trait Train { val announce:String println( announce ) } class KQ extends Train { val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!" }
Trainトレイトは、初期化ブロック内で抽象メンバーannounceをprintlnで出力します。クラスKQは、Trainトレイトを継承してannounceにあの言葉を設定します。これで、KQクラスをnewすると、みんな大好きなあの言葉が出力されるハズです。やってみましょう*2。
scala> :paste // Entering paste mode (ctrl-D to finish) trait Train { val announce:String println( announce ) } class KQ extends Train { val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!" } // Exiting paste mode, now interpreting. defined trait Train defined class KQ scala> new KQ null res3: KQ = KQ@48bc9f58
アレ、nullになっとるやん。ダァがシエリィエさないじゃないですかー!?なぜぜ?
これは、継承先のTrainの初期化ブロックが呼ばれた後に、KQの初期化ブロックが呼ばれるからです。この問題を解決するには、annouceをlazy valにすれば解決できます。
class KQ extends Train { lazy val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!" }
lazy valにしてもよいのですが、オーバーライドするメンバーがすでに継承先で具体的な値を与えられている場合は、lazy valでオーバーライドし直すことはできません。\(^o^)/
scala> :paste // Entering paste mode (ctrl-D to finish) trait Train { val announce:String = "" println( announce ) } class KQ extends Train { override lazy val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!" } // Exiting paste mode, now interpreting. <console>:14: error: overriding value announce in trait Train of type String; lazy value announce cannot override a concrete non-lazy value override lazy val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!"
事前定義で解決する
この問題は、事前定義を利用することでシエリイェッス!することができます。KQの宣言を、このように行います。
class KQ extends { override val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!" } with Train
実行してみましょう。
scala> :paste // Entering paste mode (ctrl-D to finish) trait Train { val announce:String = "" println( announce ) } class KQ extends { override val announce:String = "ダァ!! シエリイェッス!!シエリイェッス!!" } with Train // Exiting paste mode, now interpreting. defined trait Train defined class KQ scala> new KQ ダァ!! シエリイェッス!!シエリイェッス!! res6: KQ = KQ@49164555
ダァ!! シエリイェッス!!シエリイェッス!!
ダァ!! シエリイェッス!!シエリイェッス!!
ダァ!! シエリイェッス!!シエリイェッス!!
ダァ!! シエリイェッス!!シエリイェッス!!
ダァ!! シエリイェッス!!シエリイェッス!!
ダァ!! シエリイェッス!!シエリイェッス!!
ダァ!! シエリイェッス!!シエリイェッス!!
寝てる場合じゃねぇ!(Scala)
Scalaだとコンパイラ騙さなくても言語がサポートする機能でふつーにできるし。
object NoSleep extends App { trait Sleepy[A] { def sleep(n:Long):Unit } implicit val hoge = new Sleepy[Nothing]{ def sleep(n:Long) = println("寝てる場合じゃねぇ!") } def sleep[A:Sleepy] = try { implicitly[Sleepy[A]].sleep(1000) } catch { case ex:InterruptedException => ex.printStackTrace() } sleep }
Scala2.10に文字列中の\{ _ } で式展開する機能が入るかも?
以下の記事で紹介されているが、Scala2.10の実験的な機能として、 このコミットにより、 文字列中の式展開が入るかもしれない。
Algorithmically challenged: String Interpolation on 2.10?
式展開とは、Rubyでは"foo #{hogehoge}"のように文字列中に式を埋め込んで評価した結果を元に文字列を作成すること。2.10のnightly buildを持ってきて、-Xexperimentalオプションを付けて起動すると、試してみることができる。
ozaki@mbp-4 $ scala -Xexperimental Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8 Welcome to Scala version 2.10.0.r25825-b20111013020230 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_26). Type in expressions to have them evaluated. Type :help for more information. scala> val x = 1.1 x: Double = 1.1 scala> println("We have a \{ x ;2.2f}% chance of success") We have a 1.10% chance of success scala> "the random value is \{ util.Random.nextInt }!!" res2: String = the random value is 289196571!!
上記のように、sprintfのオプションを合わせて指定してフォーマットを整えることができるようだ。
他にも、 このGistで紹介されているように、無名関数リテラルのように扱うこともできる。
scala> val f = "Value of \{ _ : String} is \{ _ : Int }" f: (String, Int) => String = <function2> scala> f("foo", 23) res1: String = Value of foo is 23 scala> val g : (String, Int) => String = "Value of \{ _ } is \{ _ }" // Types can be inferred g: (String, Int) => String = <function2> scala> g("bar", 13) res2: String = Value of bar is 13 scala> def show[T](t : T)(f : T => String) = f(t) show: [T](t: T)(f: T => String)String scala> show(23)("> \{ _ } <") // Hole inferred as Int res3: String = > 23 < scala> show("wibble")("> \{ _ } <") // Hole inferred as String res4: String = > wibble <
ただし、現時点では残念ながらヒアドキュメント中の式展開には対応していない。
scala> """ | ( ゚∀゚)o彡°おっぱい!おっぱい! \{ util.Random.nextInt } | """ res7: String = " ( ゚∀゚)o彡°おっぱい!おっぱい! \{ util.Random.nextInt } "
src/compiler/scala/tools/nsc/ast/parser/Scanners.scala のprivate def getMultiLineStringLit()をいじれば対応できそうだけど……。