{"id":16340399,"url":"https://github.com/i-am-tom/learn-me-a-haskell","last_synced_at":"2025-03-20T23:31:20.310Z","repository":{"id":98194900,"uuid":"142750266","full_name":"i-am-tom/learn-me-a-haskell","owner":"i-am-tom","description":"Trying to get back all the stuff I had in JavaScript.","archived":false,"fork":false,"pushed_at":"2018-10-07T13:02:52.000Z","size":141,"stargazers_count":69,"open_issues_count":1,"forks_count":1,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-17T18:49:05.844Z","etag":null,"topics":["dependent-types","ghc","haskell","type-level-programming"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/i-am-tom.png","metadata":{"files":{"readme":"README.md","changelog":null,"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}},"created_at":"2018-07-29T10:10:05.000Z","updated_at":"2025-01-29T02:07:57.000Z","dependencies_parsed_at":null,"dependency_job_id":"29c32c3b-e0d4-409f-bb7e-75037603d5b3","html_url":"https://github.com/i-am-tom/learn-me-a-haskell","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i-am-tom%2Flearn-me-a-haskell","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i-am-tom%2Flearn-me-a-haskell/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i-am-tom%2Flearn-me-a-haskell/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i-am-tom%2Flearn-me-a-haskell/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/i-am-tom","download_url":"https://codeload.github.com/i-am-tom/learn-me-a-haskell/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244710732,"owners_count":20497298,"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","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":["dependent-types","ghc","haskell","type-level-programming"],"created_at":"2024-10-10T23:56:41.686Z","updated_at":"2025-03-20T23:31:19.961Z","avatar_url":"https://github.com/i-am-tom.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Learning Me a Haskell 🎩✨\n\nIt's about time I figured out what all the fuss is about. I'll keep all my\nfindings in this repo.\n\nAlthough this repo mainly exists for my benefit, I've tried to document\n*everything* thoroughly, so I hope it's useful to others. If you have any\nquestions or suggestions on how to improve the docs, please [send me a\ntweet](https://www.twitter.com/am_i_tom) or a PR and we'll improve things :)\n\n## Setup\n\n```bash\n$ # Build the project with haddocks\n$ cabal new-build\n$ cabal new-haddock\n\n$ # Run the tests - they're in the docs!\n$ cabal new-run test\n```\n\n## Table of Contents\n\n- [OneOf](#oneof)\n- [HList](#hlist)\n- [HTree](#htree)\n- [Bag](#bag)\n\n## [OneOf](/src/OneOf/Types.hs#L38-L40)\n\n### Intro\n\n`OneOf` is a generalised version of `Either` (I think sometimes known as a\n`Variant`?). While `Either` is strictly for one of two possibilities, `OneOf`\ngeneralises this to any (non-zero) number of possibilities with a type-level\nlist. For example:\n\n```haskell\nf :: OneOf '[String]            -- `String`\ng :: OneOf '[String, Bool]      -- `Either String Bool`\nh :: OneOf '[String, Bool, Int] -- `Either String (Either Bool Int)`\n```\n\nRather than using `Left` and `Right`, we construct these values with some\nnumber of `There`s followed by a `Here`. For example:\n\n```haskell\nf :: OneOf '[String]\nf = Here \"Hello!\"\n\ng :: OneOf '[String, Bool]\ng = There (Here True)\n\nh :: OneOf '[String, Bool, Int]\nh = There (There (Here 3))\n```\n\n### Injection\n\nThe above is quite... ugly, though, right? What we'd really like is a neat way\nto \"lift\" a type into the `OneOf` without having to worry about the number of\n`There`s we need. Luckily, the `inject` function gives us just that:\n\n```haskell\nf :: OneOf '[String]\nf = inject \"Hello\"\n\ng :: OneOf '[String, Bool]\ng = inject True\n\nh :: OneOf '[String, Integer, Bool]\nh = inject 3\n```\n\n`inject` looks through the list for the first occurrence of our type, and\nproduces the constructors required to lift our value into the `OneOf`.\n\n### Projection\n\nCool, we have a type that could hold a value that is one of any number of\ntypes... how do we _use_ it? Well, we could pattern-match, but then we're back\nto worrying about everything `Here` and `There`. To help with this, the `fold`\nfunction will generate a Church-style fold for any `OneOf` value:\n\n```haskell\nf :: OneOf '[String]\n  -\u003e (String -\u003e result)\n  -\u003e result\nf = fold\n\ng :: OneOf '[String, Bool]\n  -\u003e (String -\u003e result)\n  -\u003e (Bool   -\u003e result)\n  -\u003e result\ng = fold\n\nh :: OneOf '[String, Bool, Int]\n  -\u003e (String -\u003e result)\n  -\u003e (Bool   -\u003e result)\n  -\u003e (Int    -\u003e result)\n  -\u003e result\nh = fold\n```\n\nThe type is calculated based on the type of the `OneOf` supplied as a first\nargument, and the pattern is always the same: _give me a function from each\ntype to some `r`, and I'll just call the appropriate one._\n\nAlternatively, we can fold a `OneOf` using a constraint, assuming that all\nvalues have an instance:\n\n```haskell\nh :: OneOf '[String, Bool, Int] -\u003e String\nh = interpret @Show show\n```\n\nHere, we use the fact that `String`, `Bool`, and `Int` all have `Show`\ninstances, and thus have an instance for the `show` function that returns a\n`String`. Now, we'll just get the `show` result for whatever is in our value.\n\n## [HList](/src/HList/Types.hs#L43-L45)\n\n### Intro\n\n`HList` is a generalised version of `Tuple` which allows you to carry lists of\nvalues with *possibly-different* types. It's notionally equivalent to tuples:\n\n| Tuple              | HList                 |\n| ------------------:|:--------------------- |\n| `()`               | `HList '[]`           |\n| `(a)`              | `HList '[a]`          |\n| `(a, b)`           | `HList '[a, b]`       |\n| `(a, (b, c))`      | `HList '[a, b, c]`    |\n| `(a, (b, (c, d)))` | `HList '[a, b, c, d]` |\n\n_Of course, `(a)` is just `a` - a one-element tuple _is_ its type._ The H in\n`HList` stands for *heterogenous*, as distinct from a _homogeneous_ list where\nall elements must be the same type. Similarly to a list, we construct them with\ncons and nil constructors, imaginatively named `HCons` and `HNil`:\n\n```haskell\nf :: HList '[]\nf = HNil\n\ng :: HList '[Bool]\ng = HCons True HNil\n\nh :: HList '[String, Bool]\nh = HCons \"hello\" (HCons True HNil)\n\n-- etc.\n```\n\n### Construction\n\nAs we noted with `OneOf`, this is still rather ugly, and we can use some\ntype-level trickery to tidy this up. `build` is a function that builds an HList\nby taking all its values as parameters in the order they appear (using a\nhealthy dose of typeclass magic).\n\n```haskell\nf :: HList '[]\nf = build\n\ng :: Int -\u003e HList '[Int]\ng = build\n\nh :: String -\u003e Int -\u003e HList '[String, Int]\nh = build\n```\n\n_This involves some fun tricks behind the scenes that I've documented in [the\n`HList.build` file](src/HList/Build.hs)._\n\n### Updating\n\nNow we have our glorious `HList`, can we change the values – or even _types_ –\nwithin it? You bet!\n\nThe `update` function takes a function and an `HList`, and applies that\nfunction to an index that we specify with a *type application*. For example:\n\n```haskell\nxs :: HList '[Integer, Double, Bool]\nxs = HCons 2 (HCons 2.0 (HCons True HNil))\n\n-- HCons 2 (HCons 2.0 (HCons False (HNil)))\nf = update @2 not xs\n\n-- HCons 2 (HCons \"hello\" (HCons True (HNil)))\ng = update @1 (\\_ -\u003e \"hello\") xs\n\n-- ... • 4 is out of bounds for '[Integer, Double, Bool]!\n-- ...   We'll need at least 0, and at most 2.\nh = update @4 show xs\n\n-- • You can't apply [Char] -\u003e [Char] to the Double at index #1.\n--   I'm sorry, captain; I just won't do it.\ni = update @1 (++ \"!\") xs -- Type mismatch!\n```\n\nWe can see here that our index is updated when the types align, and we\notherwise get some nice custom type errors! _I'd like to add more type errors\nhere, but I'm at the stage where I have to wrestle a bit with incoherence)_.\n\n### Projection\n\nWe have an `HList`, and we can pattern-match as we want, but... can we _fold_\nan `HList`? Well, similarly to `OneOf`, we can have a constrained fold:\n\n```haskell\nfold\n  :: Monoid monoid\n  =\u003e (forall element. constraint element =\u003e element -\u003e monoid)\n  -\u003e HList list\n  -\u003e monoid\n```\n\nIf all our `element`s satisfy some `constraint`, we can use this `fold` method\nto combine them all under some monoid. For example, `fold @Show show` will take\nthe string representation of every element of any `HList` and concat the\nresults together, as long as all the types have a `Show` instance.\n\nAnother fun consequence of this generalisation is that we can recover\nhomogeneous operations like `foldMap` by using a constraint like `((~)\nelement)` (every element of the list must be equal to some type `element`). In\nfact, that's exactly how `foldMap` is implemented within this library!\n\n## [HTree](/src/HTree/Types.hs#L66-L78)\n\n### Intro\n\n`HList` is great and all, but it's not the most efficient structure when our\nlist grows and access is random. GHC does no caching and **no optimisation**\nwith list lookups at the type-level, so linear access becomes _very_ expensive.\nIf this is our use case, we could consider a different structure, such as a\ntree!\n\nAn `HTree` is exactly this: a heterogeneous tree, as opposed to a list.\nSpecifically, it's a **binary** tree indexed by a **red-black** tree of types,\nwhich we use to keep the tree (roughly) balanced. A serious hat-tip is due to\n**Chris Okasaki** for his **Red-Black Trees in a Functional Setting** paper,\nwhich I used for the implementation of `insert` and `delete`.\n\n### Construction\n\nSo, this all sounds well and good, but how do we construct one? We need a way\nof ordering types, which we achieve through use of the `Generic` class – we\norder types by their names – and the `insert` function:\n\n```haskell\nnewtype Name\n  = Name String\n  deriving (Generic, Show)\n\nnewtype Age\n  = Age Int\n  deriving (Generic, Show)\n\nexample :: HTree ('Node 'Black ('Node 'Empty Age 'Empty) Name 'Empty)\nexample\n  = insert (Age 25)\n  . insert (Name \"Tom\")\n  $ empty\n```\n\n`insert` gives us a way to build a tree of types, providing that the types are\n`Generic`. However, the type of an `HTree` can quickly become ugly, so you\nmight want to stick to polymorphic approaches:\n\n```haskell\naddSomeTom :: (Insert Name i m, Insert Age m o) =\u003e HTree i -\u003e HTree o\naddSomeTom = insert (Age 25) . insert (Name \"Tom\")\n```\n\nRather than talking about what a tree _is_, we can use the `Insert` typeclass\nto talk about its state _before_ and _after_ inserting a variable, and let GHC\nworry about the full type.\n\n### Deletion\n\nDeletion is as you'd imagine: we use type application to specify the type we\nwant to delete, and everything else works as `insert` did:\n\n```haskell\n-- (.)\u003c-(Age 25)-\u003e(.)\ndemo :: HTree ('Node 'Black 'Empty Age 'Empty)\ndemo = delete @Name example\n```\n\n... and we can use the constraints to deal with a tree polymorphically:\n\n```haskell\nanonymise :: Delete Name input output =\u003e HTree input -\u003e HTree output\nanonymise = delete @Name\n```\n\nNote that deleting a type from a tree is a *no-op* if the tree doesn't contain\nthe type. It is *not* a type error.\n\n### Access\n\nOf course, the last interesting function on an `HTree` is this access we keep\ntalking about. This is provided by the `getType` function within the `HasType`\nclass:\n\n```haskell\ntest :: HasType Name input =\u003e HTree input -\u003e Name\ntest = getType\n\nname = test example -- Name \"Tom\"\n```\n\nAll polymorphic, all *beautiful*. Naturally, GHC will help you if you go\nlooking for a type that isn't in the tree:\n\n```haskell\n-- ... I couldn't find any Bool in this tree...\n-- ... If it helps, here's what I did find:\n-- ... - Age\n-- ... - Name\nd'oh = getType @Bool example\n\n-- ... You won't find any Bool here!\n-- ... Your tree is empty; there's nothing to access!\noops = getType @Bool empty\n```\n\n## [Bag](/src/Bag/Types.hs#L99-L100)\n\n_The idea for this came from a talk by Will Jones, our VP Engineering at\n[Habito](habito.com), on [Deriving\nStrategies](https://www.youtube.com/watch?v=U0j9iIKOj40). Always hiring, etc!_\n\n### Intro\n\nLet's imagine you're building a **web app** that involves a lot of forms:\n\n```haskell\ndata Account\n  = Account\n      { email    :: Email\n      , password :: Password\n      }\n\ndata Profile\n  = Profile\n      { name :: Name\n      , age  :: Age\n      }\n\n-- ...\n```\n\nThese forms may be completed in various orders, and to various degrees, which\nleaves us with a _lot_ of potentially-partial data. To accommodate this, we\nhave to make all these fields optional:\n\n```haskell\ndata PartialAccount\n  = Account\n      { email    :: Maybe Email\n      , password :: Maybe Password\n      }\n\n-- ...\n```\n\nWe no longer have a compile-time way of ensuring we have all the data in this\ntype, so we have to attempt to build the original types at run-time, `Maybe`\nsucceeding, maybe failing. As we add more forms to our app, so too do we add\nmore partial types.\n\nThe `Bag` type, however, generalises this notion: it `Maybe` contains **any**\ntype! Rather than specify near-verbatim copies of every type, we can now just\nuse `Bag` to collect all the data we need. For example:\n\n```haskell\nnewtype Name = Name String deriving Generic\nnewtype Age  = Age  Int    deriving Generic\n\nuserData :: Bag'\nuserData\n  = insert (Name \"Tom\")\n  $ insert (Age  25   )\n\n  -- Under the hood, a `Bag` type is really just a newtype around `HashMap`,\n  -- which means it's automagically a `Semigroup` and a `Monoid`! As a result,\n  -- `mempty` here means \"an empty bag\".\n  $ mempty\n\n-- ... Another place at another time ...\n\nuserName :: Name\nuserName = lookup userData\n```\n\nAs type inference relies on the **return type** of the `lookup` function, it's\ncertainly encouraged to use the `TypeApplications` feature to improve\nreadability (and inference, in more ambiguous cases):\n\n```haskell\nf = do\n  name \u003c- lookup @Name userData\n  -- etc...\n```\n\n## Constrained bags\n\nWe've so far only looked at `Bag'`, which is a specialisation of the `Bag` type\nthat enforces no constraints on the contents. The `Bag` type allows us to\nspecify constraint constructors that hold for any member of the bag. For\nexample:\n\n```haskell\n-- Doesn't have a `Show` instance!\nnewtype Name = Name String\n\n-- ... No instance for (Show Name) arising from a use of ‘insert’\ntest :: Bag '[Show]\ntest = insert (Name \"Tom\") mempty\n\n-- Does have a show instance!\nnewtype Age = Age 25 deriving Show\n\nsuccess :: Bag '[Show]\nsuccess = insert (Age 25) mempty\n```\n\nYou can also use these constructors to write instances for the entire bag\n(using some [QuantifiedConstraints](/src/Bag/QuantifiedInstances.hs#L92)\nmagic) - this library provides instances for `Eq` and `Show`. In essence, if\nthe bag's constraints are enough to imply a `Show` instance, we can write a\n`Show` instance for the bag (similarly for `Eq`):\n\n```haskell\nnewtype Name = Name String deriving (Show, Eq)\nnewtype Age  = Age  Int    deriving (Show, Eq)\n\ndemo :: Bag '[Show, Eq]\ndemo = insert (Name \"Tom\") $ insert (Age 25) $ mempty\n\n-- \"Bag (fromList [(Age,Age 25),(Name,Name \\\"Tom\\\")])\"\nshowable :: String\nshowable = show demo\n\n-- False\neqable :: Bool\neqable = demo == mempty\n```\n\n## Type hydration\n\nIf you have a type whose fields are all potentially in a `Bag`, you can use the\n`populate` function to attempt to construct the type:\n\n```haskell\ndata Person\n  = Person\n      { name :: Name\n      , age  :: Age\n      }\n  deriving (Show, Generic)\n\n-- Sucess (Person {name = Name \"Tom\", age = Age 25})\nyay = populate @Person demo\n\n-- Failure [\"Name\",\"Age\"]\nboo = populate @Person (mempty :: Bag')\n```\n\nIf every field in the type's record or product can be found in the `Bag`, the\nconstructed value will be returned. Otherwise, we get a list of the names of\nthe types that were missing. There's unfortunately not much more you can do at\nthis stage, but it may be useful information for logging or debugging!\n\n## Bag hydration\n\nThe opposite control is to take a record/product type and use it to populate a\n`Bag`. For this, we have the `include` function:\n\n```haskell\nmyBag :: Bag'\nmyBag = mempty\n\ntest :: Bag '[Show]\ntest = include (Person (Name \"Tom\") (Age 25)) mempty\n\nx = show test\n-- Bag (fromList [(Name,Name \"Tom\"),(Age,Age 25)])\n```\n\nEvery time an inner type is encountered, it will attempt to insert this type\ninto the `Bag`. Note that this means **all types** inside your product must\nimplement **all constraints** on the `Bag`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fi-am-tom%2Flearn-me-a-haskell","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fi-am-tom%2Flearn-me-a-haskell","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fi-am-tom%2Flearn-me-a-haskell/lists"}