• Stars
    star
    170
  • Rank 223,357 (Top 5 %)
  • Language
    Haskell
  • License
    MIT License
  • Created over 5 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Higher-kinded data via generics

Higgledy πŸ“š

GitHub CI

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:

{-# LANGUAGE DataKinds        #-}
{-# LANGUAGE DeriveGeneric    #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TypeOperators    #-}
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 ((:!), (!))

-- An example of a record (with named fields):
data User
  = User
      { name      :: String
      , age       :: Int
      , likesDogs :: Bool
      }
  deriving (Generic, Show)

user :: User
user = User "Tom" 26 True

-- An example of a product (without named fields):
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          -- Fields may be missing.
type Bare    a = HKD a  Identity      -- All must be present.
type Labels  a = HKD a (Const String) -- Every field holds a 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
-- User
--   { name      = Last {getLast = Nothing}
--   , age       = Last {getLast = Nothing}
--   , likesDogs = Last {getLast = Nothing}
--   }

Other 'Alternative'-style functors lead to very different results:

eg1 :: Labels Triple
eg1 = mempty
-- Triple
--   Const ""
--   Const ""
--   Const ""

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
-- User
--   { name      = Identity "Tom"
--   , age       = Identity 26
--   , likesDogs = Identity True
--   }

This approach works with any applicative we like, so we can recover the other behaviours:

eg4 :: Partial Triple
eg4 = deconstruct @Last triple
-- Triple
--   Last {getLast = Just 123}
--   Last {getLast = Just ()}
--   Last {getLast = Just "ABC"}

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
-- Last {getLast = Just (Triple 123 () "ABC")}

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"]
-- Triple [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
-- Triple [] [] []

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"
-- Last {getLast = Nothing}

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
-- User
--   { name      = Last {getLast = Just "Evil Tom"}
--   , age       = Last {getLast = Nothing}
--   , likesDogs = Last {getLast = Just 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
-- False

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"
-- Triple
--   Const "hello"
--   Const "world"
--   Const ""

Again, this is a Lens, so we can just as easily set values:

eg16 :: Partial User
eg16 = eg12 & position @2 .~ pure 26
-- User
--   { name      = Last {getLast = Just "Evil Tom"}
--   , age       = Last {getLast = Just 26}
--   , likesDogs = Last {getLast = Just False}
--   }

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
-- User
--   { name = Const "name"
--   , age = Const "age"
--   , likesDogs = Const "likesDogs"
--   }

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
-- ["age"]

Documentation

All the docs in this library are tested on cabal new-test. Furthermore, this README is tested by markdown-unlit.

More Repositories

1

haskell-exercises

A little course to learn about some of the more obscure GHC extensions.
Haskell
636
star
2

holmes

A reference library for constraint-solving with propagators and CDCL.
Haskell
298
star
3

fantas-eel-and-specification

Examples and exercises from the blog series
JavaScript
85
star
4

learn-me-a-haskell

Trying to get back all the stuff I had in JavaScript.
Haskell
70
star
5

purescript-panda

What would TEA look like if we had no VDOM?
PureScript
68
star
6

purescript-prelewd

An introduction to common PureScript operators through the only truly universal language.
PureScript
49
star
7

oops

Classy error-handling (and dispatching!) in Haskell.
Haskell
48
star
8

LICK

Idris-written, correct-by-construction, simply-typed lambda calculus.
Idris
39
star
9

world-building-in-haskell

Code written for the Berlin FP meetup.
Haskell
25
star
10

dagless

A monadic interface for DAG construction.
Haskell
24
star
11

schemer

A Joi-inspired interface for formatting and validating data structures in PHP.
PHP
18
star
12

wi-jit

A very minimal set of functional utilities. Just enough to get you going.
JavaScript
14
star
13

dagmore

Less Type, more Typeable.
Haskell
14
star
14

purescript-data-algebrae

Reified operations for several common data structures.
PureScript
13
star
15

purescript-propagators

Bidirectional computations as networks of relationships.
PureScript
10
star
16

purescript-super-circles

A simplified Super Hexagon clone written in PureScript.
PureScript
9
star
17

papers

I'm trying to learn things in my time away.
Haskell
8
star
18

purescript-spirographs

CodeMesh 2018 - An introduction to PureScript canvas rendering and the Behaviors library.
PureScript
7
star
19

i-am-tom.github.io

My personal website for blogging my coding exploits.
JavaScript
6
star
20

php-folding-talk

Code for the lightning talk on folds at PHPSW.
PHP
6
star
21

haskell

A "monorepo" of "packages" that I accidentally ended up making while trying to do something else.
Haskell
5
star
22

php-free-talk

Supporting material for my PHP Free monad talk!
PHP
4
star
23

opengl-playground

Dumping ground for OpenGL experiments.
Haskell
3
star
24

herald

Experiments in re-interpreting applicative programs
Haskell
3
star
25

purescript-amplitude

Amplitude wrappers for PureScript
PureScript
3
star
26

neopreen

A formatting library to complement the neo4j-driver package.
JavaScript
3
star
27

learn-me-a-rust

Trying to get back all the stuff I had in Haskell.
Rust
3
star
28

minim

My 2-day Elm hackathon in quarantine
Elm
3
star
29

purescript-easings

The standard set of easing functions. Implemented in PureScript.
PureScript
3
star
30

hoot

Contentful + Mustache = CMS
Haskell
2
star
31

puzzled

Arved and Tom attempt to write a solver.
Haskell
1
star
32

purescript-money

Really made a meal of this one, didn't I?
PureScript
1
star
33

h-and-h

PureScript
1
star