kmizuの日記

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

Re:不満の記録

flagir を作るなどの過程で感じた Scala への不満を列挙。こんな記事を (日本語で) 公開しても誰も得しないし誰も読まないだろうから、やめようかなとも思ったんだけど。ぼくの勘違いなら指摘してもらえるかもしれないし、記録という意味もあるのでやっぱり公開。

http://d.hatena.ne.jp/ku-ma-me/20090614/p3

概ねもっともな不満だと思います。というわけで、それほど反論があるわけではないのですが、少しの間違いの指摘と、どういう理由があってそのような挙動になってるか、などの説明をさせていただきます。

束縛前の val 変数を参照できてしまった。かなりはまった。これは Scala のバグと言っていいと思う *1 。どう直すかというとなかなか難しいけれど。

object T {
  val foo = setup_foo
  val bar = 1

  def setup_foo = {
    bar // これが実行される時点では bar が 1 に束縛されていない
  }

  def main(args: Array[String]) {
    println(foo)  // 0 が出力される
  }
}

仕様かどうかという意味でなら、これはバグではなく、(少なくとも現在のScalaにおいては)仕様通りの挙動です(仕様のバグじゃないの?という話はあると思いますし、近い将来、この挙動が変更になる可能性もあると思います)。この辺とかこの辺とかに関連する議論があります(他にも色々あります)。とりあえず、誤って未初期化の変数を参照するバグを避けたいのならば、-Xcheckinitオプションを使うのが良いかと思います。実行時に未初期化の変数を参照しようとすると例外を投げてくれます。あと、引用元のコードを意図通りに動作させたい場合、lazy valで初期化を遅延させると良いです。

object T {
  lazy val foo = setup_foo
  lazy val bar = 1

  def setup_foo = {
    bar 
  }

  def main(args: Array[String]) {
    println(foo)  // 1 が出力される
  }
}

空行があるかないかで意味が変わって、はまった。ちょっとスコープを作ろうと思っただけなのに *2 。

foo = 0
{ // syntax error
  val bar = 1
}
foo = 0

{ // OK
  val bar = 1
}

このコードだと、どちらの場合もコンパイルエラーになるはずです。というのは、ブロックの中が宣言文で終わるのはScalaでは許されないので(必ず式で終了しなければならない)。var bar = 1の下に適当な式を入れると、おっしゃる通りの挙動になりますが、これは、前者の場合、foo = 0({ ... })(0に対するapplyメソッドの呼び出し)という意味に解釈されてしまうためです。何故こういう解釈をするようになってるのかまではわかりませんが、たとえば、

list.foreach
{x =>
}

みたいなのを許したかったのではないかと。

val で複数の変数が定義しにくい。

val x = 1, y = 1    // syntax error
val (x, y) = (1, 1) // 実際に Tuple を作るので、コアループでは使いたくない

val x, y, z = 1     // x == y == z == 1 になる。こんなの誰がうれしいんだ
val x@(y@z) = 1     // どうしても上の動作がしたいならこれでいいと思うんだが

valで複数の変数が定義しにくいというのは全くその通りで、個人的にはコンパイラが多少頑張って、上記のようなタプルパターンを使った代入を最適化してくれる方向が望ましいのかなと思っています。あと、val x, y, z = 1で何が嬉しいのかという点ですが、右辺が副作用を持つときのための構文なので、右辺が定数の場合にちっとも嬉しく無いのは仕様です。実際にどのような場面で嬉しいのかというと、たとえば、scala.Enumerationがいい例になっています。scala.Enumerationは(case objectなどを使うよりも簡単な方法で)列挙型を定義するためのライブラリで、

object Color extends Enumeration {
  val R, G, B = Value
}
def display(c: Color.Value) = c match {
  case Color.R => println("RED")
  case Color.G => println("GREEN")
  case Color.B => println("BLUE")
}
display(Color.R)

のようにして使います。object Colorの右辺でValueという式が出てきますが、これは、Enumerationのメソッドで、呼ばれる度に異なる値を返します。

配列リテラルやリストリテラルでいちいち Array や List と打たなきゃいけない。xml リテラルなんぞ入れる前に必要なリテラルがあるだろう。

ここは、Rubyなどをメインで使っておられる方からすると確かに気に入らないところかもしれません。個人的には、本当にめんどうなら、以下のように適当な1文字の別名付けるとか、やりようはあるので、あんまり気にならないですが。

import scala.{Array => A}
A(1, 2, 3, 4, 5)

