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のはなしは以上です。