{"id":13687139,"url":"https://github.com/mlabs-haskell/apropos","last_synced_at":"2025-04-15T22:20:10.096Z","repository":{"id":37012043,"uuid":"465732305","full_name":"mlabs-haskell/apropos","owner":"mlabs-haskell","description":"Propositional Logic Apropos Types","archived":false,"fork":false,"pushed_at":"2023-02-21T15:21:39.000Z","size":810,"stargazers_count":14,"open_issues_count":9,"forks_count":5,"subscribers_count":12,"default_branch":"apropos-lite","last_synced_at":"2025-03-29T02:04:18.212Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/mlabs-haskell.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":"ROADMAP.md","authors":null}},"created_at":"2022-03-03T13:34:13.000Z","updated_at":"2024-03-13T15:00:46.000Z","dependencies_parsed_at":"2024-01-14T16:09:48.392Z","dependency_job_id":"8e81f72d-64b4-43ac-ba0c-cba20f08eaf1","html_url":"https://github.com/mlabs-haskell/apropos","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/mlabs-haskell%2Fapropos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlabs-haskell%2Fapropos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlabs-haskell%2Fapropos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlabs-haskell%2Fapropos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mlabs-haskell","download_url":"https://codeload.github.com/mlabs-haskell/apropos/tar.gz/refs/heads/apropos-lite","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249161806,"owners_count":21222556,"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":[],"created_at":"2024-08-02T15:00:49.445Z","updated_at":"2025-04-15T22:20:10.079Z","avatar_url":"https://github.com/mlabs-haskell.png","language":"Haskell","funding_links":[],"categories":["Haskell"],"sub_categories":[],"readme":"# `apropos`\n\nHedgehog generation that sniffs out edge cases.\n\n## Why `apropos`?\n\nTraditionally, Haskell programmers have used property testing, starting with the \nlegendary [QuickCheck](https://hackage.haskell.org/package/QuickCheck), to test \ntheir code. This entails writing properties that should hold about your program,\nessentially as functions from arbitrary types to `Bool`, and the library tests\nthat against random data, trying to find counterexamples that would represent\nincorrect code.\n\nThe problem with this is, property testing uses relatively simple data \ndistributions, and there are many bugs that are activated only by very specific \nvalues - so-called 'edge cases', that these generators are very unlikely to find.\n\nWe can motivate this with an example, based on \u003chttps://github.com/nick8325/quickcheck/issues/98\u003e:\n\nThe [abs](https://hackage.haskell.org/package/base/docs/Prelude.html#v:abs)\nfunction from Prelude is supposed to return the absolute value of a number. \n\nTherefore, this function should always hold:\n\n```haskell\nabsIsAlwaysPositive :: Int -\u003e Bool\nabsIsAlwaysPositive n = abs n \u003e= 0\n```\n\nUnfortunately, due to two's complement arithmetic, the negation of\n`minBound :: Int` is not representable as an `Int`. Prelude's `abs` function just\n gives up and returns `minBound`. Hence:\n\n ```haskell\n \u003e\u003e\u003e abs minBound :: Int\n-9223372036854775808\n\u003e\u003e\u003e absIsAlwaysPositive minBound\nFalse\n```\n\nProperty testing is unlikely to catch this, though:\n```haskell\n\u003e\u003e\u003e quickCheck absIsAlwaysPositive\n+++ OK, passed 100 tests.\n```\n\nThe solution is to write additional unit or property tests for any edge cases you\ncan think of. This quickly becomes tedious. As the complexity of your code grows,\nthis can become more and more of a problem, and you won't know if you've\nmissed something.\n\n## What is `apropos`?\n`apropos` integrates with the [Hedgehog]() testing library and attempts to solve this problem using 'description types', which describe and automatically test against\nedge cases.\n\nThe core of `apropos` is the `Description` typeclass. You define an instance of\nthis class to start testing your code.\n\n```haskell\nclass Description d a | d -\u003e a where\n  describe :: a -\u003e d\n\n  refineDescription :: Formula (Attribute d)\n  refineDescription = Yes\n\n  genDescribed :: (MonadGen m) =\u003e d -\u003e m a\n```\n\n`a` is the type of your test data. At present, only one value can be tested\nagainst at a time, but you can work around this by using a product type.\n\n`d` is a type you define describing the interesting properties of `a`. This is \nknown as the 'description type'.\n\nYou begin by defining a function `describe` that generates a description from a\nvalue.\n\nWhilst it is theoretically possible to define an ADT that precisely captures any\nset of properties, in practice this may be difficult or unergonomic to do. Instead,\nYou may restrict which descriptions are valid using the optional `refineDescription`\nmethod, using a logical formula. See the Haddocks for `Attribute` for more details.\n\nFinally, you write a `Hedgehog` generator that generates a value of type `a` matching\na given description.\n\nThis allows `apropos`, using the magic of SAT solvers, to run a given test over\nall combinations of properties, hopefully testing against all relevant edge cases.\n\nLet's use this to find our bug in `abs`.\n\nFirst, let's capture the interesting properties and possible edge cases of `Int`:\n```haskell\ndata IntDescr = IntDescr\n  { sign :: Sign\n  , size :: Size\n  , isBound :: Bool -- Is this equal to `minBound` or `maxBound`?\n  }\n  deriving stock (Generic, Eq, Ord, Show)\n  \ndata Sign = Positive | Negative | Zero\n  deriving stock (Generic, Eq, Ord, Show)\n\ndata Size = Large | Small\n  deriving stock (Generic, Eq, Ord, Show)\n```\n\nWe need `Ord` for the implementation of `apropos`. `Show` is needed by `Hedgehog`.\n`Generic` (from `GHC.Generics`) is required\nfor all description types, including the types of their fields recursively.\n\nNow let's define our `Description` instance, asserting that `IntDescr` describes\n`Int`:\n\n```haskell\ninstance Description IntDescr Int where\n```\n\nNext, we derive desciptions:\n\n```haskell\n  describe :: Int -\u003e IntDescr\n  describe i =\n    IntDescr\n      { sign =\n          case compare i 0 of\n            GT -\u003e Positive\n            EQ -\u003e Zero\n            LT -\u003e Negative\n      , size =\n          if i \u003e 10 || i \u003c -10\n            then Large\n            else Small\n      , isBound = i == minBound || i == maxBound\n      }\n```\n\nNot all constructible `IntDescr` values are valid descriptions - You can't have\na `Large` `Zero` or a `Small` `isBound`. So we implement `refineDescription` to\nexclude these:\n\n```haskell\n  refineDescription :: Formula (Attribute IntDescr)\n  refineDescription =\n    All\n      [ attr [(\"IntDescr\", \"sign\")] \"Zero\" :-\u003e: attr [(\"IntDescr\", \"size\")] \"Small\"\n      , attr [(\"IntDescr\", \"isBound\")] \"True\" :-\u003e: attr [(\"IntDescr\", \"size\")] \"Large\"\n      ]\n```\n\nWe now write a Hedgehog generator that generates an `Int` matching a given\ndescription:\n\n```haskell\n  genDescribed :: (MonadGen m) =\u003e IntDescr -\u003e m Int\n  genDescribed s =\n    case sign s of\n      Zero -\u003e pure 0\n      s' -\u003e intGen s'\n    where\n      bound :: Sign -\u003e Int\n      bound Positive = maxBound\n      bound Negative = minBound\n      bound Zero = 0\n\n      sig :: Sign -\u003e Int -\u003e Int\n      sig Negative = negate\n      sig _ = id\n\n      intGen :: (MonadGen m) =\u003e Sign -\u003e m Int\n      intGen s' =\n        if isBound s\n          then pure (bound s')\n          else case size s of\n            Small -\u003e int (linear (sig s' 1) (sig s' 10))\n            Large -\u003e int (linear (sig s' 11) (bound s' - sig s' 1))\n```\n\nThe `Description` typeclass is lawful:\n\n```haskell\n-- Given a value `a` generated from a generator for a description `d` \n-- (genDescribed d), the description of `a` (describe a) equals `d`.\nforall d. forAll (genDescribed d) \u003e\u003e= (\\a -\u003e describe a === d)\n```\n\nA `selfTest` method is provided to test that this law holds, and build confidence\nthat our `Description` instance is correct.\n\n```haskell\nintSimpleSelfTest :: Group\nintSimpleSelfTest =\n  Group\n    \"self test\"\n    (selfTest @IntDescr)\n```\n\n```haskell\nself test\n  IntDescr {sign = Positive, size = Large, isBound = False}: OK (0.03s)\n      ✓ IntDescr {sign = Positive, size = Large, isBound = False} passed 100 tests.\n  IntDescr {sign = Positive, size = Large, isBound = True}:  OK (0.03s)\n      ✓ IntDescr {sign = Positive, size = Large, isBound = True} passed 100 tests.\n  IntDescr {sign = Positive, size = Small, isBound = False}: OK (0.04s)\n      ✓ IntDescr {sign = Positive, size = Small, isBound = False} passed 100 tests.\n  IntDescr {sign = Negative, size = Large, isBound = False}: OK (0.05s)\n      ✓ IntDescr {sign = Negative, size = Large, isBound = False} passed 100 tests.\n  IntDescr {sign = Negative, size = Large, isBound = True}:  OK (0.03s)\n      ✓ IntDescr {sign = Negative, size = Large, isBound = True} passed 100 tests.\n  IntDescr {sign = Negative, size = Small, isBound = False}: OK (0.03s)\n      ✓ IntDescr {sign = Negative, size = Small, isBound = False} passed 100 tests.\n  IntDescr {sign = Zero, size = Small, isBound = False}:     OK (0.03s)\n      ✓ IntDescr {sign = Zero, size = Small, isBound = False} passed 100 tests.\n```\n\nNow let's test our function!\n\nWe use the `runTests` function and the `AproposTest` type to define our test:\n\n```haskell\nrunTests :: AproposTest d a -\u003e [(s, Property)]\n\ndata AproposTest d a = AproposTest\n  { expect :: d -\u003e Bool\n  , aproposTest :: a -\u003e PropertyT IO ()\n  }\n```\n\n`expect` is a predicate that defines whether the given description should cause \nthe test to pass or fail. `apropos` by default also tests the negation of each \nproperty to ensure it fails. You can filter out properties you don't want to\ntest at all using `runTestsWhere`.\n\n`aproposTest` is a `Hedgehog` property test that tests agains the given value `a`.\n\nThe return type of `runTests` is `IsString s =\u003e [(s, Property)]`, which can be \nplugged straight into Hedgehog's `Group`.\n\nSo our `abs` test looks like this:\n\n```haskell\nabsTest :: Group\nabsTest =\n  Group\n    \"apropos testing\"\n    $ runTests @IntDescr\n      AproposTest\n        { expect = const True -- should hold for all negative integers\n        , aproposTest = \\n -\u003e assert $ abs n \u003e= 0\n        }\n```\n\nAnd we find the bug!\n\n```haskell\n    apropos testing\n      IntDescr {sign = Positive, size = Large, isBound = False}: OK (0.04s)\n          ✓ IntDescr {sign = Positive, size = Large, isBound = False} passed 100 tests.\n      IntDescr {sign = Positive, size = Large, isBound = True}:  OK (0.02s)\n          ✓ IntDescr {sign = Positive, size = Large, isBound = True} passed 100 tests.\n      IntDescr {sign = Positive, size = Small, isBound = False}: OK (0.04s)\n          ✓ IntDescr {sign = Positive, size = Small, isBound = False} passed 100 tests.\n      IntDescr {sign = Negative, size = Large, isBound = False}: OK (0.04s)\n          ✓ IntDescr {sign = Negative, size = Large, isBound = False} passed 100 tests.\n      IntDescr {sign = Negative, size = Large, isBound = True}:  FAIL (0.03s)\n          ✗ IntDescr {sign = Negative, size = Large, isBound = True} failed at src/Apropos/Runner.hs:25:9\n            after 1 test.\n          \n               ┏━━ src/Apropos/Generator.hs ━━━\n            15 ┃ runTest :: (Show a, Description d a) =\u003e (a -\u003e PropertyT IO ()) -\u003e d -\u003e Property\n            16 ┃ runTest cond d = property $ forAll (genDescribed d) \u003e\u003e= cond\n               ┃ │ -9223372036854775808\n            \n               ┏━━ src/Apropos/Runner.hs ━━━\n            20 ┃ runAproposTest :: forall d a. (Description d a, Show a) =\u003e AproposTest d a -\u003e d -\u003e Property\n            21 ┃ runAproposTest atest d =\n            22 ┃   runTest\n            23 ┃     ( \\a -\u003e do\n            24 ┃         b \u003c- passes (aproposTest atest a)\n            25 ┃         expect atest d === b\n               ┃         ^^^^^^^^^^^^^^^^^^^^\n               ┃         │ ━━━ Failed (- lhs) (+ rhs) ━━━\n               ┃         │ - True\n               ┃         │ + False\n            26 ┃     )\n            27 ┃     d\n            28 ┃   where\n            29 ┃     passes :: PropertyT IO () -\u003e PropertyT IO Bool\n            30 ┃     passes =\n            31 ┃       PropertyT\n            32 ┃         . TestT\n            33 ┃         . ExceptT\n            34 ┃         . fmap (Right . isRight)\n            35 ┃         . runExceptT\n            36 ┃         . unTest\n            37 ┃         . unPropertyT\n          \n            This failure can be reproduced by running:\n            \u003e recheck (Size 0) (Seed 17184365450024726384 16839839501823744203) IntDescr {sign = Negative, size = Large, isBound = True}\n          \n        Use '--hedgehog-replay \"Size 0 Seed 17184365450024726384 16839839501823744203\"' to reproduce.\n        \n        Use -p '/apropos testing.IntDescr {sign = Negative, size = Large, isBound = True}/' to rerun this test only.\n      IntDescr {sign = Negative, size = Small, isBound = False}: OK (0.06s)\n          ✓ IntDescr {sign = Negative, size = Small, isBound = False} passed 100 tests.\n      IntDescr {sign = Zero, size = Small, isBound = False}:     OK (0.01s)\n          ✓ IntDescr {sign = Zero, size = Small, isBound = False} passed 100 tests.\n```\n\nGiven `minBound` (`IntDescr {sign = Negative, size = Large, isBound = True}`), \nthe test suite shows that the function return is incorrect.\n\nHappy testing!","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmlabs-haskell%2Fapropos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmlabs-haskell%2Fapropos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmlabs-haskell%2Fapropos/lists"}