I find myself running into a problem commonly, when writing larger programs in Haskell. I find myself often wanting multiple distinct types that share an internal representatio
I've benchmarked toy examples and not found a performance difference between the two approaches, but usage does typically differ a bit.
For instance, in some cases you have a generic type whose constructors are exposed and you want to use newtype
wrappers to indicate a more semantically specific type. Using newtype
s then leads to call sites like,
s1 = Specific1 $ General "Bob" 23
s2 = Specific2 $ General "Joe" 19
Where the fact that the internal representations are the same between the different specific newtypes is transparent.
The type tag approach almost always goes along with representation constructor hiding,
data General2 a = General2 String Int
and the use of smart constructors, leading to a data type definition and call sites like,
mkSpecific1 "Bob" 23
Part of the reason being that you want some syntactically light way of indicating which tag you want. If you didn't provide smart constructors, then client code would often pick up type annotations to narrow things down, e.g.,
myValue = General2 String Int :: General2 Specific1
Once you adopt smart constructors, you can easily add extra validation logic to catch misuses of the tag. A nice aspect of the phantom type approach is that pattern matching isn't changed at all for internal code that has access to the representation.
internalFun :: General2 a -> General2 a -> Int
internalFun (General2 _ age1) (General2 _ age2) = age1 + age2
Of course you can use the newtype
s with smart constructors and an internal class for accessing the shared representation, but I think a key decision point in this design space is whether you want to keep your representation constructors exposed. If the sharing of representation should be transparent, and client code should be free to use whatever tag it wishes with no extra validation, then newtype
wrappers with GeneralizedNewtypeDeriving
work fine. But if you are going to adopt smart constructors for working with opaque representations, then I usually prefer phantom types.