A library that allows you to "tag" a value with a specific type for compile time verification.
The semantics associated with the value do not change.
If you had an Int
, Maybe Bool
, or List String
before,
it's still an Int
, Maybe Bool
, or List String
respectively.
It just now has a type level tag.
Tagging a value is useful for making compile time assertions about what you're building.
For instance, let's say you are modeling two resources from a server User
and Comment
.
Both of these resources have unique identifiers which could be represented as Int
s.
We might have the following:
type alias User =
{ ident : Int
, ...
}
type alias Comment =
{ ident : Int
, ...
}
Although the two are very similar,
a User
identifier should not be used in the same way as a Comment
identifier.
Since these two records use the same underlying type for their ident
fields,
the type checker wont stop you from using a User
identifier where a Comment
identifier was expected.
A first pass at making this distinction might be to give each identifier a type alias.
type alias UserIdent =
Int
type alias User =
{ ident : UserIdent
, ...
}
type alias CommentIdent =
Int
type alias Comment =
{ ident : CommentIdent
, ...
}
Now you as a human can tell a little better that the two identifiers are different.
However, the type checker still wont stop you from using a User
identifier where a Comment
identifier was expected.
A second pass at making this distinction might be to give each identifier its own type.
type UserIdent
= UserIdent Int
type alias User =
{ ident : UserIdent
, ...
}
type CommentIdent
= CommentIdent Int
type alias Comment =
{ ident : CommentIdent
, ...
}
Now you as a human can tell a little better that the two identifiers are different.
And, the type checker WILL stop you from using a User
identifier where a Comment
identifier was expected!
However, note that now operations like (<)
don't work with UserIdent
and CommentIdent
.
So you might write a function for that.
type UserIdent
= UserIdent Int
type alias User =
{ ident : UserIdent
, ...
}
ltUserIdent : UserIdent -> UserIdent -> Bool
ltUserIdent (UserIdent x) (UserIdent y) =
x < y
type CommentIdent
= CommentIdent Int
type alias Comment =
{ ident : CommentIdent
, ...
}
ltCommentIdent : CommentIdent -> CommentIdent -> Bool
ltCommentIdent (CommentIdent x) (CommentIdent y) =
x < y
Next, you might find out that you need (<=)
to work as well for each identifier.
So, you write another function.
type UserIdent
= UserIdent Int
type alias User =
{ ident : UserIdent
, ...
}
ltUserIdent : UserIdent -> UserIdent -> Bool
ltUserIdent (UserIdent x) (UserIdent y) =
x < y
gteUserIdent : UserIdent -> UserIdent -> Bool
gteUserIdent (UserIdent x) (UserIdent y) =
x >= y
type CommentIdent
= CommentIdent Int
type alias Comment =
{ ident : CommentIdent
, ...
}
ltCommentIdent : CommentIdent -> CommentIdent -> Bool
ltCommentIdent (CommentIdent x) (CommentIdent y) =
x < y
gteCommentIdent : CommentIdent -> CommentIdent -> Bool
gteCommentIdent (CommentIdent x) (CommentIdent y) =
x >= y
And then you might write another function, and another. Usually one of two things happens. You either end up writing a slew of boilerplate functions, or you lie to yourself (and your team if you're working with others) and say that expressing these constraints isn't important/worth the time.
If you continue down the first path, you might recognize a pattern that all of these functions have in common: they are all unwrapping the "tag", applying the function and wrapping the value back up in the "tag".
This module abstracts away the pattern so you don't have to spend time writing the same code over and over.
The key insight is that UserIdent
and CommentIdent
are structurally the same.
They both wrap a type—Int
—with a "tag"—UserIdent
or CommentIdent
.
So what we want is a type that carries another type and a "tag":
type Tagged tag value
= ...
Now, what values does this type need to provide? At runtime, we don't care about what type the value was tagged with. We just care about the value itself. So, let's only provide the value:
type Tagged tag value
= Tagged value
And how does this work for our running example?
Well, we need to create a "tag" for UserIdent
and a "tag" for CommentIdent
.
-- It'd be nice if we could just say `type UserIdent`, but elm can't parse that.
type UserIdent
= UserIdent
type alias User =
{ ident : Tagged UserIdent Int
, ...
}
-- It'd be nice if we could just say `type CommentIdent`, but elm can't parse that.
type CommentIdent
= CommentIdent
type alias Comment =
{ ident : Tagged CommentIdent Int
, ...
}
If we need to compare UserIdent
s,
we can use Tagged
functions rather than needing to rewrite everything.
Tagged.map2 (<) someIdent anotherIdent