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


© Xinyu 2014 - 2024