あと、Scalaの場合、標準のコレクションの種類がかなり豊富なので、ArrayとListだけ特別扱いとかしても、他のコレクション(例えばSet)にリテラルが使えないのでは、あんまり嬉しく無いというのもあります。

1 つくらいよかったところも。placeholder としての _ はやはり便利。多重配列のマップでいちいち変数名考えなくていいのがうれしい。

この辺の仕様は、他の関数型言語も採用してくれないかなーなどと時々思ったりします。カリー化とか言ったってどうせ前からしか部分適用できないわけですし。

型推論が弱すぎて頼りない。new Array(n) だけ書くと Array[Nothing] と推論するので何かと推論失敗する。Array 使うなってことですか。

基本的に、Arrayをexplicitにnewするときは、new Array[TypeName](n)みたいに書くべき、ということになるかと思います(受け取る変数はローカル変数なら型書かなくて良いわけですし)。で、型推論が弱いというのはその通りですが、じゃあ、new Array(n)にどういう型付ければいいのか、というのはScalaみたいな型システムの言語だと結構悩ましい所なのではないかと(方法はもちろんあるでしょうけど、それがユーザの意図通りなのかという点において)。

パターンマッチが型安全じゃない。asInstanceOf を使わなくても実行時に stuck することがある。要するに Obj.magic が自分で定義できちゃう。警告されるとは言え、かなりかっこ悪い。理由は (想像通りなら) JavaVM の仕様のせいなんだろうけど。

ご想像の通り、JavaVMの仕様のせい(正確には、Genericsの実装に基づく制限)で、たとえば、

(List(v) match { case x:List[A] => x }).head

の場合、コンパイル時に型パラメータAが消去されて、実際には

(List(v) match { case x:List => x }).head

のようになります(後者のコードはコンパイルは通らないですけど、イメージとして)。ただ、case x:Type形式の、型にマッチするパターン(かつ、TypeがGeneric型)さえ使わなければ基本的に安全なので、実用上注意しなければならない箇所は多く無いと思います。

便利メソッドがなさすぎて息苦しい。sort 、flatten 、each_cons 、 each_slice あたりは普通に欲しい。あと可変長配列ってないのかな。Java の Vector を直接使えって?

sortはscala.util.Sorting(object)に、flattenはscala.List(型じゃなくてobjectの方)に、そのまんまのがあります。each_cons,each_slice相当は今のところ無いですね。可変長配列に関しては、scala.collection.mutable.ArrayBufferか、scala.collection.mutable.ListBufferを使うのが基本になるかと思います。

オブジェクトを作るとみるみる遅くなる。直接の原因は JavaVM なんだろうけど、Scala 側も実用向けの細かい最適化はまだまだ? リフレクションとかの関係でアグレッシブな最適化はできないんだっけ?

この辺は、Scalaだと最適化を頑張るにしても限界があるので、むしろJVMの方の頑張りに期待する方向になるのではないかと。

追記:flatten,sort,ArrayBuffer,ListBufferなどの使用例。

scala> import scala.util.Sorting._
import scala.util.Sorting._

scala> val arr = Array("C", "B", "A")
arr: Array[java.lang.String] = Array(C, B, A)

scala> stableSort(arr)

scala> arr
res1: Array[java.lang.String] = Array(A, B, C)

scala> stableSort(arr, (x: String, y: String) => x > y)

scala> arr
res3: Array[java.lang.String] = Array(C, B, A)

scala> List("C", "B", "A").sort{(x, y) => x < y}
res4: List[java.lang.String] = List(A, B, C)

scala> List.flatten(List(List(1, 2, 3), List(4, 5, 6)))
res5: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> List(List(1, 2, 3), List(4, 5, 6)).flatten[Int]
res6: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import scala.collection.mutable.{ArrayBuffer, ListBuffer}

scala> val abuf = new ArrayBuffer[String]
abuf: scala.collection.mutable.ArrayBuffer[String] = ArrayBuffer()

scala> abuf append ("A", "B", "C", "D")

scala> abuf
res8: scala.collection.mutable.ArrayBuffer[String] = ArrayBuffer(A, B, C, D)

scala> val lbuf = new ListBuffer[String]
lbuf: scala.collection.mutable.ListBuffer[String] = ListBuffer()

scala> lbuf append ("A", "B", "C", "D")

scala> lbuf
res10: scala.collection.mutable.ListBuffer[String] = ListBuffer(A, B, C, D)