{"id":16337013,"url":"https://github.com/gvolpe/par-dual","last_synced_at":"2025-10-25T23:31:16.234Z","repository":{"id":56874551,"uuid":"258824662","full_name":"gvolpe/par-dual","owner":"gvolpe","description":":repeat: ParDual class for a Parallel \u003c-\u003e Sequential relationship","archived":false,"fork":false,"pushed_at":"2020-05-18T16:17:00.000Z","size":63,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-09T01:14:27.677Z","etag":null,"topics":["applicative","dual","monad","parallel"],"latest_commit_sha":null,"homepage":"https://hackage.haskell.org/package/par-dual-0.1.0.0","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/gvolpe.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}},"created_at":"2020-04-25T16:40:57.000Z","updated_at":"2021-08-20T21:28:39.000Z","dependencies_parsed_at":"2022-08-20T22:30:37.062Z","dependency_job_id":null,"html_url":"https://github.com/gvolpe/par-dual","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/gvolpe%2Fpar-dual","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gvolpe%2Fpar-dual/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gvolpe%2Fpar-dual/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gvolpe%2Fpar-dual/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gvolpe","download_url":"https://codeload.github.com/gvolpe/par-dual/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238223724,"owners_count":19436722,"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":["applicative","dual","monad","parallel"],"created_at":"2024-10-10T23:45:35.981Z","updated_at":"2025-10-25T23:31:10.942Z","avatar_url":"https://github.com/gvolpe.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# par-dual\n\n[![CI Status](https://github.com/gvolpe/par-dual/workflows/Haskell%20CI/badge.svg)](https://github.com/gvolpe/par-dual/actions)\n\nThe [PureScript](https://www.purescript.org/) language defines a [Parallel](https://pursuit.purescript.org/packages/purescript-parallel/4.0.0/docs/Control.Parallel.Class#t:Parallel) typeclass in the `parallel` package. Quoting its documentation:\n\n\u003e The `Parallel` class abstracts over monads which support parallel composition via some related `Applicative`.\n\nThe same typeclass is defined in the [Scala](https://www.scala-lang.org/) language, as part of the [Cats](https://typelevel.org/cats/typeclasses/parallel.html) library.\n\nThis typeclass has been controversial, in a sense, for not having strong laws. However, it has been proven to be actually useful in real-world applications.\n\nThe idea of this package is to bring this power over to the Haskell language while exploring the design space to identify and define stronger laws (if possible).\n\nOriginally, this idea has been described in [this blogpost](https://gvolpe.github.io/blog/parallel-typeclass-for-haskell/).\n\n## ParDual\n\nHere's the definition of the same typeclass in Haskell:\n\n```haskell\nclass (Monad m, Applicative f) =\u003e ParDual f m | m -\u003e f, f -\u003e m where\n  parallel :: forall a . m a -\u003e f a\n  sequential :: forall a . f a -\u003e m a\n```\n\nI decided to call it `ParDual` instead of `Parallel`, because this *duality* doesn't always define a `sequential` and `parallel` relationship. Such is the case between `[]` and `ZipList`, as we will soon discover.\n\nIt defines two functions, which are natural transformations between a `Monad m` and an `Applicative f`. It could also be seen as a typeclass version of an isomorphism such as `forall a . Iso (f a) (m a)`.\n\nThe most common and useful relationships are both `Either` / `Validation` and `IO` / `Concurrently`.\n\n```haskell\ninstance Semigroup e =\u003e ParDual (Validation e) (Either e) where\n  parallel   = fromEither\n  sequential = toEither\n\ninstance ParDual Concurrently IO where\n  parallel   = Concurrently\n  sequential = runConcurrently\n```\n\n`Validation` comes from the [validators](https://hackage.haskell.org/package/validators) package, whereas `Concurrently` comes from the [async](https://hackage.haskell.org/package/async) package.\n\nAdditionally, we can define a lot of powerful functions solely in terms of `Applicative`, `Monad`, and `ParDual`. A few other functions might require extra requirements, such as `Traversable`.\n\n### parMapN\n\nThe `parMapN` set of functions are analogue to combining `\u003c$\u003e` and `\u003c*\u003e`, for any dual `Applicative`.\n\n```haskell\nparMap2\n  :: (Applicative f, Monad m, ParDual f m)\n  =\u003e m a0\n  -\u003e m a1\n  -\u003e (a0 -\u003e a1 -\u003e a)\n  -\u003e m a\n```\n\nIn this case, `parMap2` takes only two computations and a function, but you can find other versions up to `parMap6`. If there is demand, we can consider abstracting over its arity, in order to compose an arbitrary number of computations.\n\nFor example, if we define a `Person` datatype with two fields:\n\n```haskell\ntype Name = Refined NonEmpty String\ntype Age = Refined (GreaterThan 17) Int\n\ndata Person = Person\n  { personAge :: Age\n  , personName :: Name\n  } deriving Show\n```\n\nWe can then validate different inputs, while accumulating errors on the left side, even when our type is `Either [String] Person`.\n\n```haskell\nmkPerson :: Int -\u003e String -\u003e Either [String] Person\nmkPerson a n = parMap2 (ref a) (ref n) Person\n```\n\nWhere `ref` is a generic function that converts `RefineException`s to `[String]`:\n\n```haskell\nref :: Predicate p x =\u003e x -\u003e Either [String] (Refined p x)\nref x = left (\\e -\u003e [show e]) (refine x)\n```\n\nIn case of two invalid inputs, we will get as a result a list of validation errors:\n\n```haskell\nmkPerson 10 \"\" == Left [\"error 1\", \"error 2\"]\n```\n\nIf `parMapN` didn't exist, we could do the same by manually converting between `Either` and `Validation` (which is exactly what `parMapN` does via the `ParDual` class).\n\n```haskell\nmkPerson :: Int -\u003e String -\u003e Either [String] Person\nmkPerson a n = toEither $ Person \u003c$\u003e fromEither (ref a) \u003c*\u003e fromEither (ref n)\n```\n\nThough, we can see how cumbersome and boilerplatey it gets.\n\n### parTraverse\n\nAnother great application of the `ParDual` class is the definition of a `traverse` function that takes a `Monad` and a `Traversable t`, but that operates over its dual `Applicative`, and at the end it converts back to this `Monad`.\n\n```haskell\nparTraverse\n  :: (Traversable t, Applicative f, Monad m, ParDual f m)\n  =\u003e (a -\u003e m b)\n  -\u003e t a\n  -\u003e m (t b)\n```\n\nThe type signature is exactly the same as `traverse`, except the constraints are different.\n\nWe can appreciate its usability by looking at some examples. Here's one with `Either`:\n\n```haskell\nf :: Int -\u003e Either [String] Int\nf n = Left [show n]\n\ntraverse f [1..5] == Left [\"1\"]\nparTraverse f [1..5] == Left [\"1\",\"2\",\"3\",\"4\",\"5\"]\n```\n\nBelow there is another one with `IO`:\n\n```haskell\nrandomDelay :: IO ()\nrandomDelay = do\n  r \u003c- randomRIO (1, 10)\n  threadDelay (r * 500000)\n\ntraverseIO :: IO ()\ntraverseIO = traverse_ (\\n -\u003e randomDelay \u003e\u003e print n) [1 .. 10]\n\nparTraverseIO :: IO ()\nparTraverseIO = parTraverse_ (\\n -\u003e randomDelay \u003e\u003e print n) [1 .. 10]\n```\n\nThe `traverse` version prints out numbers from 1 to 10 in sequence, while waiting for every random delay. So the output is pretty much `1 2 3 4 5 6 7 8 9 10`.\n\nThe `parTraverse` version has a non-deterministic output, since it goes through `Concurrently` (`IO`'s dual). It is exactly what you would expect while using [mapConcurrently](https://hackage.haskell.org/package/async-2.2.2/docs/Control-Concurrent-Async.html#v:mapConcurrently). One possible output is `5 10 6 1 3 2 9 4 7 8`.\n\n### ZipList\n\nThe dual `Applicative` instance of `[]` is the one defined by `ZipList`, which doesn't have anything to do with parallelism.\n\n```haskell\ninstance ParDual ZipList [] where\n  parallel   = ZipList\n  sequential = getZipList\n```\n\nLet's have a look at the examples shown below.\n\n```haskell\n((+) \u003c$\u003e [1..5] \u003c*\u003e [6..10]) == [7,8,9,10,11,8,9,10,11,12,9,10,11,12,13,10,11,12,13,14,11,12,13,14,15]\n\nparMap2 [1..5] [6..10] (+) == [7,9,11,13,15]\n```\n\nThe standard version iterates over both lists \"sequentially\". That is, it iterates over the first one, and then over the second one, returning the cartesian product of both lists.\n\nConversely, `ZipList`s only return the sum of the current elements of both lists such as `1 + 6`, `2 + 7`, and so on. It iterates over both lists in \"parallel\", effectively traversing both at once.\n\n### parBitraverse\n\nOperates over any `Bitraversable` such as `Either` or `(,,)`.\n\n```haskell\nres1 = [(\"ba\",'2','T'),(\"ba\",'2','r'),(\"ba\",'2','u'),(\"ba\",'2','e'),(\"ba\",'4','T'),(\"ba\",'4','r'),(\"ba\",'4','u'),(\"ba\",'4','e')]\n\n(bitraverse show show (\"ba\", 24, True)) == res1\n```\n\nThe standard `bitraverse` for `(String, Int, Bool)` traverses over the second value and then over the third value, combining the results on each iteration.\n\n```haskell\n(parBitraverse show show (\"ba\", 24, True)) == [(\"ba\",'2','T'),(\"ba\",'4','r')]\n```\n\nThe dual variant traverses all the values at the same time, terminating as soon as either value is empty.\n\n## Test suite\n\nThe test suite property-checks the functions defined in `ParDual` applied to different types of values.\n\n```\n$ nix-shell --run 'cabal new-run par-dual-tests'\n━━━ Main ━━━\n  ✓ prop_parMap2_on_success passed 100 tests.\n  ✓ prop_parMap2_accumulates_errors passed 100 tests.\n  ✓ prop_parTraverse_accumulates_errors passed 100 tests.\n  ✓ prop_parTraverse_io_is_concurrent passed 10 tests.\n  ✓ prop_parMap2_on_lists passed 100 tests.\n  ✓ prop_parBitraverse passed 100 tests.\n  ✓ 6 succeeded.\n```\n\n## Publishing\n\nGenerating documentation and tarball file to upload.\n\n```\n$ cabal new-haddock --haddock-for-hackage --enable-doc\n$ cabal upload -d dist-newstyle/par-dual-0.1.0.0-docs.tar.gz\n$ cabal new-sdist\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgvolpe%2Fpar-dual","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgvolpe%2Fpar-dual","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgvolpe%2Fpar-dual/lists"}