Haskell is a wonderful language and one of the features that makes it so great is its typeclass system.
Two of the simplest and most useful are Monoiod
and Semigroup
.
class Semigroup a where
(<>) :: a -> a -> a -- must be associative
class Semigroup a => Monoid a where
mempty :: a -- identity element
Semigroups are ubiquitous, but we quickly run into an issue when a type has two equally natural definition of (<>)
.
For example, Bool
is a semigroup with (<>) = (||)
or (<>) = (&&)
.
Idris solves this with named typeclasses.
In Haskell, we have to do a little more work.
newtype All = All { getAll :: Bool }
newtype Any = Any { getAny :: Bool }
Now we can define a Semigroup
and Monoid
instance for each.
instance Semigroup All where
(All p) <> (All q) = All (p && q)
instance Monoid All where
mempty = All True
instance Semigroup Any where
(Any p) <> (Any q) = Any (p && q)
instance Monoid Any where
mempty = Any False
Using these, we can write and
and or
with mconcat
.
and :: [Bool] -> Bool
and = getAll . mconcat . map All
-- foldl (&&) True
or :: [Bool] -> Bool
or = getAny . mconcat . map Any
-- foldl (||) False
In both cases, we wrap our input to select a particular Monoid
instance, apply mconcat
and revert back to the bare type.
Our “wrap” and “unwrap” operations do not actually modify any data, they just modify the type.
Haskell has a typeclass in Data.Coerce
just for this.
class Coercible a b where
coerce :: a -> b
instance Coercible Any Bool where
coerce = getAny
instance Coercible Bool Any where
coerce = Any
instance Coercible All Bool where
coerce = getAll
instance Coercible Bool Allwhere
coerce = All
The class Coercible
is not a typical typeclass, we cannot actually write instances of it ourselves.
The compiler generates the instances automatically and requires that the types have identical representations.
What makes coerce
so useful is that Haskell know it does nothing to the actual values, so (coerce :: Bool -> Any) True
gets optimized away and is not actually called.
We are not limited to just plain newtypes.
For example, Coercible a b
implies Coercible [a] [b]
.
Now we can rewrite both and
and or
as coerce . mconcat . coerce
and force GHC to use the correct monoid by giving type hints. The TypeApplications
extension makes this more convenient.
This pattern of coercion, application, then coercion is extremely common.
via :: forall a b c d
. (Coercible c a, Coercible b d)
=> (a -> b) -> c -> d
via f = (coerce :: b -> d) . f . (coerce :: c -> a)
and', or' :: [Bool] -> Bool
and' = via @[All] @All mconcat
or' = via @[Any] @Any mconcat
Here we gave both types, but via @[All]
or via @_ @All
would work as well, GHC will infer the rest.
Iceland Jack, author of DerivingVia, recently proposed ApplyingVia, a language extension that extends this approach and bakes it into GHC.
I strongly encourage reading through his proposal, there are some incredible examples in there.
This post is meant to give a rough sense of the style of ApplyingVia
using relatively straight forward haskell.
The type Num a => a
has a similar ambiguity to Bool
in that we could define (<>) = (+)
or (<>) = (*)
.
In Data.Monoiod
we have the types Sum
and Product
to give us those Monoid
instances.
newtype Sum a = Sum { getSum :: a }
instance Num a => Monoid (Sum a) where
mappend = via @a (+)
mempty = Sum 0
newtype Product a = Product { getProd :: a }
instance Num a => Monoid (Product a) where
mappend = via @a (*)
mempty = Product 1
Now we can rewrite product
and sum
in this style.
product, sum :: forall a. Num a => [a] -> a
product = via @_ @(Product a) mconcat
sum = via @_ @(Sum a) mconcat
Note the explicit forall a
, we need a
to be in scope so we can pass it to via
.
For any ordered type a
, we have two natural semigroups with (<>) = min
and (<>) = max
.
Ind Data.Semigroup
we ind types Min
and Max
as below.
newtype Min a = Min { getMin :: a }
instance Ord a => Semigroup (Min a) where
(<>) = via @a min
newtype Max a = Max { getMax :: a }
instance Ord a => Semigroup (Max a) where
(<>) = via @a max
Now we can see that minimum
and maximum
are both just sconcat
with respect to different semigroups.
minimum :: forall a. Ord a => NonEmpty a -> a
minimum = via @_ @(Min a) sconcat
maximum :: forall a. Ord a => NonEmpty a -> a
maximum = via @_ @(Max a) sconcat
For any non-commutative semigroup, we can define a new semigroup by reversing the arguments.
newtype Dual a = Dual { getDual :: a }
instance Semigroup a => Semigroup (Dual a) where
(<>) = via $ flip (<>)
We can use this to append lists in reverse order.
reverse :: forall a. [a] -> [a]
reverse = via @_ @(Dual [a]) mconcat . fmap (:[])
Note that we have to fmap (: [])
because we do not have Coercible a [a]
as they have different internal representations.
We can use more than just mconcat
. These wrappers show up wherever we find typeclass instances with no “most natural” definition. For instance, (<*>) :: [a -> b] -> [a] -> [b]
has two obvious possible definitions.
We can give an alternate definition with the ZipList
type.
instance Applicative ZipList where
(ZipList fs) <*> (ZipList xs) = zipWith fs xs
pure x = ZipList [x]
We have a Coercible (ZipList a) [a]
instance, which allows for the following.
transpose :: forall a. [[a]] -> [[a]]
transpose = via @(ZipList (ZipList a)) sequenceA
zipWith :: forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith = via @(ZipList a) . liftA2
These wrappers also allow us to write more expressive constraints.
For example, the Num
class is a famously poor design that implies an additive group, a multiplicative semigroup as well as abs
and signum
for some reason. We can define a much more reasonable hierarchy of structures and overloaded functions.
(+) :: forall a. Monoid (Sum a) => a -> a -> a
(+) = via @(Sum a) (<>)
zero :: forall a. Monoid (Sum a) => a
zero = (coerce :: Sum a -> a) mempty
(*) :: forall a. Monoid (Product a) => a -> a -> a
(*) = via @(Product a) (<>)
unit :: forall a. Monoid (Product a) => a
unit = (coerce :: Product a -> a) mempty
class Monoid g => Grp g where
inv :: g -> g
(-) :: forall a. Grp (Sum a) => a -> a -> a
a - b = a + (via @(Sum a) inv $ b)
(/) :: forall a. Grp (Product a) => a -> a -> a
a / b = a * (via @(Product a) inv $ b)