# in-other-words [![Hackage](https://img.shields.io/hackage/v/in-other-words.svg?logo=haskell)](https://hackage.haskell.org/package/in-other-words) [![build GHC 8.6](https://github.com/KingoftheHomeless/in-other-words/workflows/build%20GHC%208.6/badge.svg)](https://github.com/KingoftheHomeless/in-other-words/actions?query=workflow%3A%22build+GHC+8.6%22) [![build GHC 8.8](https://github.com/KingoftheHomeless/in-other-words/workflows/build%20GHC%208.8/badge.svg)](https://github.com/KingoftheHomeless/in-other-words/actions?query=workflow%3A%22build+GHC+8.8%22) [![build GHC 8.10](https://github.com/KingoftheHomeless/in-other-words/workflows/build%20GHC%208.10/badge.svg)](https://github.com/KingoftheHomeless/in-other-words/actions?query=workflow%3A%22build+GHC+8.10%22) [![build GHC 9.0.2](https://github.com/KingoftheHomeless/in-other-words/workflows/build%20GHC%209.0.2/badge.svg)](https://github.com/KingoftheHomeless/in-other-words/actions?query=workflow%3A%22build+GHC+9.0.2%22) [![build GHC 9.2.3](https://github.com/KingoftheHomeless/in-other-words/workflows/build%20GHC%209.2.3/badge.svg)](https://github.com/KingoftheHomeless/in-other-words/actions?query=workflow%3A%22build+GHC+9.2.3%22) - [Overview][] - [Features][] - [Required Language Extensions][] - [Examples of Simple Usage][] - [Advanced Usage][] - [Troubleshooting][] - [Performance][] [Overview]: https://github.com/KingoftheHomeless/in-other-words#overview [Features]: https://github.com/KingoftheHomeless/in-other-words#features [Required Language Extensions]: https://github.com/KingoftheHomeless/in-other-words#required-language-extensions [Examples of Simple Usage]: https://github.com/KingoftheHomeless/in-other-words#examples-of-simple-usage [Advanced Usage]: https://github.com/KingoftheHomeless/in-other-words#advanced-usage [Troubleshooting]: https://github.com/KingoftheHomeless/in-other-words#troubleshooting [Performance]: https://github.com/KingoftheHomeless/in-other-words#performance ## Overview `in-other-words` is an effect system in the vein of [`freer-simple`](https://github.com/lexi-lambda/freer-simple), [`fused-effects`](https://github.com/fused-effects/fused-effects), [`polysemy`](https://github.com/polysemy-research/polysemy), and [`eff`](https://github.com/hasura/eff). It represents effects through data types, making it simple to define, use, and interpret them. The goal of `in-other-words` is to be as expressive and general of an effect system as possible while solving the *O(n2)* instances problem. Its hallmark feature is the novel approach it takes to support higher-order effects, making it significantly more powerful -- and sometimes easier to use -- than other effect libraries of its kind. If you're experienced with the mechanisms behind `freer-simple`, `fused-effects`, and `polysemy`, and would like to learn more about what makes `in-other-words` different, see [this wiki page](https://github.com/KingoftheHomeless/in-other-words/wiki/The-Inner-Workings-of-the-Library). Unfortunately, in its current state `in-other-words` is rather inaccessible. Ample documentation and guides are provided for the library, but inexperienced users are still likely to run into gotchas that result in very confusing error messages. As such, if you're a beginner to effect systems, `freer-simple` or `polysemy` would serve as better starting points. ## Features ### Simple higher-order effects Unlike `fused-effects` and `polysemy` -- which both have intimidating boilerplate associated with the interpretation of higher-order effects --`in-other-words` makes it just as easy to interpret higher-order effects as first-order effects. Go [here](#higher-order) for an example. ### No cumbersome restrictions to effects Every effect system previously mentioned has serious restrictions in what effects they may represent. * `freer-simple` is restricted to first-order effects. * `fused-effects` and `polysemy` are built around [*Effect Handlers in Scope*](https://www.cs.ox.ac.uk/people/nicolas.wu/papers/Scope.pdf), whose approach doesn't allow for sensible implementations of effects for continuations, coroutines, or nondeterminism. * `eff` is limited to what's implementable with delimited continuations, which excludes actions such as [`pass`](https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Writer-Class.html#v:pass) from `MonadWriter`, and `async`/`await` style concurrency. `in-other-words` also places restrictions on what effects may be represented -- but in contrast to the libraries mentioned above, these restrictions are almost completely negligable.[1](#f1) This is possible because unlike most other effect systems, `in-other-words` does not attempt to make every possible effect play nicely together with every other effect: instead, just like `mtl`, some effects can't be used together with other effects (depending on how they're interpreted), and this is enforced by constraints that interpreters may introduce. ## Required Language Extensions The following extensions are needed for basic usage of the library: ``` - ConstraintKinds - DataKinds - FlexibleContexts - GADTs - LambdaCase - PolyKinds - RankNTypes - TypeApplications - TypeOperators - TypeFamilies ``` Some features of the library could require enabling more extensions. ## Examples of Simple Usage First-order usage: ```haskell import Control.Effect import Control.Effect.Error import Control.Effect.State import Control.Effect.Reader import Control.Effect.Writer import Text.Read (readMaybe) data Teletype m a where ReadTTY :: Teletype m String WriteTTY :: String -> Teletype m () readTTY :: Eff Teletype m => m String readTTY = send ReadTTY writeTTY :: Eff Teletype m => String -> m () writeTTY str = send (WriteTTY str) challenge :: Eff Teletype m => m () challenge = do writeTTY "What is 3 + 4?" readTTY >>= \str -> case readMaybe @Int str of Just 7 -> writeTTY "Correct!" _ -> writeTTY "Nope." >> challenge -- Interpret a Teletype effect in terms of IO operations teletypeToIO :: Eff (Embed IO) m => SimpleInterpreterFor Teletype m teletypeToIO = interpretSimple $ \case ReadTTY -> embed getLine -- use 'embed' to lift IO actions. WriteTTY msg -> embed $ putStrLn msg -- Make a challenge to the user challengeIO :: IO () challengeIO = runM $ teletypeToIO $ challenge -- Interpret a `Teletype` effect in terms of `Ask` and `Tell` effects runTeletype :: Effs '[Ask String, Tell String] m => SimpleInterpreterFor Teletype m runTeletype = interpretSimple $ \case ReadTTY -> ask WriteTTY msg -> tell msg -- Runs a challenge with the provided inputs purely. challengePure :: [String] -> Either String [String] challengePure testInputs = -- Extract the final result, now that all effects have been interpreted. run -- Run the @Throw String@ effect, resulting in @Either String [String]@ $ runThrow @String -- We discard the return value of @challenge@ -- () -- -- while retaining the list of told strings. $ fmap fst -- Run the @Tell String@ effect by gathering all told -- strings into a list, resulting in ([String], ()) $ runTellList @String -- Run the @State [String]@ effect with initial state -- @testInputs@. @evalState@ discards the end state. $ evalState testInputs -- Interpret the @Ask String@ effect by going through the provided inputs -- one by one. -- Throw an exception if we go through all the inputs without completing the -- challenge. $ runAskActionSimple @String (do get >>= \case [] -> throw "Inputs exhausted!" (x:xs) -> put xs >> return x ) -- Interpret @Teletype@ in terms of @Ask String@ and @Tell String@ $ runTeletype -- Run the main program @challenge@, which returns () $ challenge -- evaluates to True testChallenge :: Bool testChallenge = challengePure ["4","-7", "i dunno", "7"] == Right ["What is 3 + 4?", "Nope." ,"What is 3 + 4?", "Nope." ,"What is 3 + 4?", "Nope." ,"What is 3 + 4?", "Correct!" ] ``` Higher-order usage: ```haskell import Control.Effect import Control.Effect.Bracket import Control.Effect.Trace import GHC.Clock (getMonotonicTime) data ProfileTiming m a where ProfileTiming :: String -> m a -> ProfileTiming m a time :: Eff ProfileTiming m => String -> m a -> m a time label m = send (ProfileTiming label m) -- Interpret a ProfileTiming effect in terms of IO operations, -- 'Trace', and 'Bracket'. profileTimingToIO :: Effs '[Embed IO, Trace, Bracket] m => SimpleInterpreterFor ProfileTiming m profileTimingToIO = interpretSimple $ \case ProfileTiming label action -> do before <- embed getMonotonicTime -- To execute a provided computation when interpreting a -- higher-order effect, just bind it. -- You can also use other higher-order effects to interact with it! a <- action `onError` -- Provided by 'Bracket' trace ("Timing of " ++ label ++ " failed due to some error!") after <- embed getMonotonicTime trace ("Timing of " ++ label ++ ": " ++ show (after - before) ++ " seconds.") return a spin :: Monad m => Integer -> m () spin 0 = pure () spin i = spin (i - 1) profileSpin :: IO () profileSpin = runM $ bracketToIO $ runTracePrinting $ profileTimingToIO $ do time "spin" (spin 1000000) time "spinAndFail" (spin 1000000 >> undefined) {- This prints the following (exact times are machine specific): Timing of spin: 1.3399935999768786 seconds. Timing of spinAndFail failed due to some error! *** Exception: Prelude.undefined -} ``` ## Advanced Usage The examples above are somewhat disingenuous; they cover only the simplest uses of the library. The library has a wide variety of features, and using them properly can get very complicated. Because of this, [`in-other-words` offers a wiki covering more advanced topics of the library.](https://github.com/KingoftheHomeless/in-other-words/wiki/Advanced-Topics) Check it out if you're interested in learning more about the library, or are struggling with a feature. ## Troubleshooting [The wiki has a page for common error messages.](https://github.com/KingoftheHomeless/in-other-words/wiki/Common-Error-Messages-and-Issues) If you run into any issues or strange error messages that you can't figure out from the wiki, feel free to make an issue about it. If not already covered, and if I can generalize the problem enough, then I'll expand the wiki to cover the issue. ## Performance In the microbenchmarks offered by [`effects-zoo`](https://github.com/ocharles/effect-zoo/) `in-other-words` performs comparably to `mtl` and `fused-effects`; at worst up to 2x slower than `fused-effects`. Keep in mind, however, that these *are* only microbenchmarks, and may not predict performance in the wild with perfect accuracy. [The benchmark results are available here.](https://github.com/KingoftheHomeless/in-other-words/wiki/Benchmarks) `in-other-words` is, like `mtl` and `fused-effects`, limited by how effectively the compiler is able to optimize away the underlying abstractions. [As noted by Alexis King](https://github.com/ghc-proposals/ghc-proposals/pull/313#issuecomment-590143835), the ideal situations under which these libraries are truly zero-cost are unrealistic in practice. Although this does adversely affect `in-other-words`, the underlying dispatch cost of effects should be low enough to make to it largely negligable for most purposes -- in particular, IO-bound applications. Further benchmarking, profiling, and optimizations are currently considered future goals of the library. *** [1](#a1) Every effect is required to be *representational* in the carrier monad. This means that if you can represent your effect using: * a `mtl`-style effect class * without any associated type families * and it can be newtype derived then you can also represent your effect with `in-other-words`.