Typefamily gotchas
Typeclasses are an amazing abstraction in haskell. They are one of the
fundamental building blocks in sharing and managing code. Among other
points of mention, some of the benefits typeclasses have over OO-styled
interfaces include the ability to create objects out of nothing (like
mempty for the Monoid typeclass) and the ability to lazily add
polymorphism at compile time (ie: fromInteger :: Integer -> a in which
we define some fromInteger 1 :: Double later on in the program.
Typefamilies allow us to introduce associated types to typeclasses, bringing our code to another level by having locally defined type-dependence.
class MakesSeries s where type Emitted s data State = A | B instance MakesSeries State where type Emitted State = String
Notice that the typefamily defines a type-level function: when you apply
a State to an Emitted, you get out Text. Here, we’re building out
a typeclass that can take in some defined State (generically, just
s), and each time you step through the series, it emits something. It
could be text (like right now), or json, ints… anything you’d like.
If we’re not dealing with text, however, it might be worth having some
way to define how to serialize things that are Emitted:
class MakesSeries s where type Emitted s serialize :: s -> Emitted s -> String data State = A | B instance MakesSeries State where type Emitted State = Int serialize _ int = show int
But we don’t actually care about what the state is, we just care about
how we show the emitted values. Can’t we just drop it?
class MakesSeries s where type Emitted s serialize :: Emitted s -> String
Error, nope! Why is this? Well, serialize doesn’t uniquely define an
s anymore! What if we had some instance MakesSeries State2? The
compiler wouldn’t understand which Emitted s it should be using
because (I’m guessing) there is eta reduction happening behind the
scenes! We can fix this two different ways.
In the first way, we move our type family to a data family. Basically, this is the same as a type family, except we are creating an ADT instead of a type alias with our function:
class MakesSeries s where data Emitted s -- notice the change here serialize :: Emitted s -> String data State = A | B instance MakesSeries State where newtype Emitted State = Value Int serialize (Value int) = show int
But if we have to write lots of emitted values, that’s going to be a handful of repeated data constructions.
Alternatively, we can use a Proxy from Data.Proxy:
class MakesSeries s where type Emitted s serialize :: Proxy s -> Emitted s -> String data State = A | B instance MakesSeries State where newtype Emitted State = Value Int serialize _ int = show int -- ...later, in action: foobar = serialize (Proxy :: Proxy State) 2
For my use-case, I opted for the latter. You can see the code at
stites/test-machines.
Another thing you might see is the use of a proxy for a
ScopedTypeVariables declaration. This was used so that I can cast some
kind of mempty-defined value without having to pass in an actual
parameter.
On a tangential noted, there’s a lot of type-level hackery you can get
away with in Haskell, and I should write something on TypeRef later
for dynamic, dispatch-styled typing.