kmizuの日記

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

Weak Conformanceのはなし

この記事はPlay or Scala Advent Calendar 2012 の 6日目です。今回の記事は、とりわけ役に立たないことを自負しています(役に立つ記事を読みたい人は飛ばしてください)。

本題に入る前に、conformanceという概念についてざくっと簡単に説明しておきます。これは、(Scalaの言語仕様では)サブタイプ関係とだいたい同じです。詳しくは、Scala Language Specification 3.5.2 を参照してください。

さて、このconformanceに関係した概念としてScala 2.8ではweak conformanceというものが導入されました。というと、何やら難しく聞こえますね。でも、実はとても簡単な話で、単にJavaのプリミティブな数値型相当の型(Byte, Short, Int, Long, Float, Double)同士の関係を、通常のconformanceとは別に定めただけのものです。

Scala Language Specificationから抜粋しますが、weak conformance( <:w)は次のように定義されています。

  • Byte <:w Short
  • Short <:w Int
  • Char <:w Int
  • Int <:w Long
  • Long <:w Float
  • Float <:w Double

さて、これを見るとわかりますが、Javaのプリミティブ型同士の代入互換性に関する規則と酷似しています。なんでこのような規則をわざわざ導入したのでしょうか。

実は、Scala 2.7まではJavaのプリミティブ型相当の型同士の変換はimplicit conversionを使ってPredefに定義されていました。言語仕様上、これらの型同士の互換性については特別に定義していなかったわけです。これは、Scalaがプリミティブ型を排除した事を考えれば妥当な判断と言えるでしょう。

Scala 2.8で再びこのような「プリミティブ型」を特別扱いするような規則を入れたのは一体何故なのでしょうか。一言でいうと、「Scala Language Specification Changes in Version 2.8を読め」で終わります。でも、それで終わっても仕方が無いので説明してみます。

weak conformanceが導入された主な理由は、条件式 (if)やmatch式、try式における型の推論結果をより期待された形にするためです。

具体例として、次のようなif式についてみてみましょう。

if (cond) 1 else 1.0

Scala 2.8未満(Scala 2.7.7まで)では、この式の型はAnyValと推論されます。これは、1(Int)と1.0(Double)の間には型としては関係がなく、共通の「最も近い」祖先(ここで、「祖先」はそれぞれの型自身も含むものとする)がAnyValだからです。

一方、Scala 2.8では、この式の型はDoubleと推論されます。これはどういうことかというと、Scala 2.8では条件式

if (cond) expr1 else expr 2

の型は、expr1とexpr2の型のweak least upper boundになるように仕様が変更されたからです。このweak least uppper boundを計算するために、先ほどのweak conformance (<:w)関係が使用されます。weak least upper boundというと何やら難しそうですが、これは単にweak conformance関係をconformance関係に追加したものです。要は、

AnyVal
+ Double
   + Float
      + Long
         + Int
            + Char
            + Short
               + Byte

という継承関係があるものとして、共通の「最も近い」祖先型を探すだけです。さて、

if (cond) 1 else 1.0


をもう一度見直してみましょう。1の型であるIntと1.0の型であるDoubleについて、 Int <:w Doubleが定義されているため、IntとDoubleのweak least upper boundはDoubleになります。

match式やtry式についても同様です。たとえば、以下の式の型は、Scala 2.7.7ではAnyVal、Scala2.8以降ではIntと推論されます。

{
  val a = 1; 
  a match { 
    case 1 => 1
    case 2 => '2'
    case 3 => 3.toShort 
  }
}

ここで注意しておかなければいけないのは、あくまでweak conformanceは「weak」 conformanceであるということです。これはたとえば、以下のように明確なconformance関係を要求する場合、weak conformance関係が定義されていても意味がないということです。

def foo[T <: Double](x: T): T = x
foo(1.0) //OK
foo(1) //inferred type arguments [Int] do not conform to method foo's type parameter bounds [T <: Double]

さて、この記事の最初で役に立たない事を自負していると書いた通り、この記事の内容を理解したからといって、実際のScalaコードを書く際にはほぼ役に立ちません。あえていうなら、Scala 2.7.7をまだ使っている人がScala 2.8以降(実質的にはScala 2.9.Xになるでしょう)を使い始めるときは少し役に立つかもしれません。ですが、ふつーにScalaを使っている範囲で、この仕様の有無を知っていようがいまいがまず問題にならないでしょう。

というわけで、Weak Conformanceのはなしは以上です。

Iterator.continually()を使おう

Scalaの標準IOライブラリであるscala.io(.Source)は非常に腐ってます。読み込みしか対応してない上に、バイト列の読み込みもサポートしてないという代物。しかも、Scala 2.7まではscala.io.Sourceがそのままだとcloseできなかったりとか(今はcloseできます)。さっさと退場して欲しい代替ライブラリが標準になって欲しいところなんですが、現状の情勢をみるにしばらく先になりそうな感じです。

というわけで、Scalaを使うならJavaのIOライブラリとお付き合いしなければいけません。とりあえず、Commons IOとか使うのも良いかもしれませんが、ちょっとしたものを書くときにいちいちCommons IO使うのも面倒くさいです。

じゃあ、BufferedReaderとかをいちいちwhileループで回すのかといえばそれも面倒くさいです。そこで、その苦痛を緩和してくるのが、Iterator.continually()です。使い方は簡単。なんかループしたい処理をIterator.continually()の引数に渡すだけです。試しに標準入力から一行ずつ読み取って、行番号付きで出力する処理を書いてみましょう。

val lines = Iterator.continually(readLine()).takeWhile(_ != null).toList
for ((line, lineNum) <- lines.zipWithIndex) {
  printf("%d:%s%n", lineNum + 1, line)
}

たったこれだけです(もうちょっと短くできますが、それはおいときます)。Iterator.continually()は、引数で与えられた処理を無限回繰り返すIteratorを返してくれるので、呼ぶたびに返り値が変わる入力系のメソッド/関数と相性が良いです。もちろん、無限に読み取られても困るので、どっかでぶった切ってあげる必要があります。それがtakeWhile(_ != null)の部分で、これを入れることによって、readLine()がnullを返した時点で終了するIteratorが返ってきます。後は、それをtoListやらtoSeqでimmutableなシーケンスに変換してあげるだけ。簡単ですね。もちろん、readLine()以外にもこれは応用できます。たとえば、標準入力から1バイトずつ読み込んで配列に変換して、文字列として出力する処理は次のように書くことができます。

val bs = Iterator.continually(System.in.read()).takeWhile(_ != -1).map(_.toByte).toArray
println(new String(bs, "US-ASCII"))


なお、ほとんど同じようなことをしてくれるメソッドにStream.continually()がありますが、メソッドや関数内で完結する処理に対して、あえてこちらを使う必要はないと思います。Iterator.continually() 対 Stream.continually() の議論については、こちらを参照してください。

Jsonda 0.4.0リリース

昨日、Jsonda 0.4.0をリリースしました。先日の日記では、次のバージョンを0.3にするようなことを書いたのですが、リリース版のメジャーバージョンは0.2 -> 0.4 -> 0.6のようにすることにしました。

Jsonda 0.2.1と同じく、Scala 2.9.1とScala 2.9.2用にクロスビルドしています。sonatypeにpublishしているので、以下の一行をbuild.sbtに追加するだけで使えます。

libraryDependencies += "com.github.kmizu" %% "jsonda"  % "0.4.0"

0.4の主な変更点は、Jsonda DSLのコアを特定のJSONライブラリ非依存にしたことです。これによって、lift-json、json4s、scala.util.parsing.jsonのオブジェクトを同じDSLで構築できます。

たとえば、

import com.github.kmizu.jsonda.dsl.LiftJsonDSL._

val person = %{
  'name :- "Kota Mizushima"
  'age :- 29
}

とした場合、lift-jsonのJsonAST.JObjectが返ってきます。また、

import com.github.kmizu.jsonda.dsl.StdJsonDSL._

val person = %{
  'name :- "Kota Mizushima"
  'age :- 29
}

とした場合、scala.util.parsing.jsonのJsonObjectが返ってきます。他にもjson4sにも対応していますが、これは今のところlift-jsonとほぼ同じなので省略。

また、Option型にも対応しました。これによって、

val hoge = %{
  'foo :- Option(100)
  'bar :- None
}

とした場合、

{"hoge":100, "bar":null}

というJSONに相当するオブジェクトが作られるようになりました。

