My first trip to the phantom zone
- Magnus Therning
Based on apfelmus’ reply on the libraries mailing list it seems I got phantom types a bit wrong. So yesterday I set out to try to understand a bit better.
I got my inspiration from two blog posts in particular, Neil Mitchell’s and Lennart Augustsson’s. I am not sure I’m adding very much beyond the latter of the two but just in case it’ll help someone I’m documenting this anyway.
This is a rather contrived example but it’s short and simple. I have a type that can have two types of values:
type MyType = Either Int String
Int
s can be added and String
s can be concatenated:
add :: MyType -> MyType -> MyType
Left x) (Left y) = Left $ x + y
add (
cat :: MyType -> MyType -> MyType
Right a) (Right b) = Right $ a ++ b cat (
The problem with this of course is that nothing prevents me from trying to pass a Left
and a Right
to either of the functions. Both would result in errors as it stands. One option would of course be to explicitly deal with the error cases in code, but wouldn’t it be much nicer if it would somehow be possible to catch those error cases already at compile time?
This is exactly what phantom types allow, and it does it within “regular” Haskell ((As Lennart A points out in his post GADT’s offer a very elegant way of achieving the same results. However, GADT’s aren’t part of Haskell 98.)).
First I need a new type:
newtype MyPh a = MyPh MyType deriving (Show)
Then I rename the two functions above, add
and cat
, as add'
and cat'
and introduce two “wrapper functions”:
add :: MyPh Int -> MyPh Int -> MyPh Int
MyPh x) (MyPh y) = MyPh $ add' x y
add (
cat :: MyPh String -> MyPh String -> MyPh String
MyPh a) (MyPh b) = MyPh $ cat' a b cat (
The type signatures on these functions are the important part. Without them Haskell would infer the type MyPh a -> MyPh b -> MyPh c
for both functions and I gain nothing by that. What is left to do is to add constructors:
phInt :: Int -> MyPh Int
= MyPh . Left
phInt
phString :: String -> MyPh String
= MyPh . Right phString
Again the type signatures are crucial.
Now, to make sure that the user of this API doesn’t mess with the type safety that I’ve introduced I must take care to export the phantom type but not its constructor (MyPh
), the two manually written constructors (phInt
and phString
), and the safe functions for adding and concatenating (add
and cat
).
[Edited 17-10-2007] Corrected the inferred type after Derek’s comment.