Hi all,
Background
I often use an Id
type that helps distinguish Id
s from other Strings
and therefore prevents mixing up Ids with Strings. For example, this is possible:
updateUsersName : String -> String -> Cmd msg
updateUsersName id newName =
-- and then mixing up the parameters here
updateUsersName "cool-new-name-42" user.id
But this, using an Id
type isnt possible…
updateUsersName : Id -> String -> Cmd msg
updateUsersName id newName =
-- and then mixing up the parameters here
updateUsersName "cool-new-name-42" user.id
… you would get a compiler error.
New Problem
Recently I worked on a project that used the facebook api. The project involved updating a button on a facebook page owned by the users facebook account. I was managing the button’s Id, the page’s Id, and the users Id. My functions had type signatures like
updateUsersPagesButton : Id -> Id -> Id -> E.Value -> Cmd Msg
updateUsersPagesButton userId pageId buttonId buttonPayload =
You can see that this problem of mixing up values re-emerges. Theres nothing stopping me from mixing up userIds from pageIds
Thats okay, Phantom Types are a thing
Instead I used an `Id type like…
type Id x = Id String
so my functions were…
updateUsersPagesButton : Id User -> Id Page -> Id Button-> E.Value -> Cmd Msg
updateUsersPagesButton userId pageId buttonId buttonPayload =
and now its impossible to mix them up again.
This worked well enough, but then I realized that you cant put phantom type Ids
inside the thing they are an Id
of. The following is impossible due to a circular definition
type alias User = { id : Id User }
to work around this I did
type UserId = UserId
type alias User = { id : Id UserId }
Is there anything better than that work around?
I maintain and use a package for Id
s and managing lots of data that have Id
s. It would be really powerful to have phantom types in that package. You could have functions with type signatures like save : Id a -> a -> ...
, that commit the Id
exactly to the type of the thing its meant to be an Id
of. Id a
always refer to an a
. But the trade off is that you cant ever have the Id
internal to the data it refers to because that would always be circular.
Instead you would have to manage data with Id
s like this
(Id User, User)
I store ids like this anyway, because it avoids other problems Ive encountered as well. To me it seems like a good practice. But Im wondering if its a bad idea to fully commit all data with Id
to never containing its own Id
internally. This tuple approach is certainly a lot less obvious.
Do any problems jump out to any of you by handling Id
s in this tuple style and never inside the data itself? Is this even a worthy consideration?
Thanks