その他には、テストケースの追加、null絡みのバグをいくつか修正しました。Jsondaの開発では、これまでGithubのIssues等のIssue Trackerを使ってなかったせいで直した不具合が曖昧になっていました。これはリリース内容書くときにも不便だということに気づいたので、これからはGithubのIssuesに不具合を登録していきます。

Jsonda 0.2.1 & Jsonda 0.3について

しばらく前ですが、Jsonda 0.2.1をリリースしました。0.2からScala 2.9.1とScala 2.9.2のクロスビルドしているので、どちらでも使えます。

Jsonda 0.2からsonatypeにpublishしてるのでresolversへの追加なしで、以下の一行をbuild.sbtに追加するだけで使えます。

libraryDependencies += "com.github.kmizu" %% "jsonda"  % "0.2.1"

JsondaはScalaでJSONオブジェクトを構築するための汎用DSLライブラリです。Jsondaを使えば簡潔で読み易い記述でJSONオブジェクトを構築できます。以下は例です。

import com.github.kmizu.jsonda.Implicits._

val person = %{
  'name :- "Kota Mizushima"
  'age :- 28
}

不完全ですが、ドキュメントも少しずつ更新しています。

0.2.1では、lift-jsonのみ対応です。現在開発中の0.3では、coreはlift-json非依存になっており、lift-jsonもscala.util.parsing.jsonも同じDSLで取り扱えるようになっています。json4s対応を済ませたら、0.3出そうかと思っています。

Scala基礎勉強会行ってきました

Scala基礎勉強会という勉強会が、先日名古屋で行われたので行ってきました。Scala基礎勉強会というと一見入門勉強会のように見えますが、基礎は基礎でも内容はbasicじゃないのがポイントです。イベントページを見るとわかるかと思いますが、ぶっちゃけて言うとScalaに関する論文を発表者が選んで紹介しよう!というものです。

私は

Capabilities for Uniqueness and Borrowing, Phillip Haller, Martin Odersky, ECOOP'10

という論文を読んで紹介しました。簡単に言うと、

Actorライブラリにおいて、Actor間で安全にメッセージパッシングするにはオブジェクトがimmutableでなければいけないよねー。でも、mutableでもActor間でオブジェクトが共有されなければ問題なっしんぐ。で、そのためにあるオブジェクト群への参照が一つだけしか無いことを保証するための型システムを設計しました。型システムの健全性も証明しました。あと、Scalaのコンパイラプラグインとして実際に実装もしました。

という話です。私による紹介スライドはこちらです。口頭で補足説明する事が前提の資料なのでかなりはしょっていますがご容赦を。参加者の大部分が論文は読み慣れていないだろうと確信していたので、型付け規則とか証明の話には極力触れずに紹介するのにちょっと苦労しました。

他の方の発表や、勉強会の雰囲気についてはTogetterにまとめがあるので、こちらをみるとよいかと。

名古屋はちょっと遠かったですが、論文紹介勉強会は大学院生時代を思い出して、懐かしい気分に浸れました。

ちなみに、会場でも強調したのですが、論文を読むときはまずAbstractを読むのが手っ取り早いです。Abstractは論文全体の内容を短くまとめたものになっているので、それを見て面白そうだったら読むのもよし、つまらなければ放り投げるのもよしだと思います。また、興味のある部分だけ斜め読みしてもいいです。もちろん、研究者にとっては論文のサーベイは仕事の一部ですので、少し事情は違いますが、非研究者の方はもっと気楽に論文を読むといいと思います。特に、コンピュータサイエンスの論文は、Web上で公開されており(一部はACMなどの学会に登録しなければ読めないものもありますが)、無料ですぐにアクセスできることも多いです。

_ と _ の違い

意味不明なタイトルですね。このエントリの目的は、二つの似た_の使い方が全く異なる意味を持っていることを認識してもらうことです。

最初に結論を言います。Scalaの式中における

obj.method _

という使い方(Method Values)と

obj.method(_)

という使い方(Placeholder Syntax for Anonymous Functions)*1

は全く異なるものです。似た場面で使えることがありますが、展開/コンパイル結果は一般に異なります。これが一番重要な点です*2

両者の区別は簡単で、

obj.method _

