{"id":17692895,"url":"https://github.com/typedbyte/effet","last_synced_at":"2025-04-16T05:55:20.178Z","repository":{"id":62435884,"uuid":"280376594","full_name":"typedbyte/effet","owner":"typedbyte","description":"An effect system based on type classes, written in Haskell.","archived":false,"fork":false,"pushed_at":"2021-11-10T14:33:20.000Z","size":152,"stargazers_count":44,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-03-15T13:45:58.614Z","etag":null,"topics":["effects","handlers","haskell"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/typedbyte.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}},"created_at":"2020-07-17T08:56:59.000Z","updated_at":"2024-01-23T13:55:55.000Z","dependencies_parsed_at":"2022-11-01T21:30:34.708Z","dependency_job_id":null,"html_url":"https://github.com/typedbyte/effet","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/typedbyte%2Feffet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typedbyte%2Feffet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typedbyte%2Feffet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typedbyte%2Feffet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/typedbyte","download_url":"https://codeload.github.com/typedbyte/effet/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249205509,"owners_count":21229936,"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":["effects","handlers","haskell"],"created_at":"2024-10-24T13:07:27.942Z","updated_at":"2025-04-16T05:55:20.144Z","avatar_url":"https://github.com/typedbyte.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n\u003cimg src=\"./logo.png\"\u003e\n\u003c/p\u003e\n\n# effet\n\n[![Hackage](https://img.shields.io/hackage/v/effet.svg?logo=haskell\u0026label=effet)](https://hackage.haskell.org/package/effet)\n\n[plucking constraints]: https://www.parsonsmatt.org/2020/01/03/plucking_constraints.html\n\n[Overview]: https://github.com/typedbyte/effet#overview\n[Quick-Start Guide]: https://github.com/typedbyte/effet#quick-start-guide\n[Defining Effects]: https://github.com/typedbyte/effet#defining-effects\n[Using Effects]: https://github.com/typedbyte/effet#using-effects\n[Defining Effect Handlers]: https://github.com/typedbyte/effet#defining-effect-handlers\n[Using Effect Handlers]: https://github.com/typedbyte/effet#using-effect-handlers\n[Tagging, Retagging, Untagging]: https://github.com/typedbyte/effet#tagging-retagging-untagging\n[Limitations and Remarks]: https://github.com/typedbyte/effet#limitations-and-remarks\n\n* [Overview][]\n* [Quick-Start Guide][]\n  * [Defining Effects][]\n  * [Using Effects][]\n  * [Defining Effect Handlers][]\n  * [Using Effect Handlers][]\n  * [Tagging, Retagging, Untagging][]\n* [Limitations and Remarks][]\n\n## Overview\n\n`effet` is an effect system based on type classes, written in Haskell. A central idea of an effect system is to track effects (like performing a file system access) on the type level in order to describe the behavior of functions more precisely. Another central idea of an effect system is the well-known design principle of separating an interface (the *effect*) from its actual implementation (the *effect handler* or *effect interpreter*). Hence, `effet` allows developers to write programs by composing functions which describe their effects on the type level, and to provide different implementation strategies for those effects when running the programs.\n\n`effet` is by far not the first library which pursues these ideas. It borrows various ideas from existing libraries, just to name a few:\n\n* It is based on type classes, like `mtl` and `fused-effects`.\n* It is based on [plucking constraints][], like `mtl`, but without the \"n² instances problem\".\n* It supports `TemplateHaskell`-based generation of the effect infrastructure, like `polysemy`.\n* It supports functional dependencies and tagged effects for effect disambiguation, like `ether`.\n\n`effet` has a rather down-to-earth implementation without much magic. It is like a thin wrapper around the `transformers` library and its lifting friends `transformers-base` and `monad-control`, thus minimizing the reinvention of the wheel. The library and its documentation can be found on [Hackage](https://hackage.haskell.org/package/effet).\n\n## Quick-Start Guide\n\nExamples can be found in the [examples](/examples/Example) folder. The following sections give a quick overview of the most important features.\n\n### Defining Effects\n\nWhen defining effects or effect handlers, the module *Control.Effect.Machinery* provides everything we need:\n\n```haskell\nimport Control.Effect.Machinery\n```\n\nEffects are ordinary type classes, like the following:\n\n```haskell\nclass Monad m =\u003e FileSystem' tag m where\n  readFile'  :: FilePath -\u003e m String\n  writeFile' :: FilePath -\u003e String -\u003e m ()\n```\n\nNote the `tag` type parameter. Such a parameter is used to disambiguate effects of the same type, i.e. we can use multiple `FileSystem` effects in our program simultaneously. In `effet`, the naming convention is to use an apostrophe as name suffix whenever we use tags. However, `effet` is not limited to that, we can easily define our effect without a `tag` parameter:\n\n```haskell\nclass Monad m =\u003e FileSystem m where\n  readFile  :: FilePath -\u003e m String\n  writeFile :: FilePath -\u003e String -\u003e m ()\n```\n\nLet's go on with the tagged version for now and pretend that the untagged type class does not exist. The next step is to generate the effect handling, lifting and tagging infrastructure for our new effect using the `TemplateHaskell` language extension:\n\n```haskell\nmakeTaggedEffect ''FileSystem'\n```\n\nThis line generates the necessary infrastructure for combining our new effect with other effects. We also get the untagged version of the effect for free, i.e. the corresponding untagged definitions (`FileSystem`, `readFile`, `writeFile`, note the missing apostrophes) are generated for us. This line also generates functions which help us to tag (`tagFileSystem'`), retag\n(`retagFileSystem'`) and untag (`untagFileSystem'`) our effect. More on that later.\n\n`effet` also provides the function `makeTaggedEffectWith` to define our own naming convention if we don't like the apostrophes. For untagged effects, we would have simply used the function `makeEffect` instead.\n\n### Using Effects\n\nWe can now use our effect to write programs that access the file system. We will define a simple program which appends something to a file. In order to make it more interesting, we will combine it with the pre-defined `Writer` effect to report the file size before and after appending, just to demonstrate the interplay with other effects:\n\n```haskell\nimport Control.Effect.Writer\n```\n\n```haskell\nprogram :: (FileSystem m, Writer [Int] m) =\u003e FilePath -\u003e String -\u003e m ()\nprogram path txt = do\n  content \u003c- readFile path\n  let size = length content\n  tell [size]\n  seq size $ writeFile path (content ++ txt)\n  tell [size + length txt]\n```\n\nIn order to run this program, we need a handler (or \"interpreter\") for our effect. There are several ways to interpret our effect. We could, for example, really access our local file system or just provide a virtual, in-memory file system. Hence, we will define two effect handlers for demonstration purposes.\n\n### Defining Effect Handlers\n\nFirst of all, we could \u0026ndash; in theory \u0026ndash; interpret effects without using `effet` at all, which is something that is not possible in many other effect systems. Since effects are ordinary type classes, we would just instantiate the monad type `m` with a type that provides instances for all (!) the effect type classes that constrain `m`. This is where the so-called \"n² instances problem\" of `mtl` comes from, since every effect handler that wants to handle a single effect (i.e., that wants to provide a type class instance for the effect it wants to handle) must also provide type class instances for all other effects \u0026ndash; some of which might not even be known at compile-time \u0026ndash; in order to delegate them to their corresponding effect handlers.\n\nThe advantage of `effet` is the ability to interpret effects separately, one by one, by [plucking constraints][] and not caring about all effects at the same time. In `effet`, we write an effect handler by defining a type and a corresponding type class instance for the effect we are interested in and let the infrastructure of `effet` handle the delegation of all other effects to their corresponding effect handlers.\n\nAn effect handler is a monad transformer which provides an instance for our effect type class. First, let's define the handler type for the local file system handler. We can derive all the necessary instances for proper effect lifting, some of them by using the `DerivingVia` language extension:\n\n```haskell\nnewtype LocalFS m a =\n  LocalFS { runLocalFS :: m a }\n    deriving (Applicative, Functor, Monad, MonadIO)\n    deriving (MonadTrans, MonadTransControl) via IdentityT\n    deriving (MonadBase b, MonadBaseControl b)\n```\n\nNext, we provide the instance for our `FileSystem'` effect. We need another import for that:\n\n```haskell\nimport qualified System.IO as IO\n```\n\n```haskell\ninstance MonadIO m =\u003e FileSystem' tag (LocalFS m) where\n  readFile' path = liftIO $ IO.readFile path\n  writeFile' path txt = liftIO $ IO.writeFile path txt\n```\n\nNote that we do not need a separate instance for the untagged version of our effect which was generated before.\n\nIn order to make the usage of our instance more comfortable, we additionally provide two functions which instruct the type system to use our particular instance for handling the `FileSystem` effect when running a particular program. We write one tagged and one untagged version:\n\n```haskell\nrunLocalFileSystem' :: (FileSystem' tag `Via` LocalFS) m a -\u003e m a\nrunLocalFileSystem' = coerce\n\nrunLocalFileSystem :: (FileSystem `Via` LocalFS) m a -\u003e m a\nrunLocalFileSystem = runLocalFileSystem' @G\n```\n\nWe can also let `effet` generate the untagged version of `runLocalFileSystem`, so we don't have to write it by hand:\n\n```haskell\nmakeUntagged ['runLocalFileSystem']\n```\n\nNow let's provide similar definitions for our virtual file system. Instead of `IO`, we will use the `Map` effect that is shipped with `effet` in order to map file paths to their corresponding contents. For simplification, we will assume that the contents of non-existing files are empty strings:\n\n```haskell\nnewtype VirtualFS m a =\n  VirtualFS { runVirtualFS :: m a }\n    deriving (Applicative, Functor, Monad, MonadIO)\n    deriving (MonadTrans, MonadTransControl) via IdentityT\n    deriving (MonadBase b, MonadBaseControl b)\n\ninstance Map' tag FilePath String m =\u003e FileSystem' tag (VirtualFS m) where\n  readFile' path = VirtualFS $ fromMaybe \"\" \u003c$\u003e lookup' @tag path\n  writeFile' path txt = VirtualFS $ insert' @tag path txt\n\nrunVirtualFileSystem' :: (FileSystem' tag `Via` VirtualFS) m a -\u003e m a\nrunVirtualFileSystem' = coerce\n\nmakeUntagged ['runVirtualFileSystem']\n```\n\n### Using Effect Handlers\n\nNow we have all our puzzle pieces together in order to run our program with different effect handlers. Let's recap the type of our program:\n\n```haskell\nprogram :: (FileSystem m, Writer [Int] m) =\u003e FilePath -\u003e String -\u003e m ()\n```\n\nLet's see what happens if we use our `runVirtualFileSystem` function on that program after we feed it some file path and the content that should be appended to the file:\n\n```haskell\ntest :: (Map FilePath String m, Writer [Int] m) =\u003e m ()\ntest = runVirtualFileSystem $ program \"/tmp/test.txt\" \"hello\"\n```\n\nWhat happened? We handled the `FileSystem` effect using our virtual file system handler (i.e., we \"plucked the constraint\"), which gives us a new program as result that still needs its `Map` and `Writer` effects handled. We essentially reinterpreted one effect (`FileSystem`) in terms of another (`Map`) without touching all the other effects (`Writer`). In order to run the remaining two effects, we just need to import some of the pre-defined effect handlers for the `Map` and `Writer` effects:\n\n```haskell\nimport Control.Effect.Map.Lazy\nimport Control.Effect.Writer.Lazy\n```\n\nAnd here is the complete code for running our program either locally or virtually:\n\n```haskell\nrunLocalProgram :: MonadIO m =\u003e m ([Int], ())\nrunLocalProgram\n  = runWriter\n  . runLocalFileSystem\n  $ program \"/tmp/test.txt\" \"hello\"\n\nrunVirtualProgram :: Monad m =\u003e m ([Int], ())\nrunVirtualProgram\n  = runWriter\n  . runMap\n  . runVirtualFileSystem\n  $ program \"/tmp/test.txt\" \"hello\"\n```\n\nAs we can see by the types, no `IO` is involved in the virtual program, since we do not touch the actual file system.\n\nWe decided to handle our `Map` and `Writer` effects using their lazy implementations. We could, for example, easily switch the handlers to their strict counterparts, or even use some other Map-like implementation for our `Map` effect, like Redis.\n\n### Tagging, Retagging, Untagging\n\nIf we write a program with multiple `FileSystem'` effects, we can disambiguate them using the tags ...\n\n```haskell\nfsProgram :: (FileSystem' \"fs1\" m, FileSystem' \"fs2\" m) =\u003e m ()\nfsProgram = do\n  writeFile' @\"fs1\" \"/tmp/test.txt\" \"first content\"\n  writeFile' @\"fs2\" \"/tmp/test.txt\" \"second content\"\n```\n\n... and interpret them differently, one using the local and one using the virtual file system, for example:\n\n```haskell\nrunDifferently :: MonadIO m =\u003e m ()\nrunDifferently\n  = runMap' @\"fs1\"\n  . runVirtualFileSystem' @\"fs1\"\n  . runLocalFileSystem' @\"fs2\"\n  $ fsProgram\n```\n\nWe could also use one tagged (`FileSystem'`) and one untagged (`FileSystem`) effect to disambiguate them, of course.\n\nWe can also change the tags of our effects before interpretation using the generated `tagFileSystem'`, `retagFileSystem'` and `untagFileSystem'` functions. We could, for example, merge the two effects into a single tag and interpret them uniformly:\n\n```haskell\nrunUniformly :: Monad m =\u003e m ()\nrunUniformly\n  = runMap' @\"fs1\"\n  . runVirtualFileSystem' @\"fs1\"\n  . retagFileSystem' @\"fs2\" @\"fs1\"\n  $ fsProgram\n```\n\nIn the example above, we merge tag `fs2` into tag `fs1` and interpret them together using the virtual file system. We can achieve the same result by untagging both effects and then using the untagged versions of the interpretation functions:\n\n```haskell\nrunUntagged :: Monad m =\u003e m ()\nrunUntagged\n  = runMap\n  . runVirtualFileSystem\n  . untagFileSystem' @\"fs2\"\n  . untagFileSystem' @\"fs1\"\n  $ fsProgram\n```\n\nTagging, retagging and untagging are useful if we want to compose two functions which have the same effects, but we want to interpret them differently after composition. Note that there is a chance that two authors write two different libraries using the same effects, but do not know from each other ...\n\n```haskell\n-- somewhere in library A\nfunctionA :: FileSystem m =\u003e m ()\nfunctionA = ...\n```\n\n```haskell\n-- somewhere in library B\nfunctionB :: FileSystem m =\u003e m ()\nfunctionB = ...\n```\n\n... and we want to compose these functions, but interpret the effects differently after composition:\n\n```haskell\n-- oops, the effects were merged!\nourProgram :: FileSystem m =\u003e m ()\nourProgram = functionA \u003e\u003e functionB\n```\n\nWe then need to introduce a tag for at least one of the two functions ...\n\n```haskell\n-- now the effects are separated\nourProgram :: (FileSystem m, FileSystem' \"b\" m) =\u003e m ()\nourProgram = functionA \u003e\u003e tagFileSystem' @\"b\" functionB\n```\n\n... which again allows us to interpret the effects independently as described above.\n\n## Limitations and Remarks\n\n* `TemplateHaskell`-based code generation can yield code that does not compile if you go crazy with `m`-based parameters in higher-order effect methods (where `m` is the monad type parameter of the effect type class). In such cases, one has to write the necessary type class instances by hand. They are explained in the documentation of the module `Control.Effect.Machinery.TH`.\n* The performance should be `mtl`-like, but this has not been verified yet.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftypedbyte%2Feffet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftypedbyte%2Feffet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftypedbyte%2Feffet/lists"}