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 的一种应用,也许它的出现是存在意义的。要好好利用类型系统的强大。