のように、メソッド名の後に一個以上のスペースに加えて、単独で_が現れたら、間違いなくMethod Valuesです(:の後に型注釈が付くこともありますが、それはオフトピックなのでおいておきます)。
それ以外のケースは基本的にPlaceholder Syntax for Anonymous Functionsです。

この二つのよく似た構文ですが、実際に展開されるタイミングという点で決定的な違いがあります。

Method Valuesは、型チェック(の後)のタイミングで作用し、メソッドを無名関数でラップしてオブジェクト化するための処理をはさみ込みます。Method Valuesは、対象となる式がメソッド型または名前渡しパラメータでなければいけないため、対象となる式の型が条件を満たしているかどうかのチェックが行われ、チェックを通った後に展開が行われます*3

Placholder Syntax for Anonymous Functionsは、構文解析のタイミングで作用し、式中に現れる_を無名関数の仮引数の参照に置き換えて、無名関数に変換するための処理をはさみ込みます。型チェッカは展開結果に全く関係ありません。Placeholder Syntax for Anonymous FunctionsのルールはScala Language Specification p.94に記述されていますが、型に関して言及していないことがわかります。もちろん、その後に型チェックが行われますが、あくまで展開後の無名関数に対して型チェックが行われるのであって、Placeholder Syntax for Anonymous Functionsを使った式に対して型チェックが行われるわけではないことに注意してください。

即興で記事を書いたのでイマイチまとまりが悪いですね。とりあえず参考程度に。

*1:私が主にプレースホルダー構文と呼んでいるもの

*2:ちなみに、全く違う意味を持つものに同じ_という記号を割り当てたのはScalaの仕様が悪いと思います

*3:Method Valuesに関しては、Scala Language Specification p.80を参照してください

プレースホルダー構文クイズ(解答編)

淡々と解答編。

1. =の右辺の展開結果が全く同じになるコード例のグループを全て列挙してください

A.

(1-2, 1-5), (2-1, 2-2), (2-3, 2-4), (3-1, 3-5), (4-3, 4-4)

2. コンパイルできるコード例を全て抽出してください(e.g. 1-2, 1-3)。

A.

1-2, 1-3, 1-5, 2-1, 2-2, 2-3, 2-4, 3-3, 3-4, 4-3, 4-4

3. コンパイルできないコード例を二つ以上抽出し、その理由を簡単に述べてください。

A.

  • 1-1: 右辺の展開結果が($x, $y) => $x + $yのようになり、左辺に$x, $yの型を推測するために必要な型情報が存在しないため。
  • 1-4: 1-1と同じ。
  • 3-1: 右辺の展開結果が($x) => List(1, 2, 3).map($x)のようになり、左辺に$の型を推測するために必要な型情報が存在しないため。
  • 3-2: 右辺の展開結果は($x: Int) => List(1, 2, 3).map( ($x:Int) )のようになる。ここで、mapの引数としてはIntを受け取る関数が必要だが、$xの型はただのIntであり、型が合わない。
  • 3-5: 右辺の展開結果は($x) => List(1, 2, 3).map($x)のようになるが、左辺の型はList[Int]であり、型が合わないため。
  • 4-1: プレースホルダー(_)の外側の式が存在しないため。Scala Language Specification p.94「 Placeholder Syntax for Anonymous Functions」によれば、

Define an underscore section to be an expression of the form _:T where T is a type,
or else of the form _, provided the underscore does not appear as the expression
part of a type ascription _:T .

An expression e of syntactic category Expr binds an underscore section u, if the following two conditions hold: (1) e properly contains u, and (2) there is no other expression of syntactic category Expr which is properly contained in e and which itself
properly contains u.

である。ここで、underscore section _または_:Tを真に含む式eが存在しなければいけないことが記述されている。しかし、4-1では、_を真に含む式が存在しないため、正しいプレースホルダー構文の条件が満たされない。

  • 4-2: 右辺の展開結果は、identity*1となるが、左辺に$xの型を推測するために必要な型情報が存在しないため。

りりろじさんからの指摘を受けて追記。自分で作成した問題のくせに完全に抜けていたorz

4-5: 右辺の展開結果は、identity( ($x) => identity($x) )となるが、左辺に$xの型を推測するために必要な型情報が存在しないため。

とまあ、こんな感じです。需要があれば解説編書きますが、たぶん無い。

*1:$x) => identity($x