Phantom Types in Swift

在 Objc.io 上看到了这样一期视频。主要介绍了一种叫 Phantom Types 的技巧,它的作用就是在类型(type),而不是值(value)这个层面上来表示状态,而且在编译时期对错误类型间的运算做出提示。

Phantom Types(幽灵类型) 其实就是空类型。比如这样,

enum Miles {}
enum Kilometers {}

它比较实际的一个应用是,让编译器帮你检查某些对象在特定的状态下能够调用哪些方法。也能通过类型来表示状态。

Example

举一个 Foundation 中 API 的例子。有这样一个类 NSFileHandle

+ (nullable instancetype)fileHandleForReadingAtPath:(NSString *)path;
+ (nullable instancetype)fileHandleForWritingAtPath:(NSString *)path;

- (NSData *)readDataToEndOfFile;
- (void)writeData:(NSData *)data;

它有几种初始化方法,当你创建一个读方式的 fileHandle 时,你只能调用读相关的 API,调用写相关的是没有意义的。但是在 Objective-C 中,不足以在编译时期把这个问题搞定,你还是可以开一个读的 fileHandle,然后对它调用写的方法,

NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:@"path"];
/// it does't make sense
[handle writeData:someData];

如何通过 Phantom Types 来解决这个问题呢?实现的方式很简单,借助 Swift 中的泛型,把当前类型下能够调用的方法定义在类型限定的 extension 中就可以了。

我们定义出两个 Phantom Types,

enum Write {}
enum Read {}

然后定义出支持泛型的 FileHandle 类,

struct FileHandle<OperationType> {
let path: String
init(_ path: String) {
self.path = path
}
}

把不同类型 handle 可以调用的方法放到不同的 extension 中,

extension FileHandle where OperationType == Write {
/// 把初始化方法也放到各自的原因是可以让编译器推倒类型
static func handleWithWirtePath(path: String) -> FileHandle<Write> {
return FileHandle<Write>(path)
}

func write(string: String) {
// ...
}
}

extension FileHandle where OperationType == Read {
static func handleWithReadPath(path: String) -> FileHandle<Read> {
return FileHandle<Read>(path)
}

func read() -> String {
return ""
}
}

调用就很简单了,

let f = FileHandle.handleWithReadPath(path: "path/to/resources")
f.read()

如果你想要通过上面的 handle 调用 write 相关的方法,编译器会给出提示,

/// error: 'FileHandle<Read>' is not convertible to 'FileHandle<Write>'
f.write(string: "")

再多说两句

在 Haskell 中,这种应用是会在编译时期被优化掉的,所以并不会对性能产生任何影响。不知道 Swift 是不是也是一样(but who cares)。

以后在看到一些空类型的定义可以先怀疑它是不是 Phantom Types 的一种应用,也许它的出现是存在意义的。要好好利用类型系统的强大。

Credits