A wonderfully clear explanation of phantom types, although it misses out on one of their most important features. Phantom types are particularly excellent because they carry no runtime penalty! The types exist only at compile time, and are then stripped out once the program has passed the type checker. So you get the additional safety of a type without the necessary overhead of storing the type when the program is run.
Thanks for pointing this out, I’ve added a small update to the article with this explanation.
Wonderful! Thanks for writing such a clear explanation of phantom types.
related: this classic piece by tom moertel on using type safety to prevent things like sql injection
Phantom types are really cool but I feel the ruby example is misleading. I think that something like:
Message = Struct.new(:text) do
@ciphertext ||= # encrypt plain text logic
# send using message.ciphertext
Would be a big improvement.
This really depends. You might not want to have the encryption logic in the message itself. You’re tying your data to the way you manipulate it, which works in this case, since there’s only two states for the message.
But imagine something like strong parameters in Rails if you’re familiar with them. If you do something like User.new(params[:user]) these days, you’ll get UnpermittedParametersException, since you need to permit them first.
But the process of permitting depends on what you want to do, since you rarely want to permit them all, which would be the case of something like User.new(params[:user].permit!). Rather than that, most Rails apps do something like this User.new(params.require(:user).permit(:username, :password)).
In this case you’re bound to only pass in permitted parameters to the model, but you can’t automate it or force it by construction as you did in your example. However using phantom types it would be really easy to do, since there would just be Params Permitted and Params Unpermitted.
I’m not sure I follow. Parameter permissions is an integration problem. On reception from the outside world of incoming data from an arbitrary sender, one has to parse and/or validate that data at runtime whether the language is static or dynamic. I would not recommend passing around non-validated parameters in application code. Validate the incoming data then only provide validated data to the rest of the application.
Yes you are absolutely correct. The point I was trying to make is that if you try to pass unpermitted parameters to a function like User.new, it should be a type error, not a runtime error (which can even end up hidden sometimes).
AFAIK the current solution in Rails will raise an exception on unpermitted params, which is fine, but phantom types give you an option to implement the same thing at compile time :)
As a corollary, here is an example using OCaml’s phantom support: https://blogs.janestreet.com/howto-static-access-control-using-phantom-types/