{"id":21517574,"url":"https://github.com/tek/polysemy-resume","last_synced_at":"2025-08-28T06:13:04.042Z","repository":{"id":56875030,"uuid":"301783580","full_name":"tek/polysemy-resume","owner":"tek","description":"cross-interpreter errors for polysemy","archived":false,"fork":false,"pushed_at":"2025-04-19T19:52:59.000Z","size":222,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-07-30T17:44:35.721Z","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":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tek.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"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-10-06T16:08:53.000Z","updated_at":"2025-04-19T19:53:03.000Z","dependencies_parsed_at":"2023-01-31T07:45:16.486Z","dependency_job_id":"7882c001-d100-4a65-a48d-b083b0c5735e","html_url":"https://github.com/tek/polysemy-resume","commit_stats":{"total_commits":69,"total_committers":2,"mean_commits":34.5,"dds":0.4057971014492754,"last_synced_commit":"0044fed2b18feeca4c5213d07140372c9afcb28b"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/tek/polysemy-resume","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tek%2Fpolysemy-resume","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tek%2Fpolysemy-resume/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tek%2Fpolysemy-resume/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tek%2Fpolysemy-resume/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tek","download_url":"https://codeload.github.com/tek/polysemy-resume/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tek%2Fpolysemy-resume/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272452401,"owners_count":24937465,"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-28T02:00:10.768Z","response_time":74,"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":[],"created_at":"2024-11-24T00:43:01.464Z","updated_at":"2025-08-28T06:13:04.023Z","avatar_url":"https://github.com/tek.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# About\n\nThis library provides the Polysemy effects 'Resumable' and 'Stop' for the\npurpose of safely connecting throwing and catching errors across different\ninterpreters.\n\n# Example\n\nGiven these two effects and an error:\n\n```haskell\nimport Polysemy.Resume (Resumable, Stop, resumable, resumableFor, resume, runStop, stop)\n\ndata Stopper :: Effect where\n  StopBang :: Stopper m ()\n  StopBoom :: Stopper m ()\n\nmakeSem ''Stopper\n\ndata Resumer :: Effect where\n  MainProgram :: Resumer m Int\n\nmakeSem ''Resumer\n\ndata Boom =\n  Boom { unBoom :: Text }\n  |\n  Bang { unBang :: Int }\n  deriving stock (Eq, Show)\n```\n\nwe implement an interpreter for `Stopper` that may throw the error `Boom`, but\nwe do not want to hardcode that fact into the effect constructors, as in:\n\n```haskell\ndata Stopper :: Effect where\n  StopBang :: Stopper m (Either Boom ())\n```\n\nbecause we might want to provide alternative interpreters that do not have this\nrequirement, and `Boom` might contain information about the interpreter\nimplementation that we don't want to leak into the effect signature.\n\nOn the other hand, having no guarantee that the consumer program knows about or\ncatches the error requires us to manually ensure we handle them at the\nappropriate location.\nThis is especially critical due to the fact that using `catch` requires an\n`Error` membership, which in turn requires the `Error` to be handled outside of\nthe consumer again, hiding any new uses of the throwing interpreter in another\npart of the program.\n\nA first attempt at making this situation safer is to introduce a wrapping\neffect:\n\n```haskell\ndata Resumable err eff m a where\n  Resumable ::\n    ∀ err eff r a .\n    Weaving eff (Sem r) a -\u003e\n    Resumable err eff (Sem r) (Either err a)\n```\n\nwhich we now use instead of the raw `eff` in consumers:\n\n```haskell\ninterpretResumer ::\n  Member (Resumable Boom Stopper) r =\u003e\n  InterpreterFor Resumer r\ninterpretResumer =\n  interpret \\ MainProgram -\u003e\n    resume (192 \u003c$ stopBang) \\ _ -\u003e\n      pure 237\n```\n\nFor a nicer syntax, there is a type alias for `Resumable`:\n\n```haskell\nMember (Stopper !! Boom) r =\u003e\n```\n\nWe have now marked the interpreter for `Resumer`, which consumes `Stopper`, as\nbeing capable of handling the `Boom` error when it occurs in `Stopper`.\nThe function `resume` takes an error handler as its second argument with which\nwe can catch `Boom`.\n\nThe interpreter for `Stopper` could look like this:\n\n```haskell\ninterpretStopper ::\n  Member (Stop Boom) r =\u003e\n  InterpreterFor Stopper r\ninterpretStopper =\n  interpret \\case\n    StopBang -\u003e stop (Bang 13)\n    StopBoom -\u003e stop (Boom \"ouch\")\n```\n\nInstead of `Error`, we are using `Stop` here, which is identical except for not\nproviding `Catch`.\nThis only serves to be more explicit about the intention of the error, but\na regular `Error` can be converted with `stopOnError`.\n\nIn order to convert this interpreter to a `Resumable`, we use `resumable`:\n\n```haskell\n\u003e\u003e\u003e run $ resumable interpretStopper (interpretResumer mainProgram)\n237\n```\n\n`resumable` weaves `interpretStopper` and its `Stop` together into\na `Resumable`, which is then consumed entirely by `resume` inside\n`interpretResumer`, so no additional effects have to be handled.\n\n## Higher-Order Effects\n\nConverting an interpreter with `resumable` only works in rather simple\nconditions.\nIf there are higher-order effects involved, you may get incorrect semantics,\nfor example when inserting a `finally` around the entire resumable program:\n\n```haskell\nresumable (interpretStopper (sem `finally` releaseResources))\n```\n\nIn this case, `releaseResources` is executed after each use of `StopBang` or\n`StopBoom`.\nThis requires the use of `interpretResumable` and `interpretResumableH`, which\ntake handler functions like `interpret`:\n\n```haskell\ninterpretStopper ::\n  InterpreterFor (Stopper !! Boom) r\ninterpretStopper =\n  interpretResumable \\case\n    StopBang -\u003e stop (Bang 13)\n    StopBoom -\u003e stop (Boom \"ouch\")\n```\n\n## Partial Error Handling\n\nOf course, one requirement in the problem description still remains\nunsatisfied: We might want to hide implementation details of `interpretStopper`\nfrom consumers.\nWe can do that by transforming the `Boom` error into a more abstract version at\nthe interpretation site:\n\n```haskell\nnewtype Blip =\n  Blip { unBlip :: Int }\n  deriving stock (Eq, Show)\n\nbangOnly :: Boom -\u003e Maybe Blip\nbangOnly = \\case\n  Bang n -\u003e Just (Blip n)\n  Boom _ -\u003e Nothing\n```\n\nNow `Boom` might have contained information about e.g. an http client backend,\nand we're transforming that into an error that just says \"http error\".\nIf a consumer also deals with that backend, we might keep that information.\n\nThis modified error can now be used for `Resumable`.\nFirst, we change the `Resumer` interpreter to use `Blip`:\n\n```haskell\ninterpretResumerPartial ::\n  Member (Resumable Blip Stopper) r =\u003e\n  InterpreterFor Resumer r\ninterpretResumerPartial =\n  interpret \\ MainProgram -\u003e\n    resume (192 \u003c$ stopBang) \\ (Blip num) -\u003e\n      pure (num * 3)\n```\n\nThen we use a different adapter function for `interpretStopper`:\n\n```haskell\n\u003e\u003e\u003e runError (resumableFor bangOnly interpretStopper (interpretResumerPartial mainProgram))\nRight 39\n```\n\n`resumableFor` transforms the error and passes it to the consumer if it is\na `Just`, and rethrows it if not.\nSince the error was only partially handled and unhandled errors get thrown as\n`Error`, we have to call `runError` on the result, to obtain an `Either`.\n\nIf the consumer uses a constructor that throws an unhandled variant of the\nerror, it propagates to the call site:\n\n```haskell\ninterpretResumerPartialUnhandled ::\n  Member (Resumable Blip Stopper) r =\u003e\n  InterpreterFor Resumer r\ninterpretResumerPartialUnhandled =\n  interpret \\ MainProgram -\u003e\n    resume (192 \u003c$ stopBoom) \\ (Blip num) -\u003e\n      pure (num * 3)\n\n\u003e\u003e\u003e runError ((resumableFor bangOnly interpretStopper) (interpretResumerPartialUnhandled mainProgram))\nLeft (Boom \"ouch\")\n```\n\n# Thanks\n\nto @KingOfTheHomeless for providing the initial implementation and lots of consultation!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftek%2Fpolysemy-resume","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftek%2Fpolysemy-resume","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftek%2Fpolysemy-resume/lists"}