{"id":17335510,"url":"https://github.com/pkamenarsky/knit","last_synced_at":"2025-08-31T17:46:35.607Z","repository":{"id":56845153,"uuid":"262343426","full_name":"pkamenarsky/knit","owner":"pkamenarsky","description":"Ties the knot on data structures that reference each other by unique keys","archived":false,"fork":false,"pushed_at":"2024-08-02T14:53:42.000Z","size":63,"stargazers_count":47,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-07-30T18:39:58.070Z","etag":null,"topics":["data-structures","haskell","tie-knot"],"latest_commit_sha":null,"homepage":null,"language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pkamenarsky.png","metadata":{"files":{"readme":"README.md","changelog":"ChangeLog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-05-08T14:22:02.000Z","updated_at":"2024-08-02T14:53:47.000Z","dependencies_parsed_at":"2025-04-14T17:21:14.664Z","dependency_job_id":"70c7b283-e96a-4474-bce0-34e7fd8655ba","html_url":"https://github.com/pkamenarsky/knit","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/pkamenarsky/knit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pkamenarsky%2Fknit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pkamenarsky%2Fknit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pkamenarsky%2Fknit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pkamenarsky%2Fknit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pkamenarsky","download_url":"https://codeload.github.com/pkamenarsky/knit/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pkamenarsky%2Fknit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272988782,"owners_count":25026959,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-31T02:00:09.071Z","response_time":79,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-structures","haskell","tie-knot"],"created_at":"2024-10-15T15:10:16.814Z","updated_at":"2025-08-31T17:46:35.552Z","avatar_url":"https://github.com/pkamenarsky.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# knit\n\n[![CircleCI](https://circleci.com/gh/pkamenarsky/knit.svg?style=svg)](https://circleci.com/gh/pkamenarsky/knit)\n\n`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.\n\n## Example\n\n```haskell\ndata Person model m = Person\n  { name        :: Id model m String\n  , loves       :: ForeignId model m \"persons\" \"name\" --\n  , isPresident :: Bool                               --\n  } deriving (Generic, KnitRecord Model)              --\n                                                      -- \ndata Model m = Model                                  --\n  { persons :: Table Model m Person -- \u003c----------------\n  } deriving (Generic, KnitTables)\n```\n\nLet'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.\n\n`Id model m t` will define a key this type is referenced by (multiple keys are possible).\n\n`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!\n\nTo define a `model`, wrap each domain type with a `Table` and autoderive the `KnitTables` typeclass.\n\nLet's take a look:\n\n```haskell\nalice :: Person Model 'Unresolved\nalice = Person\n  { name        = Id \"Alice\"\n  , loves       = ForeignId \"Bob\"  -- this must be a String, since Model.persons.name is a String!\n  , isPresident = False\n  }\n\nbob :: Person Model 'Unresolved\nbob = Person\n  { name        = Id \"Bob\"\n  , loves       = ForeignId \"Alice\"\n  , isPresident = False\n  }\n\nmodel :: Model 'Unresolved\nmodel = Model\n  { persons = [alice, bob]  -- `Table` is just a regular list\n  }\n```\n\nSo far so good. Resolving an unresolved model is just a matter of calling `knit`:\n\n```haskell\nknitModel :: Model Resolved\nknitModel = case knit model of\n  Right resolved -\u003e resolved\n  Left e -\u003e error (show e)\n```\n\n(`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:\n\n```haskell\nmanualAlice :: Person Model 'Resolved\nmanualAlice = Person\n  { name        = \"Alice\"\n  , loves       = Lazy manualBob\n  , isPresident = False\n  }\n\nmanualBob :: Person Model 'Resolved\nmanualBob = Person\n  { name        = \"Bob\"\n  , loves       = Lazy manualAlice\n  , isPresident = False\n  }\n\nmanualModel :: Model 'Resolved\nmanualModel = Model\n  { persons = [manualAlice, manualBob]\n  }\n```\n\n`Lazy` is just a simple wrapper with a `get` field:\n\n```haskell\ndata Lazy a = { get :: a }\n```\n\nAnd here it is, a nicely knit model:\n\n```haskell\nname $ get $ loves (persons knitModel !! 0) -- \"Bob\"\n```\n\nThe `test` directory contains more examples, with multiple domain types.\n\n## Cascading deletes\n\nBy supplying a `Remove` key instead the regular `Id` a record is marked for deletion:\n\n```haskell\nalice :: Person Model 'Unresolved\nalice = Person\n  { name        = Remove \"Alice\"  -- mark the record for deletion\n  , loves       = ForeignId \"Bob\"\n  , isPresident = False\n  }\n```\n\nThis 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.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpkamenarsky%2Fknit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpkamenarsky%2Fknit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpkamenarsky%2Fknit/lists"}