We wish to associate an AppId/QueueType pair not only with a queue name, but also with a type. Ultimately, we would like it if GHC would reject code that tries to put the wrong type on the wrong queue.

That is, we wish to create a mapping from the two data types to a message payload type. In Haskell, to write such a mapping we should use TypeFamilies .

Now we get to the point of this article: we need to somehow write a function from data constructors (values) to types. In vanilla Haskell we cannot do that; data and types live in totally disjoint universe and you can’t have them interact.

In modern GHC, there happens to be a way to have your cake and eat it, too. We can promote data constructors from the value level to the type level by using the DataKinds extension. When we do this, the data type can be used as a new kind, and its constructors can be used as type constructors.

Manufacturing types out of thin air!

We create two new universes of type s— kinds — using the same syntax we use to declare a data type, and then declare a type family which maps pairs of those types to a concrete Type . Note the overloading of the :: syntax to create kind signatures (an ability implicitly enabled by the DataKinds extension) instead of the usual type signatures.

Type (also called * , although the poor GHC parser sometimes gets confused by this symbol when we start doing funky stuff) is a special kind, since type constructors of this kind can be inhabited: the type Char is of kind Type , and can be occupied by values such as 'a' , '1' , &c. We’re creating our own two new kinds — AppId and QueueType — which will have types, but those types will not have any values. Sounds completely useless, but it is immensely powerful.

We can now associate types to combinations of AppId s and QueueType s:

FizzBuzz takes numbers and produces text!

In practice, these instances may be declared in disparate modules, so we use an open type family (there is no where in the declaration of the family).

Note the use of prime marks ( ' ) on the type names. These are strictly not necessary, but they help us visually distinguish between using, for example, Doer as a value and as a type. It just so happens that we won’t ever use the regular data types, anyway.

Now, let’s wire up these types such that sendMessage and receiveMessage functions are better typed! We will leave the implementations blank for now:

o my!

This is how we make our API type-driven. We essentially write a type-level program, by telling GHC that we have an AppId and a QueueType . We then tell GHC to find a type c such that c is equal to AssociatedQueueType a b , and GHC will eventually find the relevant AssociatedQueueType instance and figure out the result type. We are directing the GHC type constrained solver, by declaring the properties of the types we want, and — at compile time — GHC will tell us if it couldn’t make all the constraints work together: if our code is badly typed!

We need to enable ExplicitForAll , to allow us to write the kind signatures. We also need to enable the scary-sounding AllowAmbiguousTypes . Without the latter extension enabled, we will get strange errors that look like this:

GHC helpfully tells us what to do here…

As a heuristic, the ambiguous types’ error pops us when we declare type variables but do not associated them with parameters or the result (to the left of the final => ). Haskell’s type resolution is “two-sided”: one side “pushes” a certain type, the other “pulls” another type, and our program typechecks if both sides can be made to agree (unified). I’m furiously waving my hands here because I actually haven’t the foggiest inkling how type resolution works in GHC.

In this case, GHC is rightly worried that, since there are no parameters or results of type a and b in our function definition, there won’t be a way for us to specify — in vanilla Haskell98 — at the call site what that side of the equation is, by providing explicit parameters of concrete types, or using type annotations to “pull” the types we want.

By enabling AllowAmbiguousTypes we tell GHC that we know what we’re doing. But if there are any issues they will be found by the compiler, so we’re not making our program as a whole less typesafe.

At call sites, we can have our cake and eat it. We resolve the ambiguity, and make our message passing type-driven, by utilising the TypeApplications extension. Since the a and b type variables do not exist after the => , they are invisible to the caller; we make them visible with TypeAllications — a “visibility override.” Behold:

I have to admit: that is some bad-ass syntax right there

Now GHC knows what the a and b type variables should be instantiated to, and type-checking can continue since the ambiguity has been rectified. Note also that we don’t need explicit type annotations for the messages anymore — the calls are fully type-driven!

Now, to get back to the implementations. The hedis library is unsophisticated: it merely wants a queue name and will happily push and pop ByteString s as much as we want. But we need to provide it with a queue name.

We have an issue here: obviously we want each app/queue type combination to have its own queue name, so we ultimately need to somehow map a pair of types (say, @'Doer and @'InputQueue ) to a string (the queue name).

Recall that our old getQueueName simply used the Show instances of the data types. With our API, we don’t ever want to use the data constructors, since we would be duplicating data (once for the data constructor, and once for the type application; to jump directly from one to the other we would need singletons , and even the guy who wrote the library admits that they’re ugly and terrible).

Modern GHC has a bit of a hack we can use, though. We cannot automatically convert a data-constructor-cum-promoted-type-constructor to a string, but we can associate them with Symbol s (type-level strings), and then have GHC convert the Symbol s into regular String s (which we will very quickly convert to ByteString s).

This is some dark juju…

If you blink, you might almost miss it. Symbol is a kind, whose residents are unoccupied types. The residents of Symbol look an awful lot like regular Haskell String s. Each one of these “strings” is actually an entire type, which GHC magically produces for us. Unlike our data kinds, the Symbol kind does not have a finite, closed universe of types. By the way, somebody (who knows how to use TH) should really write a library to automatically create a type-level show function for data kinds…

That’s not where the magic ends. GHC also allows us to convert Symbols into actual, value-level String s using the symbolVal function. So now, we can write a “dependently-typed” version of getQueueName :

Seriously: this is magic

Note that I’m using toS from Protolude to automatically convert from String to ByteString . Let me just say: I think the type for symbolVal is ugly — Proxy types serve to make types visible, just like TypeApplications — but I guess the writers did not want to force users to enable TypeApplications to use this function, so they added what is essentially a dummy parameter. Also, the KnownSymbol constraints feel icky.

Now we can write our two API functions properly (with those icky KnownSymbol constraints infecting everything):

‘@_@

Now if we accidentally write msg :: (QMessage Char) <- receiveMessage @'Doer @'InputQueue in the listener or sendMessage @’Doer @’InputQueue $ QMessage 'x' in the sender, then at compile-time GHC will complain about expecting an Int and getting a Char instead, just like we wanted all along. All of that just because I confused my letters with my letters…

For completeness, here’s the full dependently-typed code: