SSブログ

C# の using を Scala で [Scala]

C# には using ステートメントと IDisposable インターフェイスというのがあって確実にリソースの後始末を行いたいときに組み合わせて使う(らしいです。私は C# は殆ど知りませんが)。詳しくは [1] を参照してもらうとして、それ Scala で実現できるよ、というのが [2] の記事。

これは暗黙の型変換、structural subtyping、view bounds といった Scala っぽい技法が勢ぞろいなのでそういう意味でかなりかっこいいです。結果的に close メソッドか dispose メソッドがあれば何でも OK になっていると思うので IDisposable を継承しなければならない C# よりも優れているのではないかと思う。

ちなみに最近 F# 1.9.2 に唐突に use というキーワードが導入された [3] のをずっと不思議に思ってたんですが、これって C# からの輸入だったんですね。

[1] http://www.atmarkit.co.jp/fdotnet/dotnettips/027dispose/dispose.html
[2] http://blog.omega-prime.co.uk/2007/08/25/implementing-the-disposable-pattern-in-scala/
[3] http://blogs.msdn.com/dsyme/archive/2007/08/01/introducing-use-bindings-in-f-1-9-2.aspx


ScalaCheck を試す (3) カスタムジェネレータを作る [Scala]

間が空いてしまったけど ScalaCheck を試す続き。

たとえば「任意の整数について」という条件の代わりに「任意の偶数について」など、より限定的な条件を仕様の前提にしたいとする。ひとつの方法は [1] で言及したように ==> を使って前提条件を記述するやりかたがある。でもこの方法だと「任意の整数」を生成した後で前提条件による篩をかけるので無駄が多く、厳しい条件だといつまでたっても有効なテストデータが生成されないということがありうる。

このような場合はカスタムジェネレータを作って最初から有効なデータのみが生成されるようにする。任意の偶数を生成するジェネレータは以下のように書く。

val genEven: Gen[Int] = for (n <- arbitrary[Int]) yield n * 2

for comprehension のジェネレータの部分に ScalaCheck の既存のジェネレータを書くことが出来る。この構文を使ったのはうまいと思う。

ジェネレータを明示的に指定して性質を記述するには forAll を使う。

val propEven = forAll(genEven) (x => x % 2 == 0)

テストの実行はいつもどおり。

scala> check(propEven)
+++ OK, passed 100 tests.
res39: scalacheck.TestStats = TestStats(TestPassed(),100,0)

作成したカスタムジェネレータをその型のデフォルトのジェネレータにするには暗黙の型変換メソッドを書く。以下は Int のデフォルトジェネレータを偶数のみのジェネレータにしてしまう。

scala> check(property((x:Int) => x % 2 == 0))
*** Failed, after 1 successful tests:
The arguments that caused the failure was:
List(-1)

res43: scalacheck.TestStats = TestStats(TestFailed(List(-1)),1,0)

scala> implicit def arbitraryInt(x: Arbitrary[Int]) = genEven
arbitraryInt: (scalacheck.Arbitrary[Int])scalacheck.Gen[Int]

scala> check(property((x:Int) => x % 2 == 0))
+++ OK, passed 100 tests.
res44: scalacheck.TestStats = TestStats(TestPassed(),100,0)

scala> check(property((x:Int) => x % 2 == 0))
+++ OK, passed 100 tests.
res45: scalacheck.TestStats = TestStats(TestPassed(),100,0)

[1] http://blog.so-net.ne.jp/rainyday/2007-08-08-1


MSIL 版 Scala のジェネリクス [Scala]

Java Generics の Reification の章を読み始めている。型の情報がランタイムまで完全に残っている場合それは reifiable な型という風に呼ばれるが、Java では昔からの配列は reifiable だけど generics はそうではなくて例えば List<Integer> は List にされてしまって(これを erasure と呼ぶ)、型パラメタの情報は実行時には利用できない。これはキャストを行ったり実行時にオブジェクトの型についての問い合わせをする場合に意識しなければならない。Java はこのような方針を採用することによってクライアントはレガシーなままライブラリをジェネリックにアップグレードしても相互運用可能であるというような高い後方互換性を実現している。

さて Scala は JVM バイトコードにコンパイルされる都合上、 Java と同様にジェネリクスを erasure によって実現している。そして以下のような case 文では実行時にオブジェクトが何の型であるかという情報を利用する必要があるから上述の問題が表れる。

case class Container[T](v: T)

object TryMe {
  def f(x: Any) = x match {
    case a: Container[Double] => "double"
    case a: Container[Int] => "int"
    case _ => "other"
  }
  def main(args: Array[String]) = {
    Console.println(f(Container(1.23)))
    Console.println(f(Container(1)))
  }
}
PS D:\scala> scalac TryMe.scala
warning: there were unchecked warnings; re-run with -unchecked for details
one warning found
PS D:\scala> scalac -unchecked TryMe.scala
TryMe.scala:5: warning: non variable type-argument Double in type pattern is unchecked since it is eliminated by erasure

    case a: Container[Double] => "double"
            ^
TryMe.scala:6: warning: non variable type-argument Int in type pattern is unchecked since it is eliminated by erasure
    case a: Container[Int] => "int"
            ^
two warnings found
PS D:\scala> scala TryMe
double
double

しかし Java Generics の本によると C# のジェネリクスというのは Java とは違った方針に基づいていて、generic な型も reifiable である、つまり実行時にも型パラメタも含めた型情報が利用できるらしいのだ。

じゃあ Scala の .NET 版はひょっとして?と思うのが当然なので scala-msil を nightly build からとってきて(これのステータスってどういう扱いなんだろうか)実際に試してみた。

PS D:\scala> scalac-net -unchecked TryMe.scala
TryMe.scala:5: warning: non variable type-argument Double in type pattern is unchecked since it is eliminated by erasure

    case a: Container[Double] => "double"
            ^
TryMe.scala:6: warning: non variable type-argument Int in type pattern is unchecked since it is eliminated by erasure
    case a: Container[Int] => "int"
            ^
two warnings found
PS D:\scala> ilasm TryMe.msil > $null
PS D:\scala> ./TryMe.exe
double
double

結果は同じで、Scala は .NET 版でも Java と同じように erasure を行っていたのでした。残念。
まあ動作が違ったら違ったでこんな基本的な構文の結果が違ってもいいのかという突っ込みになっただろうけど。


Scala のコンストラクタ [Scala]

「[Scala][Java] クラスとデフォルトコンストラクタ」 [1] より引用。

class Dimension(width: int, height: int) {
  override def toString: String = "[" + width + "," + height + "]"
}
val size = new Dimension(320, 240)
println(size)
とかすると,勝手にメンバをつくり値を代入してくれる. で,じゃぁメンバのアクセス権限はどうかなと思って,
println(size.width)
println(size.height)
を実行すると,
error: value width is not a member of Dimension
println(size.width)
            ^
error: value height is not a member of Dimension
println(size.height)
            ^
two errors found
ということで隠蔽されている.

殆ど重箱の隅のような話になってしまうけど、これは自動でメンバを作って代入してアクセス制限をかけているのではなくて、「クラスのプライマリコンストラクタの仮引数のスコープはクラス定義全体であり、したがってクラス定義内のメソッドからのみ可視である」という解釈を私は今までしていて、たぶんそちらのほうが正しいと思う。

以下のような例を作ると「メンバでない」と「メンバだがアクセス可能でない」の差がエラーメッセージに現れる。

scala> class A(m: Int)
defined class A

scala> class B(n: Int) { private val m = n }
defined class B

scala> (new A(123)).m
<console>:5: error: value m is not a member of A
  val res0 = (new A(123)).m
                         ^

scala> (new B(123)).m
<console>:5: error: value m cannot be accessed in B
  val res1 = (new B(123)).m
                         ^

逆に私は Scala でクラスのコンストラクタが多重定義できることの方を知らなかった(あまり Scala の文献に現れないし…)。同名オブジェクトの apply メソッドを多重定義するみたいなことをしなければいけないのだと今まで思っていました。

追記: 書いた後で思ったけど、じゃあ case class はどうなっているんだろう。

追記:

仕様書を読んでみるといろいろと(私にとっての)新しい知識が得られた。以下のように仮引数に val とか var とかを付けた場合は本当にメンバである(自動でアクセサを定義してくれる)。

