Tech News
← Back to articles

Death to Type Classes

read original related products more articles

Death ( XIII ) Symbolizes significant change, transformation, and endings, rather than literal physical death.

Have you ever seen a Number grazing in the fields? Or a Functor chirping in the trees? No? That’s because they’re LIES. LIES told by the bourgeoisie to keep common folk down. But I say NO, no longer shall we be kept down by deceit! Come brothers and sisters, come and let us create a system of values. Where values are no longer constrained by their type class, but instead merged as a signature into a module. Come comrades, let us open the Backpack.

Here we explore an alternative universe where we neglect the existence of type classes in favor of the Backpack module system. This ends up looking like OCaml in Haskell. Let us begin with Functor.

signature Death . Functor . Signature ( Functor , map ) where import Prelude () data Functor a map :: ( a -> b ) -> Functor a -> Functor b

This is a functor😼, and this is also a functor🐫. Functor😼 is the categorical functor where we embed one category into another. In this case, the category is that of sets and functions, where types are the sets and functions are the uh, functions. But it’s also an OCaml module functor🐫, where the data keyword introduces a hole into a signature, which we can later fill in with a proper type.

We’ve got to hide Prelude because the Functor type class from base gets imported by default. Signatures like the one just introduced can be used by importing them as if they were normal modules. All a signature does is promise to the compiler we’ll make a proper module for that later. We can just start using our functor right now. For example, in that same impl, package I’ve an auxiliary module for Functor, providing some utilities:

module Death.Functor ( module X , ( <$> ) , ( <$ )) where import Death.Functor.Signature as X import Prelude ( const ) ( <$> ) :: ( a -> b ) -> Functor a -> Functor b ( <$> ) = map ( <$ ) :: b -> Functor a -> Functor b ( <$ ) b = map ( const b )

As long as a module implements the signature, you get the stuff that depends on it implemented “for free”. The code depending on the signature is abstract. You want to keep your signatures small so more code is abstract, similar to how you want to keep typeclass definitions small so you can have smaller instances, keeping code that depends on the typeclass abstract. Let’s make an “instance” of our Functor signature, for the Maybe datatype, with the power of modules:

module Death.Functor.Maybe ( Functor , map ) where import Prelude ( Maybe ( .. ), ( $ )) type Functor = Maybe map :: ( a -> b ) -> Functor a -> Functor b map fab = \ case Just x -> Just $ fab x Nothing -> Nothing

Now it’s worth pointing out that we’ve got to do a fair bit of cabal work to make the compiler realize the instance. In Cabal, our main library with the signatures looks like this:

... continue reading