型に数値を埋めこめると、コンパイル時に数値情報が使えるので便利です。
前回投稿した「コンパイル時に型レベルで整数を四則演算してみた」では一から型レベル整数の実装をしてみましたが、今回は @xuwei_k さんに紹介していただいたScalaの型レベルライブラリ sing を使ってみます。singすごい。
実行時エラーこわい
シンプルな硬貨クラスを考えてみます。
5円玉2個 Coin(5, 2)
と5円玉4個 Coin(5, 4)
を足すと5円玉6個 Coin(5, 6)
になるだけのものです。
つまりCoinのインスタンス一つで、一種類の硬貨を表すことができます。
あと、合計金額くらいは計算できるようにしておきましょう。
case class Coin(value: Int, num: Int) {
def +(a: Coin) = {
assert(value == a.value, "違う種類の硬貨を足しています")
Coin(value, num + a.num)
}
def -(a: Coin) = {
assert(value == a.value, "違う種類の硬貨を引いています")
Coin(value, num - a.num)
}
def sum = value * num
}
assertがありますね。つまり Coin(5, 3) + Coin(10, 6)
と5円玉2個と10円玉6個を足そうとしてしまうと、コンパイルは通るけれど実行するとAssertionErrorを投げます。
('ω')うわあああ実行時エラーだあああああああ
硬貨の価値は変わらない情報、すなわち5円玉が10円玉に変化したりすることはないでしょうから、こういうのは型情報に埋めこんでコンパイル時にさっさと落としてしまいたいですね。
それに実行時エラーだと、利用側はcatchしたりテスト書いて対処する必要がありますが、型チェックは一種の証明なので安心です。
コンパイル時エラーかわいい
そこで sing の提供する自然数Natの一つDenseを使ってみます。
たとえば自然数3を表す値 Dense._3
には同名の型 Dense._3
が付きます。
そして Dense._3.times(Dense._5)
には Dense._15
と同じ型が自動で付きます(!)
いったいどうやって型で自然数を表現しているかというと、singが型レベルで2進数リストを構築しているからなのですが、ひとまずメタプログラミングのヤバさだけ感じつつ、Coinクラスを書きなおしてみましょう。
import com.github.okomok.sing.Dense
case class Coin[Value <: Dense] (value: Value, num: Int) {
def +(a: Coin[Value]) = Coin(value, num + a.num)
def -(a: Coin[Value]) = Coin(value, num - a.num)
def sum = value.unsing * num // unsingでsing.Denseをscala.Intに変換。
}
やりました! さらば、assert。
硬貨の種類を型引数 [Value <: Dense]
として追加することができましたね。 Coin(Dense._5, 3)
の型は Coin[Dense._5]
になります。
これにより def +
や def -
の引数の型も、同じ種類の硬貨のみに制限することができます。
import com.github.okomok.sing.Dense
val a: Coin[Dense._5] = Coin(Dense._5, 2) + Coin(Dense._5, 4)
// val b = Coin(Dense._5, 3) - Coin(Dense._10, 6) // コンパイル不可
もうちょっと頑張ってみる
さて。このままだと、うっかり Coin(Dense._7, 1)
とすれば七円玉硬貨が作れてしまうので、型引数を制限したいところです。
気持ちとしては、 [Value <: Dense.{_1 or _5 or _10 or _50 or _100 or _500}]
みたいに、いわゆるunion type的な何かで書きたいところですが、Scalaではそう簡単にできないはずです。
今回は型引数の制限で頑張るのは諦めて、インスタンス生成を制限する方向でいきましょう。
つまり、Coinのコンストラクタをprivateにしてしまい、硬貨ごとにファクトリを作ります。こんな感じ。
import com.github.okomok.sing.Dense
class Coin[Value <: Dense] private(val value: Value, val num: Int) {
def +(a: Coin[Value]) = new Coin(value, num + a.num)
def -(a: Coin[Value]) = new Coin(value, num - a.num)
def sum = value.unsing * num
}
object Coin {
type _1 = Coin[Dense._1]
def _1(num: Int) = new _1(Dense._1, num)
type _5 = Coin[Dense._5]
def _5(num: Int) = new _5(Dense._5, num)
type _10 = Coin[Dense._10]
def _10(num: Int) = new _10(Dense._10, num)
type _50 = Coin[Dense._50]
def _50(num: Int) = new _50(Dense._50, num)
// singはDense._XXを50以下までしか定義していないので、
// 自然数100を表す値と型を計算しておく。
private type Dense_100 = Dense._50#times[Dense._2]
private val Dense_100 = Dense._50.times(Dense._2)
type _100 = Coin[Dense_100]
def _100(num: Int) = new _100(Dense_100, num)
// 同様に、自然数500を表す値と型を計算しておく。
private type Dense_500 = Dense._50#times[Dense._10]
private val Dense_500 = Dense._50.times(Dense._10)
type _500 = Coin[Dense_500]
def _500(num: Int) = new _500(Dense_500, num)
}
うーん、各種ファクトリがコピペ感ありますが、まぁ。
Coinを使ってみましょう。
val a: Coin._1 = Coin._1( 1) // 1 円 x 1
val b: Coin._5 = Coin._5( 2) // 5 円 x 2
val c: Coin._10 = Coin._10( 3) // 10 円 x 3
val d: Coin._50 = Coin._50( 4) // 50 円 x 4
val e: Coin._100 = Coin._100(5) // 100円 x 5
val f: Coin._500 = Coin._500(6) // 500円 x 6
val aa: Coin._1 = a + a
// val ab = a + b // コンパイル不可
val wallet: Seq[Coin[_]] = Seq(a, b, c, d, e ,f)
println(wallet.map(_.sum).sum) // 3741円
これでCoinを使う側は、singのDenseを知る必要もなくなりました。おしまい。
実行環境
sing はどこかのレポジトリで配布されているわけではなさそうなので、とりあえずクローンしてsbt publish-local
してから使いました。
name := "coin"
version := "1.0"
scalaVersion := "2.11.2"
libraryDependencies += "com.github.okomok" % "sing-core_2.11" % "0.2.0"
sbt.version = 0.13.6