これはなに
先日開催された iOSDC 2017 に参加しました記事です。
Ray Fix 氏の 視覚化とSwiftのタイプについて という発表で
protocol CoordinateSpace {}
enum ModelSpace: CoordinateSpace {}
enum UserSpace: CoordinateSpace {}
enum DeviceSpace: CoordinateSpace {}
struct CGPointT<Space: CoordinateSpace>: Point2DProtocol {
var xy: CGPoint
// 実装
}
struct CGAffineTransformT<From: CoordinateSpace, To: CoordinateSpace> {
var matrix: CGAffineTransform
public func inverted() -> CGAffineTransforT<To, From> {
return CGAffineTransformT<To, From>(matrix.inverted())
}
}
public func * <From, To, I>(from: CGAffineTransformT<From, I>,
to: CGAffineTransformT<I, To>) -> CGAffineTransformT<From, To> {
return CGAffineTransformT<From, To>(from.matrix.concatenating(to.matrix))
}
みたいなサンプルコードが出てきて
let modelToUser: CGAffineTransformT<ModleSpace, UserSpace> = ...
let userToDevice: CGAffineTransformT<UserSpace, DeviceSpace> = ...
let modelToDevice = modelToUser * userToDevice // CGAffineTransformT<ModelSpace, DeviceSpace>
これ型パラメータ実装ないし、実質処理はなにもしてなくない?
というのが、幽霊型 (Phantom Type) です。
Phantom Type
上のコードがほぼ全てですが、型の状態 (モデル空間にある頂点なのかユーザー空間にある頂点なのか) を型パラメータとして表現することで、実行時ではなくコンパイル時に状態を検査することができます。が、この型パラメータは実装では利用されておらずコンパイルしたら消えてしまうので、幽霊型 (Phantom Type) というわけです。洒落てますね。
状態に依存したコード
struct StateDependent {
var ready: Bool = false
func doPo() {
precondition(ready, "Not ready!")
print("Po")
}
}
let stateDependent = StateDependent()
stateDependent.doPo() // Boom! Exception in runtime!
Phantom Type で状態を型で表現したコード
protocol State {}
enum Ready: State {}
enum NotReady: State {}
struct PhantomTypeSafe<S: State> {
static func create() -> PhantomTypeSafe<NotReady> {
return PhantomTypeSafe<NotReady>()
}
func toReady() -> PhantomTypeSafe<Ready> {
return PhantomTypeSafe<Ready>()
}
}
extension PhantomTypeSafe where S == Ready {
func doPo() {
print("Po")
}
}
let phantomTypeSafe = PhantomTypeSafe<NotReady>.create()
//phantomTypeSafe.doPo() // Cannot compile
phantomTypeSafe.toReady().doPo()
Phantom Type = コンパイル時に型検査するためのタグのようなもの
上記サンプルだと、状態を表す変数 var ready
をフィールドに持った型では、実行時までエラーが検出できませんが、Phantom Type を用いた型では extension
に where
句による制約を設けることで、ある 型の状態 でないと呼べない関数を表現することができており、コンパイル時に静的にエラーにできます。
Ray Fix 氏のサンプルだと、CGAffineTransformT
は ModelSpace
でも UserSpace
でも型としての性質や実装は変わらないので、下手に型を増やさずに Phantom Type で状態を表現して、変換同士の接続に破綻がないかを型で検証できています。
繰り返しになりますが、型パラメータ自体は利用されておらず、あくまで状態を表現する変数のようなものとして扱われているところがおもしろいところです。
Phantom Type で型の条件付け
これだけだとアレなのでもうひとネタ。
ジェネリクスのある言語ならこの Phantom Type のテクニックは使えると思うのですが、関数の引数に受け取る型に制約をつけるといえば他にもあった気がしてきました。型クラスですね。
というわけで Scala で Phantom Type と impicit を組み合わせて、受け入れる型に制約をつけてみます。
sealed abstract class =:=[From, To] extends (From => To) with Serializable
private[this] final val singleton_=:= = new =:=[Any,Any] { def apply(x: Any): Any = x }
object =:= {
implicit def tpEquals[A]: A =:= A = singleton_=:=.asInstanceOf[A =:= A]
}
def doPoWhenEqualType[A, B](implicit et: A =:= B) = "Po"
val po = doPoWhenEqualType[Int, Int] // Po
// val po = doPoWhenEqualType[Int, String] // compile error
上記の =:=
の定義は実際に Predef という、Scala が標準で読み込む定義に含まれています。
Swift でもやってみました。
class TypeTag<T, U> {}
func doPoWhenEqualType<T>(_ tag: TypeTag<T, T>) -> String {
return "Po"
}
//let po = doPoWhenEqualType(tag: TypeTag<Int, String>()) // Cannot compile
let po = doPoWhenEqualType(TypeTag<Int, Int>())
なんかもっとうまく書けそうな気がします…
もう少し Swift っぽい書き方
struct PhantomTypeEqual<A: State, B: State> {
func changeState<X: State, Y: State>() -> PhantomTypeEqual<X, Y> { return PhantomTypeEqual<X, Y>() }
}
extension PhantomTypeEqual where A == B {
func doPoWhenEqualType() -> String {
return "PoPoPo"
}
}
let typeNotEqual = PhantomTypeEqual<Ready, NotReady>()
//let popopo = typeNotEqual.doPoWhenEqualType() // Cannot compile
let typeEqual: PhantomTypeEqual<Ready, Ready> = typeNotEqual.changeState()
let popopo = typeEqual.doPoWhenEqualType()
Swift だと extension where
で型制約を書いていくと直感的になりますね。メソッドに型パラメータを明に渡せないのだけが悔やまれます。
というわけで、Phantom Type をさらっとだけ見てみました。
おしまい。