Record updates have to be able to change the type of the value in order to work with polymorphic fields. Here's a contrived example:

data Foo a = Foo { foo :: a }

reFoo x = x { foo = "blarg" }

By the same logic, the resulting type can change the phantom parameter because the phantom parameter is not constrained at all. You could have the same problem without record syntax:

unsafeId (Username first last) = Username first last

That looks fine—but completely bypasses the phantom type! (In fact, it could even produce a Username Int or something.)

This is a fundamental limitation of phantom types. Since they're not constrained at all, they're very easy to change. They're useful, but more as a convention and a warning than as an ironclad guarantee.

In this specific case, I can think of two reasonable options. One is to make the Username type abstract—don't export the constructor and ensure that the only provided ways to make one are safe. Another is to make a String newtype with a flag, and keep that abstract. Domain-specific types like Username could use this newtype for their fields.

newtype Str a = Str String

data Username a = Username { first :: Str a, last :: Str a }

As long as you have access to a Str constructor, you can make "Safe" instances however you like; however, if you keep the type abstract, it can be safe outside the defining module.

There are some other design options, and it's something worth thinking about. But what you've found is a fundamental quality (and limitation) of phantom types and has to be kept in mind as you're working with them.


On Wed, Jun 17, 2015 at 7:45 PM, Dimitri DeFigueiredo <defigueiredo@ucdavis.edu> wrote:
Hi All,

My apologies if this is not the right forum, but am not satisfied with my current understanding.

I am surprised that this program compiles in GHC:

-----
data UserSupplied  = UserSupplied -- i.e. unsafe
data Safe          = Safe

data Username a = Username { first :: String, last :: String}

sanitize :: Username UserSupplied -> Username Safe
sanitize name = name { first = "John" }

main = putStrLn "Hi!"
-----

My trouble is that it seems the record syntax is *implicitly* converting from one type to the other. It seems I would have to remove the phantom type by adding a tag to avoid this:

-----
data Username a = Username { first :: String, last :: String, tag :: a }

sanitize :: Username UserSupplied -> Username Safe
sanitize name = name { first = "John" } -- FAIL as expected!! :-)
-----

But this makes me unwilling to use phantom types for security as I would be worried of unwittingly making the conversion.

Could somebody sprinkle some insight into why this is "converted automatically" for phantom types?
Is there another way around this?


Thanks!


Dimitri



_______________________________________________
Haskell-Cafe mailing list
Haskell-Cafe@haskell.org
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe