Higgledy 📚

Higher-kinded data via generics: all* the benefits, but none* of the
boilerplate.
Introduction
When we work with higher-kinded
data, we find
ourselves writing types like:
data User f
= User
{ name :: f String
, age :: f Int
, ...
}
This is good - we can use f ~ Maybe
for partial data, f ~ Identity
for
complete data, etc - but it introduces a fair amount of noise, and we have a
lot of boilerplate deriving to do. Wouldn't it be nice if we could get back to
writing simple types as we know and love them, and get all this stuff for
free?
data User
= User
{ name :: String
, age :: Int
, ...
}
deriving Generic
-- HKD for free!
type UserF f = HKD User f
As an added little bonus, any HKD
-wrapped object is automatically an instance
of all the Barbie classes, so no
need to derive anything more than Generic
!
API
All examples below were compiled with the following extensions, modules, and
example data types:
module Main where
import Control.Applicative (Alternative (empty))
import Control.Lens ((.~), (^.), (&), Const (..), Identity, anyOf)
import Data.Generic.HKD
import Data.Maybe (isJust, isNothing)
import Data.Monoid (Last (..))
import GHC.Generics (Generic)
import Named ((:!), (!))
data User
= User
{ name :: String
, age :: Int
, likesDogs :: Bool
}
deriving (Generic, Show)
user :: User
user = User "Tom" 26 True
data Triple
= Triple Int () String
deriving (Generic, Show)
triple :: Triple
triple = Triple 123 () "ABC"
The HKD type constructor
The HKD
type takes two parameters: your model type, and the functor in which
we want to wrap all our inputs. By picking different functors for the second
parameter, we can recover various behaviours:
type Partial a = HKD a Last
type Bare a = HKD a Identity
type Labels a = HKD a (Const String)
NB: as of GHC 8.8, the Last
monoid will be removed in favour of Compose Maybe Last
(using the Last
in Data.Semigroup
). Until then, I'll use Last
for brevity, but you may wish to use this suggestion for future-proofing.
Fresh objects
When we want to start working with the HKD
interface, we have a couple of
options, depending on the functor in question. The first option is to use
mempty
:
eg0 :: Partial User
eg0 = mempty
Other 'Alternative'-style functors lead to very different results:
eg1 :: Labels Triple
eg1 = mempty
Of course, this method requires every field to be monoidal. If we try with
Identity
, for example, we're in trouble if all our fields aren't themselves
monoids:
eg2 :: Bare Triple
eg2 = mempty
-- error:
-- • No instance for (Monoid Int) arising from a use of ‘mempty’
The other option is to deconstruct
a complete object. This effectively lifts
a type into the HKD
structure with pure
applied to each field:
eg3 :: Bare User
eg3 = deconstruct user
This approach works with any applicative we like, so we can recover the other
behaviours:
eg4 :: Partial Triple
eg4 = deconstruct @Last triple
There's also construct
for when we want to escape our HKD
wrapper, and
attempt to construct our original type:
eg5 :: Last Triple
eg5 = construct eg4
If none of the above suit your needs, maybe you want to try build
on for
size. This function constructs an HKD
-wrapped version of the type supplied to
it by taking all its parameters. In other words:
eg6 :: f Int -> f () -> f String -> HKD Triple f
eg6 = build @Triple
eg7 :: HKD Triple []
eg7 = eg6 [1] [] ["Tom", "Tim"]
Should we need to work with records, we can exploit the label trickery of the
named
package. The record
function behaves exactly as build
does, but produces a function compatible
with the named
interface. After that, we can use the function with labels
(and with no regard for the internal order):
eg8 :: "name" :! f [Char]
-> "age" :! f Int
-> "likesDogs" :! f Bool
-> HKD User f
eg8 = record @User
eg9 :: HKD User Maybe
eg9 = eg8 ! #name (Just "Tom")
! #likesDogs (Just True)
! #age (Just 26)
If you're still not satisfied, check out the
buniq
method hiding in barbies
:
eg10 :: HKD Triple []
eg10 = bpure empty
Field Access
The field
lens, when given a type-applied field name, allows us to focus on
fields within a record:
eg11 :: Last Int
eg11 = eg0 ^. field @"age"
As this is a true Lens
, it also means that we can set values within our
record (note that these set values will also need to be in our functor of
choice):
eg12 :: Partial User
eg12 = eg0 & field @"name" .~ pure "Evil Tom"
& field @"likesDogs" .~ pure False
This also means, for example, we can check whether a particular value has been
completed for a given partial type:
eg13 :: Bool
eg13 = anyOf (field @"name") (isJust . getLast) eg0
Finally, thanks to the fact that this library exploits some of the internals of
generic-lens
, we'll also get a nice type error when we mention a field that
doesn't exist in our type:
eg14 :: Identity ()
eg14 = eg3 ^. field @"oops"
-- error:
-- • The type User does not contain a field named 'oops'.
Position Access
Just as with field names, we can use positions when working with non-record
product types:
eg15 :: Labels Triple
eg15 = mempty & position @1 .~ Const "hello"
& position @2 .~ Const "world"
Again, this is a Lens
, so we can just as easily set values:
eg16 :: Partial User
eg16 = eg12 & position @2 .~ pure 26
Similarly, the internals here come to us courtesy of generic-lens
, so the
type errors are a delight:
eg17 :: Identity ()
eg17 = deconstruct @Identity triple ^. position @4
-- error:
-- • The type Triple does not contain a field at position 4
Labels
One neat trick we can do - thanks to the generic representation - is get the
names of the fields into the functor we're using. The label
value gives us
this interface:
eg18 :: Labels User
eg18 = label
By combining this with some of the
Barbies interface (the entirety
of which is available to any HKD
-wrapped type) such as bprod
and bmap
, we
can implement functions such as labelsWhere
, which returns the names of all
fields whose values satisfy some predicate:
eg19 :: [String]
eg19 = labelsWhere (isNothing . getLast) eg12
Documentation
All the docs in this library are tested on cabal new-test
. Furthermore, this
README is tested by markdown-unlit
.