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.