Tagged type 

「すごいHaskellたのしく学ぼう」の本を持ってるひとは新しい章に進める。モノイドだ。ウェブサイトを読んでるひとは Functors, Applicative Functors and Monoids の続きだ。

LYAHFGG:

Haskell の newtype キーワードは、まさにこのような「1つの型を取り、それを何かにくるんで別の型に見せかけたい」という場合のために作られたものです。

これは Haskell の言語レベルでの機能なので、Scala に移植するのは無理なんじゃないかと思うと思う。 ところが、約1年前 (2011年9月) Miles Sabin さん (@milessabin)gist を書き、それを Tagged と名付け、Jason Zaugg さん (@retronym)@@ という型エイリアスを加えた。

type Tagged[U] = { type Tag = U }
type @@[T, U] = T with Tagged[U]

これについて読んでみたいひとは Eric Torreborre さん (@etorreborre)Practical uses for Unboxed Tagged Types、それから Tim Perrett さん (@timperrett)Unboxed new types within Scalaz7 を書いている。

例えば、体積をキログラムで表現したいとする。kg は国際的な標準単位だからだ。普通は Double を渡して終わる話だけど、それだと他の Double の値と区別が付かない。case class は使えるだろうか?

case class KiloGram(value: Double)

型安全性は加わったけど、使うたびに x.value というふうに値を取り出さなきゃいけないのが不便だ。Tagged type 登場。

scala> sealed trait KiloGram
defined trait KiloGram

scala> def KiloGram[A](a: A): A @@ KiloGram = Tag[A, KiloGram](a)
KiloGram: [A](a: A)scalaz.@@[A,KiloGram]

scala> val mass = KiloGram(20.0)
mass: scalaz.@@[Double,KiloGram] = 20.0

scala> 2 * Tag.unwrap(mass) // this doesn't work on REPL
res2: Double = 40.0

scala> 2 * Tag.unwrap(mass)
<console>:17: error: wrong number of type parameters for method unwrap$mDc$sp: [T](a: Object{type Tag = T; type Self = Double})Double
              2 * Tag.unwrap(mass)
                      ^

scala> 2 * scalaz.Tag.unsubst[Double, Id, KiloGram](mass)
res2: Double = 40.0

以前は 2 * mass と書けたけども Scalaz 7.1 以降は明示的にタグを unwrap しなければいけなくなった。さらに REPL のバグ SI-8871 のせいで、Tag.unwrap が動作しないため、Tag.unsubst を使う必要があった。 補足しておくと、A @@ KiloGramscalaz.@@[A, KiloGram] の中置記法だ。これで相対論的エネルギーを計算する関数を定義できる。

scala> sealed trait JoulePerKiloGram
defined trait JoulePerKiloGram

scala> def JoulePerKiloGram[A](a: A): A @@ JoulePerKiloGram = Tag[A, JoulePerKiloGram](a)
JoulePerKiloGram: [A](a: A)scalaz.@@[A,JoulePerKiloGram]

scala> def energyR(m: Double @@ KiloGram): Double @@ JoulePerKiloGram =
         JoulePerKiloGram(299792458.0 * 299792458.0 * Tag.unsubst[Double, Id, KiloGram](m))
energyR: (m: scalaz.@@[Double,KiloGram])scalaz.@@[Double,JoulePerKiloGram]

scala> energyR(mass)
res4: scalaz.@@[Double,JoulePerKiloGram] = 1.79751035747363533E18

scala> energyR(10.0)
<console>:18: error: type mismatch;
 found   : Double(10.0)
 required: scalaz.@@[Double,KiloGram]
    (which expands to)  AnyRef{type Tag = KiloGram; type Self = Double}
              energyR(10.0)
                      ^

見ての通り、素の DoubleenergyR に渡すとコンパイル時に失敗する。これは newtype そっくりだけど、Int @@ KiloGram など定義できるからより強力だと言える。