scala> class A(val m: Int)
defined class A

scala> (new A(123)).m
res0: Int = 123

scala> class B(var m:Int)
defined class B

scala> val b = new B(123)
b: B = B@17366cb

scala> b.m
res1: Int = 123

scala> b.m = 456

scala> b.m
res3: Int = 456

そして case class の場合はこの val が暗黙のうちに付与されたものとみなされる。

[1] http://cappuccino.jp/keisuken/logbook/20070817.html#p04


RemoteActor を使ってみた [Scala]

きっかけはどう書く?orgの「分散関数呼び出し」というお題 [1] で、そういえば Scala に scala.actors.remote っていうパッケージあったなと思って使ってみた。

使い方はかなり簡単。これがサーバ。

import scala.actors.Actor._
import scala.actors.remote.RemoteActor._

object RemoteServer {
  def main(args: Array[String]) {
    actor {
      alive(9000)
      register('priceString, self)
      loop {
        react {
          case ('priceString, price: Int, discount: Int) =>
            val result = 
              "販売価格" + (price * (100 - discount) / 100) + "円" +
              "(定価" + price + "円から" + discount + "%引き)"
            reply(result)
          case 'shutdown => exit
        }
      }
    }
  }
}

クライアントからはこんな風に使う。

import scala.actors.remote.RemoteActor._
import scala.actors.remote.Node

val c = select(new Node("127.0.0.1", 9000), 'priceString)
c !? ('priceString, 2000, 100)

しかしパフォーマンスについてはCeleron2.8GHz/768MBメモリの同一マシンで10000回呼び出したところ110秒とかそんな感じの結果だった。どうも見劣りするなあ。

[1] http://ja.doukaku.org/45/


Scala の動的変数で DSL を簡潔にする [Scala]

Ruby のブロックつき open のように勝手に後始末をしてくれる open メソッドを Scala で実装することを考えて見ます。これを素直に書けばこうなります。

import java.io.FileOutputStream

object DisposableFile1 {
  def openOut(path: String)(block: FileOutputStream => Unit) {
    val out = new FileOutputStream(path)
    try {
      block(out)
    } finally {
      out.close()
      // Console.println("closed")
    }
  }
}

これはこのようにして使えます。

scala> import DisposableFile1._
import DisposableFile1._

scala> openOut("d1.txt") { (out:FileOutputStream) =>
     |   out.write("hello".getBytes)
     | }

何か問題はあるでしょうか。特になさそうです。Ruby もこんな感じです。ただ、こうしたブロックの中で操作されるファイルというのは普通 openOut メソッドで開いたファイルに決まっています。何故いちいちブロック内で out 変数を使わなければいけないのでしょうか。

こうした場合役に立つのが Scala で標準装備されている DynamicVariable クラスです(以前書いた記事 [1] ではこれが標準装備だと知らずに自分で似たようなコードを実装していましたが)。これを使うと先のコードは以下のように書き換えられます。

import java.io._
import scala.util.DynamicVariable

object DisposableFile2 extends OutputStream {
  val out = new DynamicVariable[FileOutputStream](null)
  def openOut(path: String)(block: => Unit) {
    val out_ = new FileOutputStream(path)
    try {
      out.withValue(out_) { block }
    } finally {
      out_.close()
      // Console.println("closed")
    }
  }
  def write(b: Int) = out.value.write(b)
  override def write(b: Array[Byte]) = out.value.write(b)
  override def write(b: Array[Byte], off: Int, len: Int) = out.value.write(b, off, len)
  override def flush = out.value.flush
}

使い方はこうです。

scala> import DisposableFile2._
import DisposableFile2._

scala> openOut("d2.txt") {
     |   write("hello".getBytes)
     | }

DisposableFile2 で定義している write メソッドなどは常に DisposableFile2 オブジェクトの out 変数を参照して処理を委譲していますが、この out 変数は DynamicVariable クラスのインスタンスで、その内容(value で取得できる)は動的に変化します。

DynamicVariable クラスの withValue メソッドはある値とブロックをとり、渡された値で動的変数の内容を一時的に改変してブロックを実行します。ブロックの実行が終わると動的変数の値は元に戻ります。つまり Perl の local や Common Lisp の special variable のような動的スコープを実現しているわけです。

ブロック実行後に内容が元に戻るわけなので以下のようなネストも可能です。この場合 d3.txt の内容は "hello world" になります。

openOut("d3.txt") {
  write("hello".getBytes)
  openOut("d4.txt") {
    write("howdy".getBytes)
  }
  write(" world".getBytes)
}

動的変数をうまく使うと Scala で DSL 的なことをする場合に構文を簡潔にすることができると思います。ちなみに Scala の Console オブジェクトの withIn メソッドや withOut メソッドでも内部的に DynamicVariable が使われています。

[1] http://blog.so-net.ne.jp/rainyday/2007-06-17-1


ScalaCheck / QuickCheck を試す (2) [Scala]

前回 [1] の記事を書いた後 ScalaCheck の生成しているテストケースにちょっと疑問を感じたので引き続きいろいろ試してみたらどうにも理不尽な現象に気がついた。

まず「任意の整数は10より小さい」という馬鹿みたいな性質をテストしてみる。

scala> check(property((x:Int) => x < 10))
*** Failed, after 20 successful tests:
The arguments that caused the failure was:
List(20)

res0: scalacheck.TestStats = TestStats(TestFailed(List(20)),20,0)

scala> check(property((x:Int) => x < 10))
*** Failed, after 15 successful tests:
The arguments that caused the failure was:
List(13)

res1: scalacheck.TestStats = TestStats(TestFailed(List(13)),15,0)

scala> check(property((x:Int) => x < 10))
*** Failed, after 12 successful tests:
The arguments that caused the failure was:
List(11)

res2: scalacheck.TestStats = TestStats(TestFailed(List(11)),12,0)

当然テストにパスしない。で、これを「任意の整数は100より小さい」に置き換えてみる。

scala> check(property((x:Int) => x < 100))
+++ OK, passed 100 tests.
res3: scalacheck.TestStats = TestStats(TestPassed(),100,0)

scala> check(property((x:Int) => x < 100))
+++ OK, passed 100 tests.
res4: scalacheck.TestStats = TestStats(TestPassed(),100,0)

scala> check(property((x:Int) => x < 100))
+++ OK, passed 100 tests.
res5: scalacheck.TestStats = TestStats(TestPassed(),100,0)

馬鹿馬鹿しさ加減は変わらないのにこんどはテストにパスするようになってしまった。
最初これは ScalaCheck のバグかと思った(まだ 0.2 だし)のだが、本家の QuickCheck でも同様だった。

Hugs.Base> :load Test.QuickCheck
Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 31 tests:
14

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 50 tests:
21

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 36 tests:
11

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 23 tests:
12

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 24 tests:
14

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 31 tests:
10

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 32 tests:
18

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 59 tests:
23

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 18 tests:
11

Test.QuickCheck> quickCheck $ \x -> x < (10::Int)
Falsifiable, after 19 tests:
10

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
Falsifiable, after 99 tests:
37

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
Falsifiable, after 95 tests:
38

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
Falsifiable, after 84 tests:
38

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
Falsifiable, after 87 tests:
44

Test.QuickCheck> quickCheck $ \x -> x < (35::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

Test.QuickCheck> quickCheck $ \x -> x < (100::Int)
OK, passed 100 tests.

これって ScalaCheck/QuickCheck は Int の範囲の値から等確率で値を選び出すんじゃなくて0あたりから始めて最初のほうほど選ばれる確率が高いみたいな実装なのだろうか。もしそうだとしたらかなり不便というかこのままではまったく使えない気がするのだけど、こんな事実が知られていないわけでもないだろうから何か合理的な理由があってみんな納得して使っているのだろうか。それとも使い方を間違えているのかな。

[1] http://blog.so-net.ne.jp/rainyday/2007-08-08-1


ScalaCheck を試す [Scala]

The fun of programming の第2章は QuickCheck という Haskell 用のテスト支援ツールの紹介。Scala にも QuickCheck を参考にした ScalaCheck [1] というのがあるので触ってみることにした。

* インストール

sbaz をセットアップしてあれば sbaz install scalacheck とするか、よくわからなければ ScalaCheck-0.2.jar をダウンロードしてきて Scala の lib ディレクトリに おけばよい。

* 使ってみる

インタプリタ上で使うことにします。まずは以下の名前をインポート。

scala> import scalacheck._
import scalacheck._

scala> import scalacheck.Gen._
import scalacheck.Gen._

scala> import scalacheck.Prop._
import scalacheck.Prop._

scala> import scalacheck.Test._
import scalacheck.Test._

加算演算子について「結合法則を満たす」という性質をテストしたい場合は以下のように仕様を書きます。

scala> val propAssoc = property( (x:Int, y:Int, z:Int) => (x + y) + z == x + (y + z) )
propAssoc: scalacheck.Gen[scalacheck.Prop.PropRes] = scalacheck.Gen$$anon$0@1fe500a

これをテストするには check メソッドを使います。

scala> check(propAssoc)
+++ OK, passed 100 tests.
res0: scalacheck.TestStats = TestStats(TestPassed(),100,0)

これで自動的にテストデータを100件作ってテストをしてくれました。(テストされたデータをログとして残すことはできないのだろうか?)
本ではこれを整数でなく浮動小数点数にすると丸め誤差でテストを通らなくなる!というのが面白かったのですが ScalaCheck には任意の浮動小数点数データを生成してくれる機能は今のところ無い模様。

今度は与えられた整数の桁数を求める関数 (cf. [2]) を以下のように定義してみます。

scala> def len(x: Int) = Math.floor(Math.log(x) / Math.log(10)).toInt + 1
len: (Int)Int

この関数はきっと次の性質を満たすことでしょう。

scala> val propLen = property ( (x:Int) => len(x) == x.toString.length )
propLen: scalacheck.Gen[scalacheck.Prop.PropRes] = scalacheck.Gen$$anon$0@3da1dc

テストしてみます。

scala> check(propLen)
*** Failed, after 0 successful tests:
The arguments that caused the failure was:
List(0)

res4: scalacheck.TestStats = TestStats(TestFailed(List(0)),0,0)

失敗しました。List の内容が失敗する場合の引数の例のようで、この関数は引数が 0 の時は失敗するということです。あー、本当はそういう仕様かも、ということで仕様のほうを変えてしまいます。「引数が 0 より大きい場合は」というような前提条件をつけるには ==> を使います(演算子の優先順位に注意)。

scala> val propLen = property ( (x:Int) => x > 0 ==> (len(x) == x.toString.length) )
propLen: scalacheck.Gen[scalacheck.Prop.PropRes] = scalacheck.Gen$$anon$0@1bf446e

scala> check(propLen)
+++ OK, passed 100 tests.
res5: scalacheck.TestStats = TestStats(TestPassed(),100,123)

今度はテストを通りました。実はこれも log の誤差で失敗してほしかったのですが

どうも Scala の Int の範囲では失敗しない様子です。

追記: x が 1000 のときにちゃんと?誤差が出るようになっていました。check(propLen) は何度やっても成功するのでたまたま当たらないということでしょうか。なんかいきなり ScalaCheck の限界を見てしまったような…

続くかも。

[1] http://code.google.com/p/scalacheck/
[2] http://ja.doukaku.org/40/


Scala で Binary heap tree [Scala]

何がきっかけで買おうと思ったのか不思議と全く思い出せないのだけど The fun of programming [1] という本が届いた(日本の Amazon には高いハードカバーしかなかったので出版元に直接注文して2週間くらいだった)ので、とりあえず第1章で紹介されている binary heap tree (のうち skew heap と呼ばれていたもの)を Scala で実装してみた。これは優先順位つきのキューに使える構造で、先頭要素(もっとも優先順位の高い要素)の参照は定数時間、取り出しと新規要素の追加は平均して対数時間(最悪で線形時間)という方式らしい。

abstract class BinaryHeapTree[T <% Ordered[T]] {
  def isEmpty: Boolean
  def top: T
  def pop: BinaryHeapTree[T]
  def insert(elem: T): BinaryHeapTree[T]
  def merge(that: BinaryHeapTree[T]): BinaryHeapTree[T]
}

case class Empty[T <% Ordered[T]] extends BinaryHeapTree[T] {
  def isEmpty = true
  def top = error("Empty.top")
  def pop = error("Empty.pop")
  def insert(elem: T): BinaryHeapTree[T] = Fork(elem, Empty[T], Empty[T])
  def merge(that: BinaryHeapTree[T]) = that
}

case class Fork[T <% Ordered[T]](label: T, left: BinaryHeapTree[T], right: BinaryHeapTree[T])
    extends BinaryHeapTree[T] {
  def isEmpty = false
  def top = label
  def pop = left.merge(right)
  def insert(elem: T): BinaryHeapTree[T] = this.merge(Fork(elem, Empty[T], Empty[T]))
  def merge(that: BinaryHeapTree[T]) = {
    if (that.isEmpty) this
    else if (this.top <= that.top) Fork(label, right, left.merge(that))
    else that.merge(this)
  }
}

object Main {
  def main(args: Array[String]) = {
    var t: BinaryHeapTree[Int] = Empty[Int]
    for (i <- 1 to 7) {
      t = t.insert(i)
      Console.println(t)
    }
  }
}

いま悩んでいること:
- 関数的な構造なので variance annotation をつけて covariant な型付けをさせたいが、なんかうまくいかない。「 <% Ordered[T] 」がついているとそういうことはできないのかも。頭が整理できない。

[1] http://web.comlab.ox.ac.uk/oucl/publications/books/fop/


Scala の Variance の話の続き [Scala]

Scala 2.6.0 で新規追加された existential type と Scala の既存機能の variance annotation の関係について調べたり考えたりしていた。既存の variance annotation のこともよくわかっていなかったなあ。

前回 [1] 書いたように covariant な型に対して出来る操作は基本 get のみである。逆に言うとパラメタ化された型が immutable であれば covariant な型付けは安全だし失うものは何もない。そこで Scala では従来から型パラメタに + を付けることによってコンパイラに covariant なサブタイピングを指示出来る。これを以下のような関数的なリスト構造で確認してみる。まずは + なしから。

scala> abstract class MyList[T]
defined class MyList

scala> case class MyNil[T] extends MyList[T]
defined class MyNil

scala> case class MyCons[T](head: T, tail: MyList[T]) extends MyList[T]
defined class MyCons

scala> val ints = MyCons(123, MyNil[Int])
ints: MyCons[Int] = MyCons(123,MyNil())

scala> val vals: MyCons[AnyVal] = ints
<console>:7: error: type mismatch;
 found   : MyCons[Int]
 required: MyCons[AnyVal]
val vals: MyCons[AnyVal] = ints
                           ^

Scala はデフォルトでは invariant なサブタイピングであるため上記はコンパイルエラーとなる。これを以下のようにするとコンパイルを通る。

scala> abstract class MyList[+T]
defined class MyList

scala> case class MyNil[+T] extends MyList[T]
defined class MyNil

scala> case class MyCons[+T](head: T, tail: MyList[T]) extends MyList[T]
defined class MyCons

scala> val ints = MyCons(123, MyNil[Int])
ints: MyCons[Int] = MyCons(123,MyNil())

scala> val vals: MyCons[AnyVal] = ints
vals: MyCons[AnyVal] = MyCons(123,MyNil())

うっかり前者のように + なしでクラスを定義してしまっていても Scala 2.6.0 からは以下のように救済(?)できる。

scala> val vals: MyCons[T] forSome { type T <: AnyVal } = ints
vals: MyCons[T] forSome { type T <: AnyVal } = MyCons(123,MyNil())

ここで疑問。「『 + 付き』で MyCons を定義していた場合の MyCons[AnyVal] 」と、「『 + 無し』で MyCons を定義していた場合の MyCons[T] forSome { type T <: AnyVal } 」は、型名は違うけど同じ型だと考えてよいのだろうか? 何か違いが出る状況はあるのか? それからひょっとして existential type のみをサポートしていれば従来からの variance annotation は不要になるとも言えるのだろうか? こうした微妙な点がまだよくわからない。

[1] http://blog.so-net.ne.jp/rainyday/2007-07-29


この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。