Posted at

Swiftで幽霊型を実装する


概要

Swiftで幽霊型を実装する方法を説明します。

※幽霊型は英語でPhantom Type(ファントムタイプ)と呼ばれますが、本記事内では幽霊型で統一します。


幽霊型とは

幽霊型はSwiftのclass、struct、enumのような言語機能として提供される型ではありません

幽霊型は実装パターンの一種です。実装パターンのため、Swiftに限定されるものではなく、Scala、Haskellなどでも同様の表現があります。(参考: Phantom Typeの紹介

コンパイル時にしか存在しないという特徴を持つため、幽霊型と呼ばれるようです。


幽霊型の特徴

幽霊型は以下のような特徴を持ちます。


  • 型に型パラメータとして付加情報を持たせることにより、コンパイラでの静的チェックを強化する

  • 型による制約がコードに表現されるため、コードが自己文書化され、可読性も向上する

  • 幽霊型の実装はジェネリクスの型パラメータにすぎないため、実行時には影響しない


もっとも簡単な実装例

幽霊型のミニマムな実装例はこれだけです。ここでenumで表現している理由は、幽霊型はインスタンス化する必要がないからです。caseを持たないenumはインスタンス化することができないという一般的なテクニックを利用しています。

enum ATag {}

enum BTag {}

struct Hoge<T> { }

型引数により、Aという情報を付与できています。

例えば以下のような実装により、ATag情報を持つHogeインスタンスのみを受け入れるような関数を作成できます。

func print(hoge: Hoge<ATag>) {

print("ATagのみ出力")
}

let hoge = Hoge<BTag>()
print(hoge: hoge) // Extraneous argument label 'hoge:' in call


もう少し面白い実装例

以下のようなCat型を考えます。

struct Cat {}

幽霊型によってねこが空腹かどうかの付加情報を表現してみます。

まず、Cat型に型パラメータTを持たせ、ねこの状態をenumで表現します。

// ジェネリック型に変更する

struct Cat<CatState> {}

// ねこの状態を表す幽霊型
protocol CatState {}
protocol Runnable {}
protocol Eatable {}
enum HungryCat: CatState, Eatable {} // 空腹
enum NotHungryCat: CatState, Runnable {} // 満腹

これでねこの状態を型情報に持たせることができるようになりました。catAインスタンスは空腹で、catBインスタンスは満腹であることがコンパイラにも理解できるようになります。

let catA = Cat<HungryCat>()

let catB = Cat<NotHungryCat>()

これを利用し、Cat型を型制約付きのエクステンションを実装することで幽霊型に応じた実装が可能です。

extension Cat where T: Eatable {

func eat() {
print("食べる")
}
}

extension Cat where T: Runnable {
func run() {
print("走る")
}
}

let catA = Cat<HungryCat>()
catA.eat()
catA.run() // Type 'HungryCat' does not conform to protocol 'Runnable'

let catB = Cat<NotHungryCat>()
catB.eat() // Type 'NotHungryCat' does not conform to protocol 'Eatable'
catB.run()


より実践的な実装例

【Swift】型を使うという意味を考える (Phantom Typeを通して) - Qiita

こちらの記事では幽霊型をショッピングアプリケーションでの状態管理に利用したり、UserのidやDogのidなどを同じString型で表現せず、複数のIDを幽霊型を利用して識別する方法が示されています。


幽霊型を利用しない場合

逆に幽霊型を利用しないパターンも容易に想像がつきます。例えば、先述のHungryCat、NotHungryCatを個別のstructで定義する場合です。

struct HungryCat: Eatable {}

struct NotHungryCat: Runnable {}

それぞれの型の固有の振る舞いはそれぞれの型に定義すれば良いですが、共通の振る舞いを安直に実装すると、冗長なコードになります。(nameプロパティやsleep()メソッド)

struct HungryCat: Eatable {

let name: String

func sleep() {
print("ねる")
}
}

struct NotHungryCat: Runnable {
let name: String

func sleep() {
print("ねる")
}
}

メソッドであればプロトコルエクステンションでの処理の共通化が可能ですが、プロパティはスーパークラスを定義しないと共通化はできません。(スーパークラスの設計の難しさや構造体からクラスに変更する必要が出てくる)

extension Sleepable {

func sleep() {
print("ねる")
}
}

struct HungryCat: Eatable, Sleepable {
let name: String
}

struct NotHungryCat: Runnable, Sleepable {
let name: String
}

上記のように、独自型を用いるのか、幽霊型を用いて既存の型を拡張していくのかどちらが良いのかは修正による影響範囲なども関係するため、ケースバイケースかと思います。独自型と幽霊型の使い分けなどあればコメントに書いていただけるとうれしいです。


参考