Blog

Functional Snippet #13: Phantom Types

Sometimes, we want to have additional safety when dealing with important types in our application. For example, suppose your application revolves around file handles, and you want to be sure that you never try to perform any write operations on a file handle that’s open for reading. As a first step, we can create a wrapper around NSFileHandle with an extra generic parameter:

struct FileHandle<A> {
    let handle: NSFileHandle
}

We can add two empty types, which won’t do anything at the value level, but only record information on the type-level.

struct Read {}
struct Write {}

Using these types, we can create two more wrappers, but this time around NSFileHandle’s initializers. We’ll use the Read and Write types, even though the FileHandle doesn’t contain any values of that type. That’s why the A parameter is called a phantom type. The types are just there to track permissions.

func openForReading(path: String) -> FileHandle<Read>? {
    return NSFileHandle(forReadingAtPath: path).map { FileHandle(handle: $0) }
}

func openForWriting(path: String) -> FileHandle<Write>? {
    return NSFileHandle(forWritingAtPath: path).map { FileHandle(handle: $0) }
}

Our FileHandle type nicely records what’s happening. For example, we can write a function that only works on file handles which are open for reading:

func readDataToEndOfFile(handle: FileHandle<Read>) -> NSData {
    return handle.handle.readDataToEndOfFile()
}

Trying to call this function on a file-handle that’s open for writing will not even compile. You can use these techniques for important parts of your own code as well, if you have areas where you want the compiler to check errors for you, rather than having the code fail at runtime.

Back to the Blog

recent posts