knit
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. Simplified it's just:
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. ForeignId
s that reference non-existent Id
s) will still throw an error when knit
-ting a model.