I normally write about Haskell, but I’m also super excited about Rust!
I’m pretty new to it, though, so this post might have some incorrectness.
One convenient feature that Rust has is a type-level
It’s used to provide trait bounds (equivalent to Haskell’s type class constraints) to type variables.
Here’s a type signature from
nom, a parser combinator library (github link):
pub fn anychar<T>(input: T) -> IResult<T, char> where T: InputIter + InputLength + Slice<RangeFrom<usize>> + AtEof, <T as InputIter>::Item: AsChar,
We’re declaring a public function named
anychar that is parameterized by some type
It accepts a single argument, named
input, that has the type
It returns a value of type
where bit defines the trait bounds on the
T type – we say that
T must satisfy the
Slice, etc. traits.
:: refers to an associated type, which is essentially the same thing as an associated type on a type class in Haskell.
<T as InputIter>::Item refers to the
Item type for the implementation of
Let’s write this in Haskell:
anychar :: forall t. ( InputIter t, InputLength t , Slice (RangeFrom Int) t, AtEof t , AsChar (InputIterItem t) ) => t -> Result t Char
Haskell has implicit quantification by default.
That is, you don’t have to introduce type variables – you can just use them where you want.
forall keyword introduces type variables to a type.
So the syntax
forall t. is analogous to the
<T> syntax in Rust.
Haskell’s type classes take type variables as arguments and turn them into
So, instead of Rust’s
t : InputIter (“
t satisfies the trait bound
InputIter”), we say
InputIter t (“given an instance of
Can we get this into Haskell, as well? Yes, with some type families!
type family Where a cs :: Constraint where Where _ ' = () Where a (c ': cs) = (c a, Where a cs)
Here, we define a type family
Where, which takes two parameters: a type
a of kind
k, and a type
cs of kind
[k -> Constraint], and it turns them into a single
For the empty list, we have no constraints to add – therefore, we use the empty constraint,
If we have a constraint, then we add that constraint
c a and do constraint union with the result of
Where a cs.
Here’s how it looks:
anychar :: forall t. ( Where t [ InputIter, InputLength , Slice (FromRange Int), AtEof ] , AsChar (InputIterItem t) ) => f a -> f a
It might be good to remember the story “If you give a mouse a cookie…”
If we can have
Where, that makes me want
It would be really nice to be able to provide shorthands for commonly repeated types in a function.
Consider this signature:
doThings :: MaybeT (ExceptT MyError IO) Int -> MaybeT (ExceptT MyError IO) Char -> MaybeT (ExceptT MyError IO) (Int, Char) doThings mi mc = do i <- mi c <- mi pure (i, c)
Look at all of that repetition. We can factor it out into a top level definition:
type ThingDoing = MaybeT (ExceptT MyError IO) doThings :: ThingDoing Int -> ThingDoing Char -> ThingDoing (Int, Char)
But that can clutter the namespace.
We want something local, for the same reason we want
where in the value language.
What do we have to do to make the following code work?
doThings :: Let m (MaybeT (ExceptT MyError IO)) => m Int -> m Char -> m (Int, Char)
Well, it’s easier than you might think:
type Let = (~)
(~) is a type equality constraint.
We’re saying that
MaybeT (ExceptT MyError IO) must be equal.
So that lets us use
m where we might use the longer, explicit type.
This trick is great for reducing duplication in type signatures. Thanks, Rust, for inspiring me to want this.