概要
型レベルで整数を表現できると便利です。ええと、整数の型Intではなく。
たとえば固定長配列。長さ3の配列と長さ5の配列を結合すると、要素に依らず長さ8の配列になるはずです。この配列から、13番目の要素を取りだすコードは明らかにバグっているわけです。静的型付け脳としては、こういうは実行時ではなくコンパイル時にエラーにしてほしいですね?
たとえば行列。2x4行列と3x6行列を掛け算するコードは、やはりコンパイルエラーになってほしいですね?
たとえば単位系。3km/sec ÷ 7secという計算結果の値には、自動的にkm/sec^2の型が付いてほしいですね?
これらを実現するためには、型に数値情報を埋め込んで、それをコンパイル時に足し算したり比較する必要があります。
そのように値が入り込んでいる型を、依存型(dependent type)と呼びます。まさにそのためのライブラリ shapeless に自然数を表現する型 Nat があります。ただ整数は見当たらなかったので、色々と参考にしながら四則演算できるようにしてみましたーという記事です。
今回はScalaでやってみます。が、手法自体は他の言語でも使えるものなのだと思います。
型レベル真偽値の論理演算
準備として、まず型レベルで真偽値を表現して、論理演算ができるようにしてみます。
単位系ライブラリ KarolS/units の internal/Bools.scala で使われている手法を、ほぼそのまま使いました。
真を表すクラスTBoolと偽を表すクラスTFalseを作ります。どちらもTBoolトレイトをミックスインしています。
import scala.language.higherKinds
sealed trait TBool {
type Not <: TBool
type And[A <: TBool] <: TBool
type Or[A <: TBool] <: TBool
type If[Then <: T, Else <: T, T] <: T
}
class TTrue extends TBool {
type Not = TFalse
type And[A <: TBool] = A
type Or[A <: TBool] = TTrue
type If[Then <: T, Else <: T, T] = Then
}
class TFalse extends TBool {
type Not = TTrue
type And[A <: TBool] = TFalse
type Or[A <: TBool] = A
type If[Then <: T, Else <: T, T] = Else
}
object TBool {
type ![A <: TBool] = A#Not
type &&[A <: TBool, B <: TBool] = A#And[B]
type ||[A <: TBool, B <: TBool] = A#Or[B]
type If[A <: TBool, Then <: T, Else <: T, T] = A#If[Then, Else, T]
object ToBool {
def apply[A <: TBool](implicit toBool: ToBool[A]): Boolean = toBool()
}
trait ToBool[A <: TBool] {
def apply(): Boolean
}
implicit val toBoolTTrue = new ToBool[TTrue] {
def apply() = true
}
implicit val toBoolTFalse = new ToBool[TFalse] {
def apply() = false
}
}
これでOKです。TBoolの派生型をBooleanの値に変換するToBoolを用意したので、簡単に型チェックすることができます。
import TBool._
type Temp0 = ![TTrue] // TFalse
println(ToBool[Temp0]) // false
type Temp1 = TTrue && TFalse // TFalse
println(ToBool[Temp1]) // false
type Temp2 = TTrue || TFalse // TTrue
println(ToBool[Temp2]) // true
type Temp3 = If[TFalse, TFalse, TTrue, TBool] // TTrue
println(ToBool[Temp3]) // true
たしかに論理演算ができています。コンパイル時に――!
高カインド型について
import scala.language.higherKinds
は書かないと、TBoolのAnd / Or / Ifについて、
higher-kinded type should be enabled by making the implicit value scala.language.higherKinds visible.
とコンパイラが警告を出します。この理由は Scala Standard Library 2.11.2 - language に、
Higher kinded types in Scala lead to a Turing-complete type system, where compiler termination is no longer guaranteed.
と書かれています。デメリットを意識して自己責任で使えよ、ということですね。
とくに今回は、型付け時に計算しようとしているので、うっかり終わらない計算を書くと型付けが停止しなくなってしまいます。実際、実装ミスってコンパイラがスタックオーバーフローしたり、開発環境が固まったりしました。メタプログラミングの代償。。。
型レベル整数の四則演算
型レベルで整数を表現するために、 Practical Church Numerals in Scala で解説されている手法を使います。
たとえば、3を表現する型は次のようになります。いわゆるペアノ数ですね。
type T3 = TSuc[TSuc[TSuc[TZero]]]
-3を表現する型のためには、Successorの代わりにPredecessorを使います。
type T_3 = TPre[TPre[TPre[TZero]]]
それでは実装を貼ります。掛け算と割り算が自力で試行錯誤したところです。
import scala.language.higherKinds
import TBool._
sealed trait TInt {
type Neg <: TInt
type Abs <: TInt
type Suc <: TInt
type Pre <: TInt
type Add[A <: TInt] <: TInt
type Sub[A <: TInt] = Add[A#Neg]
type Mul[A <: TInt] <: TInt
// type Div[A <: TInt] <: TInt
protected type TSucDivTSuc[A <: TInt] <: TInt
type Div[A <: TInt] = IfTZeroTSucTPre[
TZero,
A#IfTZeroTSucTPre[TZero, TSucDivTSuc[A], TSucDivTSuc[A#Neg]#Neg, TInt],
A#IfTZeroTSucTPre[TZero, Neg#TSucDivTSuc[A]#Suc#Neg, Neg#TSucDivTSuc[A#Neg]#Suc, TInt],
TInt]
type Mod[A <: TInt] <: TInt
type IfTZeroTSucTPre[TZeroThen <: T, TSucThen <: T, TPreThen <: T, T] <: T
type IsTZero = IfTZeroTSucTPre[TTrue, TFalse, TFalse, TBool]
type IsTSuc = IfTZeroTSucTPre[TFalse, TTrue, TFalse, TBool]
type IsTPre = IfTZeroTSucTPre[TFalse, TFalse, TTrue, TBool]
type Equal[A <: TInt] = Sub[A]#IsTZero
type Greater[A <: TInt] = Sub[A]#IsTSuc
type Less[A <: TInt] = Sub[A]#IsTPre
}
trait TPos extends TInt
trait TNeg extends TInt
class TZero extends TPos with TNeg {
type Neg = TZero
type Abs = TZero
type Suc = TSuc[TZero]
type Pre = TPre[TZero]
type Add[A <: TInt] = A
type Mul[A <: TInt] = TZero
// type Div[A <: TInt] = TZero
type TSucDivTSuc[A <: TInt] = TZero
type Mod[A <: TInt] = TZero
type IfTZeroTSucTPre[TZeroThen <: T, TSucThen <: T, TPreThen <: T, T] = TZeroThen
}
class TSuc[P <: TPos] extends TPos {
type Neg = Pre#Neg#Pre
type Abs = Pre#Suc
type Suc = TSuc[TSuc[Pre]]
type Pre = P
type Add[A <: TInt] = Pre#Add[A]#Suc
type Mul[A <: TInt] = Pre#Mul[A]#Add[A]
/*
type Div[A <: TInt] = A#IfTZeroTSucTPre[
TZero, // ゼロで割った商はゼロとする
If[Less[A], TZero, Sub[A]#Div[A]#Suc, TInt],
If[Less[A#Neg], TZero, Add[A]#Div[A]#Pre, TInt],
TInt]
*/
type TSucDivTSuc[A <: TInt] = If[Less[A#Abs], TZero, Sub[A#Abs]#TSucDivTSuc[A]#Suc, TInt]
type Mod[A <: TInt] = A#IfTZeroTSucTPre[
TZero, // ゼロで割った余りはゼロとする
If[Less[A], Pre#Suc, Sub[A]#Mod[A], TInt],
If[Less[A#Neg], Pre#Suc, Add[A]#Mod[A], TInt],
TInt]
type IfTZeroTSucTPre[TZeroThen <: T, TSucThen <: T, TPreThen <: T, T] = TSucThen
}
class TPre[S <: TNeg] extends TNeg {
type Neg = Suc#Neg#Suc
type Abs = Neg
type Suc = S
type Pre = TPre[TPre[Suc]]
type Add[A <: TInt] = Suc#Add[A]#Pre
type Mul[A <: TInt] = Suc#Mul[A]#Sub[A]
/*
type Div[A <: TInt] = A#IfTZeroTSucTPre[
TZero, // ゼロで割った商はゼロとする
Add[A]#Div[A]#Pre,
Sub[A]#Div[A]#Suc,
TInt]
*/
type TSucDivTSuc[A <: TInt] = TZero
type Mod[A <: TInt] = A#IfTZeroTSucTPre[
TZero, // ゼロで割った余りはゼロとする
Add[A]#Mod[A],
Sub[A]#Mod[A],
TInt]
type IfTZeroTSucTPre[TZeroThen <: T, TSucThen <: T, TPreThen <: T, T] = TPreThen
}
object TInt {
type +[N1 <: TInt, N2 <: TInt] = N1#Add[N2]
type -[N1 <: TInt, N2 <: TInt] = N1#Sub[N2]
type *[N1 <: TInt, N2 <: TInt] = N1#Mul[N2]
type /[N1 <: TInt, N2 <: TInt] = N1#Div[N2]
type %[N1 <: TInt, N2 <: TInt] = N1#Mod[N2]
type ==[A <: TInt, B <: TInt] = A#Equal[B]
type !=[A <: TInt, B <: TInt] = A#Equal[B]#Not
type >[A <: TInt, B <: TInt] = A#Greater[B]
type <=[A <: TInt, B <: TInt] = A#Greater[B]#Not
type <[A <: TInt, B <: TInt] = A#Less[B]
type >=[A <: TInt, B <: TInt] = A#Less[B]#Not
object ToInt {
def apply[N <: TInt](implicit toIntN: ToInt[N]): Int = toIntN()
}
trait ToInt[N <: TInt] {
def apply(): Int
}
implicit def toIntTZero = new ToInt[TZero] {
def apply() = 0
}
implicit def toIntTSuc[P <: TPos](implicit toIntP: ToInt[P]) = new ToInt[TSuc[P]] {
def apply() = toIntP() + 1
}
implicit def toIntTPre[S <: TNeg](implicit toIntS: ToInt[S]) = new ToInt[TPre[S]] {
def apply() = toIntS() - 1
}
type T0 = TZero
type T1 = T0#Suc
type T2 = T1#Suc
type T3 = T2#Suc
type T4 = T3#Suc
type T5 = T4#Suc
type T6 = T5#Suc
type T7 = T6#Suc
type T8 = T7#Suc
type T9 = T8#Suc
type T_1 = T0#Pre
type T_2 = T_1#Pre
type T_3 = T_2#Pre
type T_4 = T_3#Pre
type T_5 = T_4#Pre
type T_6 = T_5#Pre
type T_7 = T_6#Pre
type T_8 = T_7#Pre
type T_9 = T_8#Pre
}
うまく出来ているか、TIntの派生型をIntの値に変換するToIntを使って、確かめてみます。
import TInt._
println(ToInt[T8 + T3]) // 11
println(ToInt[T8 + T_3]) // 5
println(ToInt[T_8 + T3]) // -5
println(ToInt[T_8 + T_3]) // -11
println(ToInt[T8 - T3]) // 5
println(ToInt[T8 - T_3]) // 11
println(ToInt[T_8 - T3]) // -11
println(ToInt[T_8 - T_3]) // -5
println(ToInt[T8 * T3]) // 24
println(ToInt[T8 * T_3]) // -24
println(ToInt[T_8 * T3]) // -24
println(ToInt[T_8 * T_3]) // 24
println(ToInt[T8 / T3]) // 2
println(ToInt[T8 / T_3]) // -2
println(ToInt[T_8 / T3]) // -3
println(ToInt[T_8 / T_3]) // 3
println(ToInt[T8 % T3]) // 2
println(ToInt[T8 % T_3]) // 2
println(ToInt[T_8 % T3]) // 1
println(ToInt[T_8 % T_3]) // 1
たしかに四則演算ができています。コンパイル時に――!
以下、余談です。
まず、TIntの演算についてですが、 Typelevel FizzBuzz in Scala のようにimplicit使って外部で実装する手法もあって、そちらの方が筋は良い気がするのですが、計算結果の型そのものを取りだす方法が分からなかったので、TIntトレイトに演算を詰め込みました。
さて、TIntの商を求める割り算Divが、なんだかおかしな実装になっています。これは元々コメントアウトしたコードでいけると思ったのですが、scalacがStackOverflowErrorを吐いてしまいました。おそらくtyperフェーズで、type aliasの再帰呼び出しが循環しているか収束しなくなっているのだと思います。If文で短絡評価できていないのが原因な気はするのですが、このあたりscalacがどういう順番でtype aliasを解決しているか知らないとどうしようもない感じがあって、苦し紛れに色々変形していったら偶然うまくいったのが上記のコードです……。
あと、高カインドなペアノ数で表せる数の上限ですが、 Limits of Nat type in Shapeless によると、
Shapeless 2.0 currently still uses the Church encoding, which will get you to 1,000 or so before the compiler gives up.
だそうです。コンパイル時間の問題もあるので、実用的にはどうなんでしょうね……。
単位系の次元を表す用途の場合は、二桁の整数が必要になることはそうそうないと思うので、適していそうです。
実行環境
name := "type_level_encode"
version := "1.0"
scalaVersion := "2.11.2"
sbt.version=0.13.6