@hackage knit0.3.0.0

Ties the knot on data structures that reference each other by unique keys.

knit

CircleCI

knit ties the knot on data structures that reference each other by unique keys. Above all it aims to be easy to use - boilerplate is kept to a minimum and its API is as simple as it gets.

Example

data Person model m = Person
  { name        :: Id model m String
  , loves       :: ForeignId model m "persons" "name" --
  , isPresident :: Bool                               --
  } deriving (Generic, KnitRecord Model)              --
                                                      -- 
data Model m = Model                                  --
  { persons :: Table Model m Person -- <----------------
  } deriving (Generic, KnitTables)

Let's break that down: when defining a domain type, like Person, we'll need two additional type parameters that will determine the final shape of that type: the model Person belongs to (it may belong to multiple models), and its "mode" (m) - whether it's resolved or unresolved. Additionally, we need to derive KnitRecord for every domain type, supplying it with a concrete model type.

Id model m t will define a key this type is referenced by (multiple keys are possible).

ForeignId is where the magic happens - in addition to the two generic parameters from above it takes a "table" name (which is just a field in the model) and a field name in the referenced domain type; the final type of the ForeignId field (both resolved and unresolved) can then be inferred from this information alone!

To define a model, wrap each domain type with a Table and autoderive the KnitTables typeclass.

Let's take a look:

alice :: Person Model 'Unresolved
alice = Person
  { name        = Id "Alice"
  , loves       = ForeignId "Bob"  -- this must be a String, since Model.persons.name is a String!
  , isPresident = False
  }

bob :: Person Model 'Unresolved
bob = Person
  { name        = Id "Bob"
  , loves       = ForeignId "Alice"
  , isPresident = False
  }

model :: Model 'Unresolved
model = Model
  { persons = [alice, bob]  -- `Table` is just a regular list
  }

So far so good. Resolving an unresolved model is just a matter of calling knit:

knitModel :: Model Resolved
knitModel = case knit model of
  Right resolved -> resolved
  Left e -> error (show e)

(knit may fail due to invalid or duplicate keys). If all goes well, we'll get the following resolved model, if we were to do it by hand:

manualAlice :: Person Model 'Resolved
manualAlice = Person
  { name        = "Alice"
  , loves       = Lazy manualBob
  , isPresident = False
  }

manualBob :: Person Model 'Resolved
manualBob = Person
  { name        = "Bob"
  , loves       = Lazy manualAlice
  , isPresident = False
  }

manualModel :: Model 'Resolved
manualModel = Model
  { persons = [manualAlice, manualBob]
  }

Lazy is just a simple wrapper with a get field:

data Lazy a = { get :: a }

And here it is, a nicely knit model:

name $ get $ loves (persons knitModel !! 0) -- "Bob"

The test directory contains more examples, with multiple domain types.

Cascading deletes

By supplying a Remove key instead the regular Id a record is marked for deletion:

alice :: Person Model 'Unresolved
alice = Person
  { name        = Remove "Alice"  -- mark the record for deletion
  , loves       = ForeignId "Bob"
  , isPresident = False
  }

This will remove the record from the resolved result, as well as all other records that depend transitively on it. Invalid keys (i.e. ForeignIds that reference non-existent Ids) will still throw an error when knit-ting a model.