Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/pkamenarsky/knit

Ties the knot on data structures that reference each other by unique keys
https://github.com/pkamenarsky/knit

data-structures haskell tie-knot

Last synced: 2 months ago
JSON representation

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

Awesome Lists containing this project

README

        

# knit

[![CircleCI](https://circleci.com/gh/pkamenarsky/knit.svg?style=svg)](https://circleci.com/gh/pkamenarsky/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

```haskell
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:

```haskell
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`:

```haskell
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:

```haskell
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:

```haskell
data Lazy a = { get :: a }
```

And here it is, a nicely knit model:

```haskell
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:

```haskell
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.