{"id":16662456,"url":"https://github.com/nicolast/lawful-classes","last_synced_at":"2025-06-20T06:06:20.945Z","repository":{"id":65626821,"uuid":"595824335","full_name":"NicolasT/lawful-classes","owner":"NicolasT","description":"Classes with laws lead to more lawful instances","archived":false,"fork":false,"pushed_at":"2023-02-02T21:28:06.000Z","size":38,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-20T06:05:36.226Z","etag":null,"topics":["haskell","laws","property-based-testing","quickcheck","testing"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/NicolasT.png","metadata":{"files":{"readme":"README.lhs","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}},"created_at":"2023-01-31T22:06:18.000Z","updated_at":"2023-02-01T12:07:07.000Z","dependencies_parsed_at":"2023-02-18T00:46:04.736Z","dependency_job_id":null,"html_url":"https://github.com/NicolasT/lawful-classes","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/NicolasT/lawful-classes","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NicolasT%2Flawful-classes","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NicolasT%2Flawful-classes/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NicolasT%2Flawful-classes/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NicolasT%2Flawful-classes/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/NicolasT","download_url":"https://codeload.github.com/NicolasT/lawful-classes/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/NicolasT%2Flawful-classes/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260891142,"owners_count":23077910,"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":["haskell","laws","property-based-testing","quickcheck","testing"],"created_at":"2024-10-12T10:37:58.021Z","updated_at":"2025-06-20T06:06:15.930Z","avatar_url":"https://github.com/NicolasT.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# lawful-classes\n\nThe `lawful-classes` packages provide a simple framework to define laws for\nclasses (e.g., some `MonadState` class and laws related to the behaviour of\nactions defined in `MonadState`) without incurring a dependency on libraries\nlike [Tasty][tasty] or [Hspec][hspec], or [QuickCheck][quickcheck] or\n[Hedgehog][hedgehog], within the library where said class and its related\nlaws are defined.\n\nLaw definitions can make use of generators of values, as provided by\nQuickCheck or Hedgehog, but can request a test iteration to be discarded if\nsome generated value doesn't meet some criteria (alike QuickCheck's `==\u003e`\nimplication arrow), without a dependency on QuickCheck or other libraries:\nin essence, every test-case returns a value of type `Maybe Bool` (in some\nmonadic context), where `Nothing` implies a test-case should be discarded,\nand `Just b` represents an (un)successful test-case/assertion.\n\nHence, `Law`s are computations of type `m (Maybe Bool)`, and are grouped in\n`Laws`, a list of `(String, Law)` pairs where the first member provides some\nhuman-readable description of the law being checked.\n\nThese type aliases, and some trivial helper functions, are provided in the\n`lawful-classes-types` package. This package only depends on `base`, but it's\nnot strictly required as a dependency to define laws: everything can be\nachieved by only using `base`.\n\n[tasty]: https://hackage.haskell.org/package/tasty\n[hspec]: https://hackage.haskell.org/package/hspec\n[quickcheck]: https://hackage.haskell.org/package/QuickCheck\n[hedgehog]: https://hackage.haskell.org/package/hedgehog\n\n## Packages\n[lawful-classes-types][lawful-classes-types] provides common type aliases and\nsome trivial helper functions. A dependency on this library is not required\nto define laws.\n\n[lawful-classes-hedgehog][lawful-classes-hedgehog] provides code to integrate\nlaw-checking in a Tasty environment, when using Hedgehog to generate exemplars.\nIt also provides some plumbing functions for integration in frameworks other\nthan Tasty.\n\n[lawful-classes-quickcheck][lawful-classes-quickcheck] provides code to\nintegrate law-checking in a Tasty environment, when using QuickCheck to\ngenerate exemplars. It also provides some plumbing functions for integration \nin frameworks other than Tasty.\n\n[lawful-classes-types]: https://hackage.haskell.org/package/lawful-classes-types\n[lawful-classes-hedgehog]: https://hackage.haskell.org/package/lawful-classes-hedgehog\n[lawful-classes-quickcheck]: https://hackage.haskell.org/package/lawful-classes-quickcheck\n\n## Example\n\nHere's an example, using Literate Haskell.\n\n### Boilerplate\nFirst, some boilerplate, only required for the code below but in no way\nrequired to use the framework:\n\n```haskell\n{-# LANGUAGE TypeApplications #-}\n{-# LANGUAGE FlexibleInstances #-}\n{-# LANGUAGE MultiParamTypeClasses #-}\n{-# LANGUAGE FunctionalDependencies #-}\n{-# LANGUAGE GeneralizedNewtypeDeriving #-}\n```\n\nIn this example, we'll define some implementations of a class, using `State`\nand `StateT` under the hood:\n\n```haskell\nimport Control.Monad.State (State, evalState)\nimport Control.Monad.State.Class (get, put)\nimport Control.Monad.Trans.Class (MonadTrans)\nimport Control.Monad.Trans.State (StateT, evalStateT)\n```\n\nChecking the laws can be done using any testing framework. Using\n`lawful-classes-hedgehog` or `lawful-classes-quickcheck`, integration with\neither Hedgehog or QuickCheck becomes easier, especially when using Tasty as\na test runner. Generally, you'll use either Hedgehog or QuickCheck. In this\nexample, we'll showcase both:\n\n```haskell\nimport Hedgehog (withTests)\nimport qualified Hedgehog.Gen as Gen\nimport qualified Hedgehog.Range as Range\nimport Test.QuickCheck (arbitrary, once)\nimport Test.Tasty (TestTree, defaultMain, testGroup)\nimport Test.Tasty.ExpectedFailure (expectFail)\n```\n\nFinally, import the code from `lawful-classes-types`, `lawful-classes-hedgehog`\nand `lawful-classes-quickcheck`:\n\n```haskell\nimport Test.Lawful.Types (Laws, assert, discard)\nimport qualified Test.Lawful.Hedgehog as LH\nimport qualified Test.Lawful.QuickCheck as LQ\n```\n\n### A class and its laws\nIn our library, we define a class, `MonadStore`, which exposes two actions,\n`store` and `retrieve`:\n\n```haskell\n-- | An environment which allows to store some value, and retrieve it later.\nclass Monad m =\u003e MonadStore a m | m -\u003e a where\n  -- | Store the given value.\n  store :: a -\u003e m ()\n  -- | Retrieve a previously stored value, if any.\n  retrieve :: m (Maybe a)\n```\n\nGiven this, we can define some `Laws` to which any valid instance of\n`MonadStore` should obey:\n\n```haskell\n-- Remember, `Laws m` is just `[(String, m (Maybe Bool)]`\nmonadStoreLaws :: (MonadStore a m, Eq a) =\u003e m a -\u003e Laws m\nmonadStoreLaws gen = [\n  (\"When nothing is stored, nothing can be retrieved\", do\n      a0 \u003c- retrieve\n      assert $ a0 == Nothing  -- assert = pure . Just\n  ),\n\n  (\"Retrieve yields what was stored\", do\n      -- Retrieve the currently stored value\n      a0 \u003c- retrieve\n      -- Generate some arbitrary value\n      a \u003c- gen\n\n      if Just a == a0\n        -- Debatable: if the generated value equals the currently stored one, this test is void\n        then discard  -- discard = pure Nothing\n        else do\n          store a\n          a' \u003c- retrieve\n          assert $ a' == Just a\n  )\n  ]\n```\n\n### Instances\nWe define two instances of `MonadStore`, one which is correct and obeys the\nlaws, one which (intentionally) doesn't. First, `LockerT`:\n\n```haskell\nnewtype LockerT a m b = LockerT (StateT (Maybe a) m b)\n  deriving (Functor, Applicative, Monad, MonadTrans)\n\ninstance Monad m =\u003e MonadStore a (LockerT a m) where\n  store = LockerT . put . Just\n  retrieve = LockerT get\n\nevalLockerT :: Monad m =\u003e LockerT a m b -\u003e m b\nevalLockerT (LockerT act) = evalStateT act Nothing\n```\n\nGiven the above, we can check whether the `monadStoreLaws` hold for this\nimplementation as follows, first using Hedgehog:\n\n```haskell\nlockerTTestsHedgehog :: TestTree\nlockerTTestsHedgehog = \n  LH.testLaws \"monadStoreLaws (Hedgehog)\" evalLockerT $\n    monadStoreLaws (LH.forAll (Gen.int Range.linearBounded))\n```\n\nThen, using QuickCheck:\n\n```haskell\nlockerTTestsQuickCheck :: TestTree\nlockerTTestsQuickCheck =\n  LQ.testLaws \"monadStoreLaws (QuickCheck)\" evalLockerT $\n    monadStoreLaws (LQ.forAll (arbitrary @Int))\n```\n\nA second instance of `MonadStore` doesn't play by the rules:\n\n```haskell\nnewtype UnlawfulLockerT a m b = UnlawfulLockerT (StateT (Maybe a) m b)\n  deriving (Functor, Applicative, Monad, MonadTrans)\n\ninstance (Monad m, Eq a, Num a) =\u003e MonadStore a (UnlawfulLockerT a m) where\n  store v = do\n    let v' = if v == 0 then v else v + 1\n    UnlawfulLockerT (put $ Just v')\n  retrieve = UnlawfulLockerT get\n\nevalUnlawfulLockerT :: (Monad m, Num a) =\u003e UnlawfulLockerT a m b -\u003e m b\nevalUnlawfulLockerT (UnlawfulLockerT act) = evalStateT act (Just 1)\n```\n\nSimilarly, test its law-(non-)abiding using Hedgehog and QuickCheck as exemplar\ngenerators (note, in this case we wrap them with `expectFail`, since this\ndocument is tested in CI, and we actually want the following tests to fail, but\nnot CI to fail):\n\n```haskell\nunlawfulLockerTTests :: TestTree\nunlawfulLockerTTests = expectFail $ testGroup \"UnlawfulLockerT\" [\n  LH.testLaws \"monadStoreLaws (Hedgehog)\" evalUnlawfulLockerT $\n    monadStoreLaws (LH.forAll (Gen.int Range.linearBounded)),\n  LQ.testLaws \"monadStoreLaws (QuickCheck)\" evalUnlawfulLockerT $\n    monadStoreLaws (LQ.forAll (arbitrary @Int))\n  ]\n```\n\n#### Plain monads\nIn the above, we use a monad transformer, since the laws are defined such that\na generator of values can be used. This generator is, then, lifted into a\nmonad lower in the stack, e.g., `PropertyT` when using Hedgehog or `PropertyM`\nwhen using QuickCheck.\n\nWe can, of course, check the laws against a plain monad as well, but in this\ncase no values can be generated: there's no lower monad in the stack. We can,\nhowever, provide a generator which yields exactly one (constant) value. In this\ncase, it doesn't make sense to run the test, say, 100 times. Hence, the\n`testLaws` functions have a counterpart which allow to modify the tested\nproperties: `testLawsWith`.\n\nLet's first define `Locker`, which wraps `State` (it could, of course, reuse\nthe definition of `LockerT` and use `Identity` as the base monad):\n\n```haskell\nnewtype Locker a b = Locker (State (Maybe a) b)\n  deriving (Functor, Applicative, Monad)\n\ninstance MonadStore a (Locker a) where\n  store = Locker . put . Just\n  retrieve = Locker get\n\nevalLocker :: Locker a b -\u003e b\nevalLocker (Locker act) = evalState act Nothing\n```\n\nNow, define the tests, and ensure they run only once:\n\n```haskell\nlockerTests :: TestTree\nlockerTests = testGroup \"Locker\" [\n  LH.testLawsWith (withTests 1) \"monadStoreLaws (Hedgehog)\"\n    (pure . evalLocker)\n    (monadStoreLaws $ pure (1 :: Int)),\n  LQ.testLawsWith once \"monadStoreLaws (Hedgehog)\"\n    (pure . evalLocker)\n    (monadStoreLaws $ pure (1 :: Int))\n  ]\n```\n\nHence, we'll run the tests with only a single generated value, being `1`.\n\n### Running tests\nFinally, we can hook everything together and run the tests, using Tasty:\n\n```haskell\nmain :: IO ()\nmain = defaultMain $ testGroup \"lawful-classes-readme\" [\n    testGroup \"LockerT\" [\n      lockerTTestsHedgehog,\n      lockerTTestsQuickCheck\n      ],\n    unlawfulLockerTTests,\n    lockerTests\n  ]\n```\n\nAnd indeed:\n\n```\nlawful-classes-readme\n  LockerT\n    monadStoreLaws (Hedgehog)\n      When nothing is stored, nothing can be retrieved: OK\n          ✓ \u003cinteractive\u003e passed 100 tests.\n      Retrieve yields what was stored:                  OK\n          ✓ \u003cinteractive\u003e passed 100 tests.\n    monadStoreLaws (QuickCheck)\n      When nothing is stored, nothing can be retrieved: OK\n        +++ OK, passed 100 tests.\n      Retrieve yields what was stored:                  OK\n        +++ OK, passed 100 tests.\n  UnlawfulLockerT\n    monadStoreLaws (Hedgehog)\n      When nothing is stored, nothing can be retrieved: FAIL (expected)\n          ✗ \u003cinteractive\u003e failed at src/Test/Lawful/Hedgehog.hs:37:47\n            after 1 test.\n            shrink path: 1:\n\n            This failure can be reproduced by running:\n            \u003e recheckAt (Seed 15866264305184189776 9458969182257889837) \"1:\" \u003cproperty\u003e\n\n        Use ...\n         (expected failure)\n      Retrieve yields what was stored:                  FAIL (expected)\n          ✗ \u003cinteractive\u003e failed at src/Test/Lawful/Hedgehog.hs:37:47\n            after 2 tests and 56 shrinks.\n            shrink path: 2:bA55\n\n                ┏━━ README.lhs ━━━\n            172 ┃ unlawfulLockerTTests :: TestTree\n            173 ┃ unlawfulLockerTTests = expectFail $ testGroup \"UnlawfulLockerT\" [\n            174 ┃   LH.testLaws \"monadStoreLaws (Hedgehog)\" evalUnlawfulLockerT $\n            175 ┃     monadStoreLaws (LH.forAll (Gen.int Range.linearBounded)),\n                ┃     │ 2\n            176 ┃   LQ.testLaws \"monadStoreLaws (QuickCheck)\" evalUnlawfulLockerT $\n            177 ┃     monadStoreLaws (LQ.forAll (arbitrary @Int))\n            178 ┃   ]\n            179 ┃ ```\n            180 ┃\n            181 ┃ ### Running tests\n            182 ┃ Finally, we can hook everything together and run the tests, using Tasty:\n            183 ┃\n            184 ┃ ```haskell\n\n            This failure can be reproduced by running:\n            \u003e recheckAt (Seed 9014603521135969515 646347982343338801) \"2:bA55\" \u003cproperty\u003e\n\n        Use ...\n         (expected failure)\n    monadStoreLaws (QuickCheck)\n      When nothing is stored, nothing can be retrieved: FAIL (expected)\n        *** Failed! Assertion failed (after 1 test):\n        Use --quickcheck-replay=181324 to reproduce. (expected failure)\n      Retrieve yields what was stored:                  FAIL (expected)\n        *** Failed! Assertion failed (after 4 tests):\n        3\n        Use --quickcheck-replay=404272 to reproduce. (expected failure)\n  Locker\n    monadStoreLaws (Hedgehog)\n      When nothing is stored, nothing can be retrieved: OK\n          ✓ \u003cinteractive\u003e passed 1 test.\n      Retrieve yields what was stored:                  OK\n          ✓ \u003cinteractive\u003e passed 1 test.\n    monadStoreLaws (Hedgehog)\n      When nothing is stored, nothing can be retrieved: OK\n        +++ OK, passed 1 test.\n      Retrieve yields what was stored:                  OK\n        +++ OK, passed 1 test.\n\nAll 12 tests passed (0.01s)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicolast%2Flawful-classes","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnicolast%2Flawful-classes","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicolast%2Flawful-classes/lists"}