If you’ve been using Haskell you’ll know that it has an expressive static type system. This gives us a way to reason about how our programs will behave before we even run them, and, at least in my case, prevents a lot of dumb errors.
Just as Haskell has a type system to classify values, it has a kind system to classify types. It provides a way to reason about what types make sense to construct. Let’s see what it’s all about.
Type
s, Type
s EverywhereThe most common kind of type^{1} you’ll see are Type
s (shocking, isn’t it?). These include Int
, Bool
, Char
, as well as Double > String
, [Integer]
, and Either Rational [Bool]
. Another name for the kind Type
is *
. We can check a type’s kind in GHCi with :kind
or :k
for short.
ghci> :k [Integer]
[Integer] :: *
If we turn on the language extension NoStarIsType
GHCi will instead use the more modern name Type
(which is located in the Data.Kind
module).
ghci> :set XNoStarIsType
ghci> :k [Integer]
[Integer] :: Type
If you see that a type has kind Type
that means it can appear on its own on the righthand side of a ::
^{2}, as in
ints :: [Integer]
= [1,2,3] ints
Of course, it’s hard to understand what being a Type
means if you don’t know what types don’t have kind Type
. An example of something with kind different from Type
is Maybe
. Maybe
can’t show up on its own as a type signature, it just doesn’t make sense. The same thing goes for Either
. However, if we apply Maybe
to a type, for example, Int
, and then we get a normal type, Maybe Int
, which can appear on its own on the right of a ::
.
it'sAnIntIPromise :: Maybe Int
= Just 42 it'sAnIntIPromise
Also notice that we specifically have to give Maybe
something of kind Type
. It wouldn’t make sense to have a Maybe Either
. So if we give Maybe
a Type
, it becomes a Type
. So you might even say that the kind of Maybe
is Type > Type
. And indeed GHCi agrees with us:
ghci> :k Maybe
Maybe :: Type > Type
Now, what about Either
? Just as with Maybe
it doesn’t make sense to pass it something that’s not a Type
. But even if we pass it a Type
, we don’t get a Type
just yet, I can’t have an Either Bool
in my pocket. But if we pass Either Bool
a Type
we indeed get a normal bona fide Type
, such as Either Bool Int
. Meaning, Either Bool
must have kind Type > Type
, just like Maybe
. But what that means is that if we pass Either
a Type
we get back a Type > Type
, meaning Either
must have kind Type > (Type > Type)
.
Did you notice? This is currying! Since >
associates to the right by default we can drop the parentheses and say that Either
has kind Type > Type > Type
. Just like with currying functions, there are two equivalent ways of looking at Either
. We can look at it as a type that takes two Type
s and becomes a Type
, or as a type that takes a Type
and returns a Type > Type
. Check it in GHCi!
If we try to have something nonsensical like Maybe Either
then GHC will give us a kind error:
λ> type T = Maybe Either
<interactive>:1:16: error:
• Expecting two more arguments to ‘Either’
Expected a type, but ‘Either’ has kind ‘Type > Type > Type’
• In the first argument of ‘Maybe’, namely ‘Either’
In the type ‘Maybe Either’
In the type declaration for ‘T’
This is the bread and butter of what kinds are for, sanity checking.
Another thing we can notice is that (>)
is just like Either
, it takes two Type
s and returns a Type
, specifically a function type. Another important type to consider is []
, the list type. It takes a type, such as Int
, and returns a Type
. In fact, [Int]
is the same as [] Int
, it’s just syntax sugar, you can try it yourself.
Let’s look at a more complex example:
data OfInts f = MkOfInts (f Int)
This is a pretty weird type. It takes some f
and stores an f
of Int
s. Let’s figure out the kind of OfInts
. f
must take a type as input since we see f Int
appear. But then f Int
must be a Type
since OfInts
stores a value of type f Int
. So f
must have kind Type > Type
. Now, OfInts f
must be a Type
, since it’s a fullyfledged datatype that you can have values of. So putting this together, OfInts
must take a Type > Type
and returns a Type
, meaning it has (Type > Type) > Type
.
Without using GHCi, what is the kind of (,,)
, the type constructor for 3tuples?
Consider the following datatype declaration:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
Without using GHCi, what is the kind of MaybeT
? Note that newtype
is the same thing as data
as far as the kinds go.
Consider the following datatype declaration:
data Contrived f g a = MkContrived (f Maybe (Either a (g Bool)))
Without using GHCi, what is the kind of Contrived
?
Constraint
We’ve seen the kinds of various datatypes, and it seems they always have a kind which is some mix of Type
and >
perhaps with some parentheses. What about something a bit different, like Num
? Well, as input, it only makes sense to pass Num
a Type
, such as Int
or Double
, or even Bool
, since Num
defines methods like negate :: Num a => a > a
. Since the function arrow takes Type
s as inputs (remember the kind of (>)
?) it means that a
must be a Type
. What does Num
return though? What is the kind of Num Int
? Well, this is a new kind called Constraint
. A type expression of kind Constraint
is any type expression that can appear on the left of a =>
in a type signature. For example, the following is legal:
what :: Num Bool => Bool
= True + False what
This is a bit of an odd example, but it’s an important one to understand. Bool
is a type of kind Type
, so it’s legal to pass it to Num
, and the resulting Constraint
is completely valid. Beyond that, unrelated to the kind system, in the definition for what
we’ve required that there be a Num Bool
instance in scope at the call site. Therefore we can safely use +
with our Bool
s. Of course, we can’t actually use what
without defining this instance. GHC will complain:
ghci> what
<interactive>:5:1: error:
• No instance for (Num Bool) arising from a use of ‘what’
• In the expression: what
In an equation for ‘it’: it = what
So Num
has kind Type > Constraint
since it takes a Type
and returns a Constraint
. So do many other familiar type classes such as Ord
, Show
, and Monoid
. An example of a typeclass with a more complex kind would be Functor
. Functor
doesn’t take a Type
. Ror example, it doesn’t make sense to write a Functor Bool
instance. To see exactly why let’s look at the type of fmap
:
ghci> :t fmap
fmap :: Functor f => (a > b) > f a > f b
Since a
appears next to a >
it must have kind Type
, and similarly f a
appears next to a >
so it must also have kind Type
. Therefore f
must have kind Type > Type
, which makes sense as f
could be Maybe
or Either Bool
for example. So Functor
takes a Type > Type
and yields a Constraint
, meaning it has kind (Type > Type) > Constraint
. Applicative
must also have the same kind, even without looking at its methods. If we look at the class header for Applicative
we see
class Functor f => Applicative f where
Since f
appears as an argument to Functor
it must have kind Type > Type
and therefore Applicative
must have kind (Type > Type) > Constraint
. For the same reason, Monad
must also have that kind.
Consider the following (trimmed down) definition of the Bifunctor
class:
class Bifunctor p where
bimap :: (a > b) > (c > d) > p a c > p b d
Without using GHCi, what is the kind of Bifunctor
? Don’t worry about understanding what the class is for, just focus on the kinds.
Haskell has an extension called MultiParamTypeClasses
. It allows for typeclass with multiple type parameters. For example, take the following class:
class Convert a b where
convert :: a > b
Convert
has kind Type > Type > Constraint
.
Consider the following (simplified) definition of the MonadReader
class:
class Monad m => MonadReader r m where
ask :: m r
Without using GHCi, what is the kind of MonadReader
?
Consider the following (simplified) definition of the MonadTrans
class:
class MonadTrans t where
lift :: Monad m => m a > t m a
Without using GHCi, what is the kind of MonadTrans
?
Hint: Recall MaybeT
from earlier. There is an instance MonadTrans MaybeT
defined in the transformers
library.
Define your own type such that it could potentially have a MonadTrans
instance, considering only the kinds involved.
Consider this curious function:
import Data.Kind (Type)
myId :: Type > Type
= x myId x
GHC accepts this code, which seems very odd. What’s Type
doing in a type signature? I thought Type
was a kind? In truth, in modern GHC there is no such thing as a kind. By that, I mean that there’s no separate “kind level” with its own special values. Rather, all kinds are just types. The >
in kinds is the normal function arrow. This is a relatively recent change introduced by the TypeInType
language extension in GHC 8.0 which later became the default way GHC handles kinds (deprecating the TypeInType
extension).
If we ask GHCi the kind of Type
we get back
ghci> import Data.Kind
ghci> :k Type
Type
So Type
is a normal type that could potentially have values. In actuality, it’s uninhabited like the Void
datatype is. As a reminder, Void
is an empty datatype, with no values (that don’t loop or crash when evaluated), defined as
data Void
Just like you can have undefined :: Void
and myId :: Void > Void
so too you can have undefined :: Type
and myId :: Type > Type
.
It’s best to think of “kinds” as a relationship between types. Type
“is the kind of” Int
. Type > Type
“is the kind of” Maybe
. The only thing that differentiates “a kind” from “a type” is that a kind is just a type that happens to be the kind of another type. As an analogy, “a parent” is a person that happens to “be the parent of” another person.
This is cool and all, but perhaps this will make more sense once we learn about DataKinds
.
Write a function with the type Void > Type
. I recommend taking a look at Data.Void
for this.
Write a function preposterous :: Type > a
. You can make use of the EmptyCase
extension for this.
DataKinds
Up until now, the only kinds we’ve seen are Type
/*
, Constraint
, and k1 > k2
where k1
and k2
are themselves kinds. Keep in mind that “kind” here means a type that is the kind of another type. The DataKinds
extension opens up the world of kinds and turns every algebraic datatype into a kind. As an example, normally Bool
is a type but not a kind. There is no type expression with Bool
as its kind. But with DataKinds
we get two new type expressions, 'False
and 'True
, which both have kind Bool
. Notice the tick marks, these are not the same as the termlevel values False
and True
, they exist on the type level. It also doesn’t make sense to have a value whose type is 'True
, since it only makes sense to have values if your kind is Type
and 'True
has kind Bool
, not Type
.
So what is this useful for? In general, DataKinds
is mostly useful in conjunction with other extensions to the type system, since those extensions are all about manipulating types, and DataKinds
gives us a plethora of new types to work with. You’ll notice that I’ve used them in every single one of my previous posts because they’re just that useful.
Just as an example, you can have a datatype that’s parameterized over a Bool
type parameter.
newtype NonZero (verified :: Bool) = NonZero Int
 ^^^^^^^^^^^^^^^^^
 This is a kind signature, which lets us tell GHC
 what kind `verified` has. Without it, GHC will infer the kind
 to be `Type`.
Then you can have code like
mkNonZero :: Int > NonZero False
= NonZero
mkNonZero
verifyNonZero :: NonZero False > Maybe (NonZero True)
NonZero 0) = Nothing
verifyNonZero (NonZero n) = Just (NonZero n)
verifyNonZero (
divSafe :: Int > NonZero True Int > Int
NonZero n) = div m n divSafe m (
This is a bit of a dumb example since having an unverified NonZero
isn’t very useful,
but you can imagine that it might be useful for something like an email address type, for example,
where an unverified email address is syntactically valid, while a verified email address is one
that has been verified to exist, perhaps by sending a verification email.
Write the data/newtype declaration for an Email
type as mentioned above.
Change the kind of the type parameter from Bool
to Verification
, where Verification
is
data Verification = Unverified  Verified
Write a hypothetical API for such an email type as type signatures. You can fill in the definitions with undefined
or typed holes if you wish.
There is much that this article hasn’t covered, such as PolyKinds
, TYPE r
, and more. The kind system pervades the GHC Haskell language more and more as more typelevel programming features are added. This adds many interactions which could each be enough to fill their own article.
One last thing I will note is that since GHC 8.10 you can give your type declarations what are called standalone kind signatures, enabled by the (gasp) StandaloneKindSignatures
language extension. They look much like type signatures do, but with the type
keyword in front of them. Some examples:
type Foo :: Type > Type
newtype Foo a = Foo [a]
type C :: Type > Constraint
class C a where
type NonZero :: Bool > Type
newtype NonZero bool = NonZero Int
 ^^^^
 This is a phantom type parameter of kind Bool
 using DataKinds.
Suppose you had a list that you wanted to split according to some predicate, like splitting it up into the portion that’s greater than 7, and the portion that’s less than 7. Then we could take advantage of the partition
function from Data.List
:
 A reminder, the type of partition is:
partition :: (a > Bool) > [a] > ([a], [a])
  Moves the elements of a given list that are greater than 7 to the front.
greaterThan7sFirst :: [Int] > [Int]
=
greaterThan7sFirst xs let (lessThans, greaterThans) = partition (>7) xs
in greaterThans ++ lessThans
Pretty clean, right? Well when we run it:
> greaterThan7sFirst [4,9,4,8,2,7,9,2,6]
ghci4,4,2,7,2,6,9,8,9] [
Oops! It looks like we put things in the wrong order. The fix is simple, of course, just swap the order in the tuple.
But it’d be nice if we could prevent these sorts of mixups from happening in the first place. Also,
it would be nice if we could reflect this in the type signature of partition
, that way we don’t even need to read the documentation
every time we want to remember which half of the tuple is which.
newtype
s” ApproachSo we want to reflect the different meanings of the lists in the type system. So we can create a couple of newtypes:
newtype TruePart a = Trues [a]
deriving (Eq, Ord, Show, Read)
newtype FalsePart a = Falses [a]
deriving (Eq, Ord, Show, Read)
Then our result tuple will simply contain a true part and a false part.
partitionTyped :: (a > Bool) > [a] > (TruePart a, FalsePart a)
=
partitionTyped p xs let (trues, falses) = partition p xs
in (Trues trues, Falses falses)
Already we have more clarity in the type signature. Now we can adjust our greaterThan7sFirst
function to use partitionTyped'
:
  Moves the elements of a given list that are greater than 7 to the front.
greaterThan7sFirst :: [Int] > [Int]
=
greaterThan7sFirst xs let (Falses lessThans, Trues greaterThans) = partitionTyped (>7) xs
in greaterThans ++ lessThans
Immediately we get a complaint:
• Couldn't match type ‘TruePart Int’ with ‘FalsePart a’
Expected type: (FalsePart a, TruePart a1)
Actual type: (TruePart Int, FalsePart Int)
This is great! The type checker has successfully saved us from our own foolishness. Fixing our function makes it type check:
  Moves the elements of a given list that are greater than 7 to the front.
greaterThan7sFirst :: [Int] > [Int]
=
greaterThan7sFirst xs let (Trues greaterThans, Falses lessThans) = partitionTyped (>7) xs
in greaterThans ++ lessThans
And testing it out, we have our desired behaviour:
> greaterThan7sFirst [4,9,4,8,2,7,9,2,6]
ghci9,8,9,4,4,2,7,2,6] [
This approach does have the downside that we’ve had to introduce two new datatypes to accomplish the task. Any instances we write for one of them need to be duplicated for the other. What if we had a single type that did both jobs?
Consider the following type instead:
{# LANGUAGE DataKinds #}
{# LANGUAGE KindSignatures #}
newtype Part (bool :: Bool) a = Part { unPart :: [a] }
This type has a seemingly extraneous type parameter with what appears to be a type signature on it, (bool :: Bool)
.
This field is indeed “extraneous” in a sense, but what it does is that when we construct a Part
we can choose
what we want bool
to be. If we have two values where the value of bool
is different, then we won’t be able to
confuse one for the other, since their types will be different, even if the actual runtime values are the same.
bool
is called a phantom type parameter, since it doesn’t appear on the righthand side of the =
.
So what sorts of values can bool
take on? Well, that syntax that looks like a type signature is actually a kind signature.
So bool
exists on the type level and thus has a kind that dictates what sort of values it can take on. In this case,
its kind is Bool
, so it can take on any of the values of Bool
, meaning True
or False
. This can be a bit confusing
since normally True
and False
are runtime values. The DataKinds
extension essentially creates two new types
called 'True
and 'False
with kind Bool
. These are separate from the value level True
and False
. The compiler
allows us to leave off the tick marks if it’s still clear what we’re referring to. ^{1}
What does this look like in practice? Here’s an example:
truePart :: Part True Int
= Part [1,2,3,4]
truePart
falsePart :: Part False Int
= Part [1,2,3,4]
falsePart
 The following gives a type error!
= [truePart, falsePart] parts
• Couldn't match type ‘'False’ with ‘'True’
Expected type: Part 'True Int
Actual type: Part 'False Int
So we can’t define parts
since it would have two elements of different types, even though
their contents are the same. This is a good thing, we can use this to implement
a typesafe partition
:
partitionTyped :: (a > Bool) > [a] > (Part True a, Part False a)
=
partitionTyped p xs let (trues, falses) = partition p xs
in (Part trues, Part falses)
There’s an issue when we go to implement greaterThan7sFirst
, though. We don’t have two
separate constructors to match on as we did with the newtypes. We can work around that,
but it’s not very ergonomic:
greaterThan7sFirst :: [Int] > [Int]
=
greaterThan7sFirst xs let greaterThans :: Part True Int
lessThans :: Part False Int
= partitionTyped (>7) xs
(greaterThans, lessThans) in unPart greaterThans ++ unPart lessThans
It would be much nicer to have separate constructors that we could match on like with the two newtypes. Turns out pattern synonyms are just the thing we need!
{# LANGUAGE PatternSynonyms #}
pattern Trues :: [a] > Part True a
pattern Trues xs = Part xs
{# COMPLETE Trues #}
pattern Falses :: [a] > Part False a
pattern Falses xs = Part xs
{# COMPLETE Falses #}
So pattern synonyms allow us to define, well, synonyms for existing patterns. In this case,
we’ve defined two patterns Trues
and Falses
that are the same as Part
. The only difference
is that we’ve restricted their types. If we ask GHCi the type of Part
we get:
> :t Part
ghciPart :: [a] > Part bool a
So of course, Part
can return a Part
with any value of bool
that we choose. In the case of Trues
we
want to restrict its type to only match when the type of the receiving value has a bool
parameter
equal to True
. By giving Trues
this restricted type we accomplish two things:
Trues
will have True
as the bool
type parameter.Trues
must have True
as the bool
type parameter.In this case, we’re concerned with the second behaviour, but the first is nice to have as well. So if we go into GHCi we can witness these two facts in action:
>  Fact 1:
ghci> :t Trues "abcd"
ghciTrues "abcd" :: Part 'True Char
>  Fact 2:
ghci> Trues t = truePart
ghci> Falses f = truePart
ghci
<interactive>:9:12: error:
Couldn't match type ‘'True’ with ‘'False’
• Expected type: Part 'False Int
Actual type: Part 'True Int
In the expression: truePart
• In a pattern binding: Falses f = truePart
The COMPLETE
pragmas above are there to tell GHC that when pattern matching, matching only on
Trues
(or only on Falses
) constitutes a complete pattern match, this way GHC doesn’t give
us a “missing patterns” warning if we don’t provide a fallback case.
Now we can implement greaterThan7sFirst
nicely:
greaterThan7sFirst :: [Int] > [Int]
=
greaterThan7sFirst xs let (Trues greaterThans, Falses lessThans) = partitionTyped (>7) xs
in greaterThans ++ lessThans
And if we mess up then we get an error:
greaterThan7sFirst :: [Int] > [Int]
=
greaterThan7sFirst xs let (Falses lessThans, Trues greaterThans) = partitionTyped (>7) xs
in greaterThans ++ lessThans
• Couldn't match type ‘'True’ with ‘'False’
Expected type: (Part 'False Int, Part 'True Int)
Actual type: (Part 'True Int, Part 'False Int)
Nice. As a bonus, let’s implement Show
instances that display the results
using our pattern synonym syntax. We’ll need FlexibleInstances
to convince GHC
that it’s okay to write separate instances for True
and False
.
{# LANGUAGE FlexibleInstances #}
instance Show a => Show (Part False a) where
showsPrec n (Falses xs) = showParen (n > 10) (showString "Falses " . shows xs)
instance Show a => Show (Part True a) where
showsPrec n (Trues xs) = showParen (n > 10) (showString "Trues " . shows xs)
> partitionTyped (>7) [4,9,4,8,2,7,9,2,6]
ghciTrues [9,8,9],Falses [4,4,2,7,2,6]) (
This particular motivating example may feel a bit overcomplicated for what it’s trying to do, and
indeed the solution with two newtypes was already quite satisfactory. The benefits to using phantom types
with DataKinds
really come up more in situations where you may want to work more with the type, unlike here where it exists purely to be pattern matched on immediately like in this example.
As an example, a while ago I wrote a Wordle solver that makes use of this technique to differentiate between words that represent a guess versus words that represent a possible solution. In that case, I had something like
 "Wordlet" is a cutesy word that I use to refer to a Wordle word.
data WordletType = Guess  Master
newtype Wordlet (wlty :: WordletType) = Wordlet { ... }
newtype WordList (wlty :: WordletType) = WordList { getWordList :: Data.Vector.Vector (Wordlet wlty) }
Because words and word lists are tagged, it means that on the one hand, I have type safety, so if I have a function like
againstMaster :: Wordlet Guess > Wordlet Master > Colors
which checks a given guess word against a candidate master word, then I can use it to write the following function, which checks if a given guess word and feedback to that guess word is consistent with some possible master word:
consistentWith :: Colors > Wordlet Guess > Wordlet Master > Bool
= guess `againstMaster` master == colors consistentWith colors guess master
This is just a small example, but the fact that words are tagged ensures that I can’t mess up
the argument order when applying againstMaster
. Another example would be the filterMasters
function, which filters a word list full of master word candidates to only those that
are consistent with a given guess word and response to that guess word
filterMasters :: Colors > Wordlet Guess > WordList Master > WordList Master
WordList masters) =
filterMasters colors guess (WordList $ Data.Vector.filter (consistentWith colors guess) masters
The types guide us towards a solution. We have a vector of WordList Master
s and we want
to filter it. So of course we need Data.Vector.filter
. The filtering function needs to combine
some Colors
, a Wordlet Guess
and a WordLet Master
in some way to give us a Bool
. This is
exactly consistentWith
. ^{2}
When I first added these phantom types to my code it indeed found a couple of places where I was implicitly confusing a master word for a guess word.
As far as how this is beneficial over a couple of newtypes, it means that I don’t need separate functions for dealing with guess words and master words in cases where it doesn’t matter. For example:
wordletToString :: Wordlet wlty > String
parseWordlet :: Text > Either Text (Wordlet wlty)
In both of these cases it doesn’t matter whether we’re dealing with a guess word or a master word, and the phantom types approach makes it very easy to be polymorphic over the type of word, since the implementation is the same, and thus they only involve a single function body.
So we’ve learned today that phantom types give us the ability to both communicate more in our type signatures, and they can help us prevent mistakes and misuses by allowing us to track where a value came from.
Link to the full code for this post
If we had another type in scope that happened to be called True
then we’d need to use ticks
in order for GHC to understand that we’re referring to the lifted version of the value True
and
not the type True
.↩︎
Presumably, I used againstMaster
directly first and then abstracted it out into consistentWith
, but
the point still stands againstMaster
is the obvious choice for comparing some Colors
combined
with a guess word and a master word, just by looking at the types involved.↩︎
How do you put on your shoes?
Do you put them both on and then tie them both? In what order? Do you put on one shoe, tie it and then do the other one? That might be a bit odd, but it’s acceptable. Anyone who ties either of their shoes before putting them on is a nogo, though. And since my Haskell file is my world, and GHC is my enforcer, let’s make this state of affairs unrepresentable.
Maybe
is an algebraic data type (ADT for short) that we all know and love. It’s defined as:
data Maybe a = Nothing  Just a
We can ask GHCi the types of the data constructors:
> :t Nothing
λNothing :: Maybe a
> :t Just
λJust :: a > Maybe a
Cool! This even defines what Maybe
is. It would also be pretty cool if we could define Maybe
that way.^{1}
data Maybe a where
Nothing :: Maybe a
Just :: a > Maybe a
Of course, the return types of these constructors have to be the same. But what if things didn’t have to be that way?
Consider the following data type:
data Expr a where
ABool :: Bool > Expr Bool
AnInt :: Int > Expr Int
If :: Expr Bool > Expr a > Expr a > Expr a
If we could make such a data type, then an expression like If (ABool True) (AnInt 5) (AnInt 7)
would be fine, while an expression If (AnInt 5) (AnInt 5) (AnInt 7)
wouldn’t typecheck, since If
expects an Expr Bool
as its first argument. Similarly, the following expression would also not typecheck: If (ABool False) (AnInt 6) (ABool 5)
. If
expects its second and third arguments to have the same type, which is not true in this case.
This sort of data type can indeed be defined if you enable the GHC GADTs
language extension, a GADT being a generalized ADT. If we didn’t index our expressions by their types like this, we wouldn’t have been able to prevent the programmer from constructing these invalid expressions.
data Expr = ABool Bool  AnInt Int  If Expr Expr Expr
= If (AnInt 8) (ABool True) (AnInt 4) thisTypeChecks
We can use a similar principle to construct valid shoetying methods.
First, we need to define what states a shoe can be in.
data ShoeState = Off  Untied  On
So a shoe is either completely off, on but untied, or on.
Let’s define what operations we can perform on our pair of shoes, by using a GADT.
data Shoes l r where
 Both shoes can be off:
OffLR :: Shoes Off Off
 If a shoe is off, we can put it on:
PutOnL :: Shoes Off r > Shoes Untied r
PutOnR :: Shoes l Off > Shoes l Untied
 If a shoe is on, but untied, we can tie it:
TieL :: Shoes Untied r > Shoes On r
TieR :: Shoes l Untied > Shoes l On
This is pretty cool. We’re defining a set of valid state transitions, and enforcing it in the type system.
One other thing to note here is that we’re using the DataKinds
extension to allow us to use these runtime values (Off
, Untied
, and On
) in our types. We could have defined empty dummy types with no constructors (e.g data Off
), but using DataKinds
will give us tighter control over the types and give better error messages when we mess up.
We can also define a valid shoe tying method to be a function from Off
shoes to On
shoes.
type Method = Shoes Off Off > Shoes On On
It almost reads like English, nice.^{2} One last thing is we’ll define a flipped composition operator so that we can compose our constructors from left to right, as opposed to (.)
which composes right to left.
>>>) = flip (.) (
Let’s define our first method.
rllr :: Method
= PutOnR >>> PutOnL >>> TieL >>> TieR rllr
It typechecks, great! Let’s see what happens when we try to tie our shoes before we put them on.
don't :: Method
= TieR >>> TieL >>> PutOnR >>> PutOnL don't
We get a bunch of type errors, which is exactly what we wanted.
Shoe.hs: error:
• Couldn't match type ‘'Off’ with ‘'On’
Expected type: Shoes 'On 'On > Shoes 'On 'Untied
Actual type: Shoes 'On 'Off > Shoes 'On 'Untied

 don't = TieR >>> TieL >>> PutOnR >>> PutOnL
 ^^^^^^
One last thing we can do is since these are runtime values, we could print them as userfriendly instructions.
describe :: Method > IO ()
= go (method OffLR) *> putStrLn "Done!"
describe method where
go :: Shoes l r > IO ()
OffLR = putStrLn "Alright."
go PutOnL x) = go x *> putStrLn "Put on the left shoe."
go (PutOnR x) = go x *> putStrLn "Put on the right shoe."
go (TieL x) = go x *> putStrLn "Tie the left shoe."
go (TieR x) = go x *> putStrLn "Tie the right shoe." go (
Note that the type signature on go
is required, the typechecker can’t infer it. This is part of the price we pay with GADTs, expressions don’t always have a principal^{3} type.
We can run it, and finally, get some instructions on how to put on and tie our shoes.
λ> describe rllr
Alright.
Put on the right shoe.
Put on the left shoe.
Tie the left shoe.
Tie the right shoe.
Done!
We can, in fact, do this with the GHC language extension GADTSyntax
, which will be on by default starting in GHC 9.2. Of course, we’re getting ahead of ourselves.↩︎
In fact, we won’t even need OffLR
to define a Method
. We will need it if we want to pattern match on them though, which we’ll do later when writing the describe
function.↩︎
Unambiguous and most general. A simple example is with our Expr
type from earlier:
ABool b) = b foo (
Both foo :: Expr a > a
and foo :: Expr a > Bool
are valid types to give foo
here, neither being more general than the other.↩︎
Suppose you wanted to write a type class for indexable containers. It might look something like this:
class Indexed f where
(!?) :: f a > Int > Maybe a
We could then go on to write instances for our favorite types:
instance Indexed [] where
!? _ = Nothing
[] :xs) !? n
(x n == 0 = Just x
 n > 0 = xs !? (n  1)
 otherwise = Nothing
instance Indexed Vector where
!?) = (V.!?)
(
instance Indexed IntMap where
!?) = (IntMap.!?) (
But now, we decide that we also want to be able to index Map
s as well. The issue is that we’ve already hardcoded the index type to be Int
.
One way to allow arbitrary index types is to use an associated type family:
{# LANGUAGE TypeFamilies #}
class Indexed f where
type Idx f
(!?) :: f a > Idx f > Maybe a
instance Indexed [] where
type Idx [] = Int
 ...
instance Ord k => Indexed (Map k) where
type Idx (Map k) = k
!?) = (Map.!?) (
Say we want to write the following function:
firstElem :: Indexed f => f a > Maybe a
= f !? (0 :: Int) firstElem f
We’d get a type error:
• Couldn't match expected type ‘Idx f’ with actual type ‘Int’
Fair enough, this function obviously won’t work for just any indexable container, only ones that are indexed by Int
s. We can resolve this by adding a constraint:
firstElem :: (Indexed f, Idx f ~ Int) => f a > Maybe a
= f !? (0 :: Int) firstElem f
For those unfamiliar, ~
here is an operator provided by GHC that yields a constraint that the type on the left equals the type on the right. In this case, by constraining Idx f
to equal Int
, GHC can now add that to its list of known facts, so we don’t get that type error anymore. ~
is enabled by both the TypeFamilies
extension and the GADTs
extension.
Another way we can encode this information, the type of the index, would be to add an additional parameter to the type class.
{# LANGUAGE MultiParamTypeClasses #}
class Indexed f idx where
(!?) :: f a > idx > Maybe a
instance Indexed [] Int where
 ...
instance Indexed (Map k) k where
!?) = (Map.!?) (
We can then write firstElem
in a straightforward manner.
firstElem :: Indexed f Int => f a > Maybe a
= f !? (0 :: Int) firstElem f
Yes! This type checks just fine.
All is good, right? Well, let’s try and use this instance.
c :: Maybe Char
= "hello" !? 3 c
Oh no! When we compile, we get errors:
• Ambiguous type variable ‘idx0’ arising from a use of ‘!?’
prevents the constraint ‘(Indexed [] idx0)’ from being solved.
Probable fix: use a type annotation to specify what ‘idx0’ should be.
• Ambiguous type variable ‘idx0’ arising from the literal ‘3’
prevents the constraint ‘(Num idx0)’ from being solved.
Probable fix: use a type annotation to specify what ‘idx0’ should be.
Now sure, if we add a type signature to the literal 3
, then the error goes away, but this is really not ideal. Ideally, we’d like the compiler to infer right away that since we’re indexing a list, the index type must be Int
. This is indeed what happens when we use an associated type family, but not here.
The core issue here is that there’s nothing stopping someone from writing their own instance:
instance Indexed [] Integer where
...
This is a completely valid instance that doesn’t overlap with our own. Therefore, GHC can’t infer that the literal 0
must be an Int
, since it can just as well be an Integer
, or any number of types, really.
The solution is to add a functional dependency.
{# LANGUAGE FunctionalDependencies #}
class Indexed f idx  f > idx where
(!?) :: f a > idx > Maybe a
x :: Maybe Char
= "hello" !? 3 x
What this syntax means is that the type variable f
must uniquely determine the type variable idx
. This has two effects. One, if we have an instance instance Indexed [] Int
, then we can’t go ahead and write another instance instance Indexed [] Integer
, since then f
doesn’t uniquely determine idx
. Two, GHC can now infer the type of idx
just from knowing what f
is, so our program will now type check. GHC will see we’re indexing a list, and therefore the index type must be Int
, so the type of the literal 3
here must be Int
.
Looking back, when we used a type family, we were doing the same thing, only implicitly. When we write Idx f
it’s implicit that Idx
gives back a single type when applied. In other words, f
uniquely determines Idx f
.^{1}
Both of these methods give very similar results, but it’s good to be familiar with both of them, as they can have different ergonomics. It’s also good to be able to recognize both of these patterns in other libraries, as they’re both used throughout Hackage.
Interestingly, we can go in the other direction with the TypeFamilyDependencies
extension.↩︎
calamity
is the most fullyfeatured library for writing Discord bots on Hackage, rivaling frameworks like discord.py
and discord.js
in features, while offering all the benefits of Haskell: a strong type system, pure functions, and riskfree refactoring. It may however seem impenetrable at first glance, using many language extensions such as TypeApplications
, TypeFamilies
, and DataKinds
. It also uses the polysemy
effect system rather than the more common mtl
. The end result, however, is a very nice interface for making bots.
This post is intended to guide those less familiar with this part of the Haskell ecosystem on how to write their own bots using calamity
and hopefully come out of the experience with a greater understanding of how to use these features and how they can be leveraged in their own programs and libraries. It’s possible to use calamity
without understanding all the types involved, but it becomes significantly harder to debug and to understand any error messages you may get.
I recommend following along with the Hackage documentation open. You can search the haddocks by pressing s
to open a search dialogue.
polysemy
When writing realworld IOheavy applications, it’s almost always advantageous to use an effect system for storing configuration data, error handling, incorporating state, etc. A common choice is mtl
, the monad transformer library. polysemy
is another such effect system that opts for working in a single monad, the Sem r
monad, which is parameterized by a typelevel list of effects, called the “effect row”.
Here’s an example snippet of code using mtl
:
data Config = Config
cmdPrefix :: Text
{...
, }
handleMessage :: (MonadReader Config m, MonadIO m)
=> String
> m ()
= do
handleMessage msg < asks cmdPrefix
prefix `isPrefixOf` msg) $
when (prefix $ putStrLn "Received a command: " <> msg liftIO
Using polysemy
we’d instead work in the Sem r
monad and require that the effect row contain the effects we need using Member
, which takes an effect and an effect row and yields a constraint that the effect must be present in the effect row.
handleMessage :: (Member (Reader Config) r, Member (Embed IO) r)
=> String
> Sem r ()
= do
handleMessage msg < asks @Config cmdPrefix
prefix `isPrefixOf` msg) $
when (prefix . putStrLn $ "Received a command: " <> msg embed
As you can see, all we have to do is declare which effects we require to be present in the effect row. polysemy
also encourages using more granular effects instead of using IO
, so we could have used the Trace
effect instead and called trace
function, which means all we require is that our effect row can log strings, whatever that may mean. This removes our dependency on the IO
monad and makes our code more flexible.
handleMessage :: (Member (Reader Config) r, Member Trace r)
=> String
> Sem r ()
= do
handleMessage msg < asks @Config cmdPrefix
prefix `isPrefixOf` msg) $
when (prefix $ "Received a command: " <> msg trace
How these effects are interpreted, and which concrete effect row is inferred depends on which effect interpreters we choose. For the Reader
effect there’s really only one interpreter that polysemy
provides to us, runReader :: i > Sem (Reader i ': r) a > Sem r a
. The type signature says that given some value i
we can handle a Reader i
effect from the effect row. Remember that the effect row is just a typelevel list of effects, so (Reader i ': r)
just pattern matches on that list, a list with Reader i
as its head and r
as its tail. So the interpreter essentially strips an effect off the head of the effect row.
For interpreting the Trace
effect we have a few options. We could gather all the logged messages into a list with runTraceList :: Sem (Trace ': r) a > Sem r ([String], a)
, we could also just ignore the messages with ignoreTrace :: Sem (Trace ': r) a > Sem r a
. But in this case we’ll have the messages printed to stdout
with traceToIO :: Member (Embed IO) r => Sem (Trace ': r) a > Sem r a
. Note that this interpreter requires that Embed IO
be present in our effect list, meaning that our monad must be able to handle arbitrary IO
actions. That’s fine, but now we’ll have to handle that effect as well. Since our goal is to eventually peel this onion into the IO
monad, we’ll want some function that can convert some Sem r a
into an IO a
. That function would be runFinal :: Monad m => Sem '[Final m] a > m a
which takes a Sem
with a singleton effect row containing just Final m
for some monad m
and returns a plain m a
. To get such a Final m
we can just use the embedToFinal :: (Member (Final m) r, Functor m) => Sem (Embed m ': r) a > Sem r a
interpreter, which peels off an Embed m
effect and delegates its effects to the Final m
effect in our effect row.
Putting it all together:
main :: IO ()
= runFinal
main . embedToFinal @IO
. traceToIO
. runReader (Config "!" ...)
$ handleMessage "!polysemy"
This may seem a bit complicated, but it’s a method that lets us easily deal with all the effects we need, one at a time. The result is very clean. Note that we never deal with an explicit effect row, the type checker infers it for us based on the interpreters we choose.
di
and dipolysemy
di
is a fullyfeatured structured logging library. dipolysemy
provides polysemy
style effects for di
. This is what calamity
uses for logging.
lens
For a full understanding of how lens works, there are better resources such as the excellent lens over tea. This will just be a quick overview of the basic usage of lenses for those who aren’t familiar with them.
A lens can be thought of as a value that focuses on some part of a larger structure, such as a field in a record. For example, lens
provides the _1
lens which focuses on the first element of a tuple. We can use a lens to extract part of a value with the view
function or its operator counterpart, (^.)
:
> view ('a', 5) _1
ghci'a'
> ('a', 5) ^. _2
ghci5
We can replace part of a structure with the set
function or its operator counterpart, (.~)
. We can use the flipped application operator (&) = flip ($)
alongside this operator.
> set _1 "hello" ('a', 5)
ghci"hello", 5)
(> ('a', 5) & _2 .~ False
ghci'a', False) (
And finally, we have over
and its operator %~
which work much the same way as set
, but takes a function to modify the structure with rather than a set value.
> over _1 succ ('a', 5)
ghci'b', 5)
(> ('a', 5) & _2 %~ show
ghci'a', "5") (
Notably, we can compose lenses with the (.)
operator from the Prelude
to focus on parts of nested structures.
> (True, ('a', 5)) ^. _2 . _1
ghci'a'
genericlens
The goal of using lens
here is to be able to access the fields of the various record types that calamity
exposes to us. We could use record accessors and update notation, but many of these records contain fields with the same name, so using them directly will lead to ambiguity errors. genericlens
solves this issue for us by allowing us to create lenses on the fly using the OverloadedLabels
extension. For example, the Member
type has a field called guildID
. If we want to access this field from some member mem
, we can simply write mem ^. #guildID
.
Note that to use the field names as lenses the type must have an instance of the Generic
typeclass. This can be done with the DeriveGeneric
extension, which, unsurprisingly, allows the user to write deriving Generic
. All, or at least most, of the record types defined in calamity
have a Generic
instance.
Let’s begin by building the skeleton for our project. Initialize a fresh cabal project by running cabal init
in an empty directory. We’ll start by putting the adding the following dependencies in our bot.cabal
file under builddepends
.
 Use a version of `base` corresponding with GHC 8.10.x
builddepends:
, base ^>=4.14
, calamity >=0.2.0 && <0.2.1
, datadefault
, dataflags
, di
, dipolysemy
, genericlens
, lens
, polysemy
, polysemyplugin
, text
, textshow
polysemyplugin
is a GHC plugin for polysemy
which improves type inference inside the Sem r
monad. We have to enable it by adding fplugin=Polysemy.Plugin
to our GHC options.
ghcoptions: fplugin=Polysemy.Plugin
Let’s also start with a couple of language extensions.TypeApplications
which lets us instantiate polymorphic values with concrete types, OverloadedStrings
to be able to write Text
literals, and OverloadedLabels
for use with genericlens
. We’ll also need DataKinds
later.
defaultextensions:
DataKinds
OverloadedLabels
OverloadedStrings
TypeApplications
We’ll build all these dependencies with cabal build onlydependencies
.
Let’s write the skeleton for the main
function.
module Main where
import Calamity
import Calamity.Cache.InMemory
import Calamity.Commands
import Calamity.Commands.Context (useFullContext)
import Calamity.Metrics.Noop
import Control.Lens
import Control.Monad
import Data.Default
import Data.Generics.Labels ()
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as T
import qualified Di
import DiPolysemy
import qualified Polysemy as P
main :: IO ()
= Di.new $ \di >
main
void. P.runFinal
. P.embedToFinal @IO
. runDiToIO di
. runCacheInMemory
. runMetricsNoop
. useFullContext
. useConstantPrefix "!"
. runBotIO (BotToken "<token>") defaultIntents
$ do
@Text "Setting up commands and handlers..." info
Let’s break this down bit by bit.
Di.new
has the type
new :: (Di Level Path Message > IO a) > IO a
It’s a bit more polymorphic than that, but we’re just using it in plain IO
. It essentially provides us a Di Level Path Message
using a continuation. That value can be thought of as a sort of handle to the logger.
Now for the effect interpreters.
 snip 
. runBotIO (BotToken "<token>") defaultIntents
 snip 
 `runBotIO` has the type:
runBotIO :: forall r a. ( Members '[Embed IO, Final IO, CacheEff, MetricEff, LogEff] r
Typeable (SetupEff r))
, => Token
> Intents
> Sem (SetupEff r) a
> Sem r (Maybe StartupError)
runBotIO
is the main effect interpreter. It may look daunting, but let’s break it down piece by piece. Firstly, we’ll note it takes a Token
and an Intents
as inputs. A Token
, as you can see in our skeleton, can be constructed using the BotToken
constructor. An Intents
is just a Word32
representing a bunch of binary flags. You can combine these intents and manipulate them with methods from the Flags
typeclass from the dataflags
package.
SetupEff r
is a type alias for Reader Client ': (AtomicState EventHandlers ': (Async ': r))
. What that means, in particular, is not relevant to us right now, just understand that it’s r
with extra effects tacked on.
Members '[Embed IO, Final IO, CacheEff, MetricEff, LogEff] r
is equivalent to (Member (Embed IO) r, Member (Final IO) r, ...)
, it’s just a more convenient way of listing them all.
So, what this whole function does is strip off some of the main effects but requires us to interpret a few other effects afterward. It’s the main interpreter, in a nutshell.
 snip 
. useConstantPrefix "!"
 snip 
 `useConstantPrefix` has the type:
useConstantPrefix :: Text > Sem (ParsePrefix ': r) a > Sem r a
Having this interpreter will strip off the ParsePrefix
effect, which will be required when we want to register commands later.
 snip 
. useFullContext
 snip 
 `useFullContext` has the type:
useFullContext :: Member CacheEff r => Sem (ConstructContext Message FullContext IO () ': r) a > Sem r a
useConstantPrefix
handles the ConstructContext
effect, which determines what information we receive when a command is invoked.
 snip 
. runMetricsNoop
 snip 
 `runMetricsNoop` has the type:
runMetricsNoop :: Sem (MetricEff ': r) a > Sem r a
runMetricsNoop
will strip off the MetricEff
effect by ignoring any metrics that were collected. calamity
doesn’t provide any other interpreter for the MetricEff
effect, but if we wanted to we could write our own. That’s outside the scope of this post, however.
 snip 
. runCacheInMemory
 snip 
 `runCacheInMemory` has the type:
runCacheInMemory :: Member (Embed IO) r => Sem (CacheEff ': r) a > Sem r a
runCacheInMemory
strips the CacheEff
for us by storing the cache in memory. If we wanted to we could write an effect interpreter that stores the cache in a file or database.
 snip 
. runDiToIO di
 snip 
 `runDiToIO` has the type:
runDiToIO :: forall r level msg a.
Member (Embed IO) r
=> Di level Path msg
> Sem (Di level Path msg ': r) a
> Sem r a
runDiToIO
will interpret our di
logging effect. Note that the LogEff
effect mentioned earlier is just a type alias for Di Level Path Message
, so this is just an interpreter for the LogEff
effect.
Let’s create an event handler that will do something wholesome, like react with 😄 on any message containing the string “Haskell”.
 snip 
$ do
@Text "Setting up commands and handlers..."
info
@'MessageCreateEvt $ \(msg, _usr, _member) > do
react "Haskell" `T.isInfixOf` (msg ^. #content)) $
when (. invoke $ CreateReaction msg msg (UnicodeEmoji "😄") void
react
is a function that registers an event handler.
react :: forall (s :: EventType) r.
BotC r, ReactConstraints s)
(=> (EHType s > Sem r ())
> Sem r (Sem r ())
The type of event we want to handle has to be passed in as a type argument using the TypeApplications
extension, which allows us to instantiate type variables to a specific concrete type. calamity
provides the EventType
datatype which is a simple enumeration, one of the values being MessageCreateEvt
. react
expects a type variable with the kind EventType
, meaning EventType
is used as a datakind. All this means is that the type EventType
is promoted from the type level to the kind level.^{1} Its values, such as MessageCreateEvt
, are promoted from the term level to the type level. Just as the type Int
has kind Type
, the type 'MessageCreateEvt
has the kind EventType
. Notice the tick mark on 'MessageCreateEvt
. This is to differentiate the value MessageCreateEvt
from the type. Usually, GHC can infer this without us explicitly specifying it, and indeed here it’s not required, but we’ll leave it in just to be explicit.
Also, note that the effect row that react
uses has a BotC
constraint which is just an alias for a few other constraints. We won’t go into the details here, but just know that the runBotIO
interpreter handles these constraints for us.
react
also expects a function as an argument, the handler body. The type of the input to the body depends on the EventType
we specified; different types of events have different data associated with them. This is determined by the EHType
type family. For those unfamiliar, a type family is essentially a function that operates on types rather than terms. In the case of EHType
, it takes a type with kind EventType
(remember, DataKinds
promotes EventType
to the kind level) and gives us a type. So in our case, EHType 'MessageCreateEvt = (Message, Maybe User, Maybe Member)
. That means our event handler’s body should have the type (Message, Maybe User, Maybe Member) > Sem r ()
, although we’ll only be using the Message
portion of that tuple. You can verify this in a GHCi session (by running cabal repl
in the terminal) with the command :kind! EHType 'MessageCreateEvt
which will tell you the kind of the argument and will also attempt to evaluate it.
λ> :kind! EHType MessageCreateEvt
EHType MessageCreateEvt :: *
= (Message, Maybe User, Maybe Member)
So for the actual body, we want to take the message and perform an action conditional on the message’s content. We use the when
function from Control.Monad
, which conditionally performs a monadic/applicative action.
when :: Applicative f => Bool > f () > f ()
To access the message’s content we use genericlens
to view
the content
field of the message (remember that (^.)
is just the infix counterpart to view
).
To create the action, we’ll need to create a “create reaction” request, specifically a ChannelRequest
.
CreateReaction
:: (HasID Channel c, HasID Message m)
=> c
> m
> RawEmoji
> ChannelRequest ()
CreateReaction
is polymorphic in its first two arguments. Rather than requiring a concrete Snowflake
for the channel and message to react to, which, it only requires that it’s possible to extract a Snowflake
from the inputs. It does this via a typeclass, HasID
, which has two type parameters the type of the Snowflake
we wish to extract and the type of the thing to extract from. It has a single method, HasID b a => getID :: a > Snowflake b
. If we look at the instances for HasID
we see that there are indeed instances HasID Channel Message
and HasID Message Message
. To illustrate the convenience that this technique gives us, this is what our handler would look like if CreateReaction
required concrete Snowflake
s.
 snip 
. invoke $ CreateReaction (msg ^. #channelID) (msg ^. #id) (UnicodeEmoji "😄") void
Okay, so now that we have our ChannelRequest
we can use the invoke
function to run it.
invoke :: (BotC r, Request a, FromJSON (Result a))
=> a
> Sem r (Either RestError (Result a))
Here, Request
is a typeclass for which every ChannelRequest a
has an instance, and Result
is a type family which extracts the type a
from a ChannelRequest a
.
In our case, since we don’t need the result of the request, we can discard it using the void
function from Control.Monad
.
void :: Functor f => f a > f ()
One last thing to note about react
is that it returns an action that you can use to unregister it, hence the output being Sem r (Sem r ())
.
That’s it! That’s our event handler. You can try it out yourself by putting your bot token in the runBotIO
interpreter and running the project.
Let’s add a command to enable (or disable) slow mode at a given duration, optionally specifying a channel, defaulting to the channel the command was invoked in.
Luckily for us, calamity
provides a handy DSL for creating commands.
 snip 
. invoke $ CreateReaction msg msg (UnicodeEmoji "😄")
void
$ do
addCommands
helpCommand@'[Int, Maybe GuildChannel] "slowmode" $ \ctx seconds mchan > do
command let cid = maybe (ctx ^. #channel . to getID) getID mchan :: Snowflake Channel
. invoke $ ModifyChannel cid $ def
void & #rateLimitPerUser ?~ seconds
. invoke $
void CreateReaction (ctx ^. #channel) (ctx ^. #message) (UnicodeEmoji "✅")
Registering commands is done with the addCommands
function.
addCommands :: (BotC r, Member ParsePrefix r)
=> Sem (DSLState r) a
> Sem r (Sem r (), CommandHandler, a)
It constructs the commands and registers the proper event handlers to handle them. It requires the ParsePrefix
effect to be present in the effect row. It also allows extra effects in the input’s effect row which are used to track the state of the command DSL, using the type alias DSLState
. It yields an action to unregister the event handlers, and an object representing the commands that were registered.
helpCommand
will add a default help command in the DSL.
To create a command, we use the command
function
command :: forall ps r.
Member (Final IO) r, TypedCommandC ps r)
(=> Text
> (Context > CommandForParsers ps r)
> Sem (DSLState r) Command
command
takes a command name and a command body and adds a command in the DSL. It also takes a typelevel list of command arguments, which it uses to parse the inputs and compute the type of the command body.
In our case, we pass the typelevel list '[Int, Maybe GuildChannel]
(using TypeApplications
) which means our command requires an Int
and optionally a GuildChannel
.
The body of the command will take a Context
and any type parameters we pass in will also be the types of arguments to the body (computed by the CommandForParsers
type family).
We use the maybe
function to get a default channel id either the id of the channel provided or the id of the channel the message was invoked in, which is extracted from the Context
. We need the id specifically even though the ModifyChannel
request only requires something which has a HasID Channel
instance, since maybe
requires the output types to match up.
We invoke the ModifyChannel
request.
ModifyChannel
:: HasID Channel c
=> c
> ChannelUpdate
> ChannelRequest Channel
It requires a ChannelUpdate
as input.
data ChannelUpdate = ChannelUpdate
name :: Maybe Text
{ position :: Maybe Int
, topic :: Maybe Text
, nsfw :: Maybe Bool
, rateLimitPerUser :: Maybe Int
, bitrate :: Maybe Int
, userLimit :: Maybe Int
, permissionOverwrites :: Maybe [Overwrite]
, parentID :: Maybe (Snowflake Channel)
, }
In our case, we just need the rateLimitPerUser
field. Luckily, ChannelUpdate
implements the Default
typeclass, and so it provides a default value def
in which all the fields are set to Nothing
. We can then use lens
to update just the field we need. We can use the (?~)
operator, which is a convenience function that set
s a field to Just
the value on the righthand side.
We also react to the command invocation, just like we did in our event handler, this time using the Context
instead of a Message
.
The parser for the command is quite intelligent, so when we come to run the command, we can pass in either a link to the channel (ala #channelname
) or the id of the channel.
tell
Functioncalamity
has a typeclass called Tellable
, which has a method getChannel
.
getChannel :: (BotC r, Member (Error RestError) r)
=> a
> Sem r (Snowflake Channel)
Any type which has an instance of Tellable
can be sent a message with the tell
function.
tell :: forall msg r t.
BotC r, ToMessage msg, Tellable t)
(=> t
> msg
> Sem r (Either RestError Message)
It also requires the item being sent to have a ToMessage
instance. Many types have this instance, among them the various string types, files, embeds, and mentions.
Let’s create an event handler that responds to any message edit with “Hey! I saw that!”.
 snip 
@'MessageUpdateEvt $ \(_oldMsg, newMsg, _user, _member) > do
react . tell @Text newMsg $ "Hey! I saw that!"
void  snip 
Another useful function to check out is reply
, which will use Discord’s reply system to reply to a message.
Snowflake
s with upgrade
It can be a hassle to get a value if you only have access to its id. calamity
provides us with the Upgradable
typeclass, which provides the upgrade
method.
class Upgradeable a ids  a > ids, ids > a where
upgrade :: BotC r => ids > P.Sem r (Maybe a)
upgrade
will take your id(s) and search the cache for the corresponding value, making a request if it’s not in the cache. There are several useful instances defined, all of which take one or a pair of Snowflake
s and provide a complete value.
Upgradeable GuildChannel (Snowflake GuildChannel)
Upgradeable Member (Snowflake Guild, Snowflake Member)
Calamity.Utils
provides many useful functions: permissions calculations, message formatting, and colors for custom embeds. It’s worthwhile to check it out.
There are many things we didn’t get to cover in this post. We only scratched the surface of the commands DSL, we didn’t cover metrics collection, presences, nor the countless datatypes that are defined to model the Discord API. We also didn’t cover most of the effects available in the polysemy
library, which could come in handy for storing configuration data, state, etc. But that’s where the documentation comes in. If you’re new to developing realworld Haskell programs, learning how to read the haddocks is invaluable. (A tip: Don’t forget to read the instances!)
If there’s further interest in this I may be inclined to write another post where I write a more fullyfeatured bot and step through the process, involving other libraries like aeson
and database libraries, and defining our own polysemy effects and effect interpreters.
The full source code for the bot is available here.
This is somewhat inaccurate as of TypeInType
, in which the kind level and type level have been unified. A more precise way of saying this is that DataKinds
gives us the type level entity 'MessageCreateEvt
, whose kind is EventType
, which previously was uninhabited at the type level.↩︎