polysemy-research / polysemy

:gemini: higher-order, no-boilerplate monads
BSD 3-Clause "New" or "Revised" License
1.04k stars 73 forks source link

More noob-friendly documentation #234

Open sir4ur0n opened 5 years ago

sir4ur0n commented 5 years ago

In the same spirit as #232 but for the other side of the spectrum: I feel like the documentation (particularly the main readme displayed on Hackage/Stackage, but also in each module doc) could be improved, and help new users onboarding in Polysemy.

We just started using Polysemy in my team and we have some feedback I propose to gather here as we progress on the Polysemy scale. Each point could become a dedicated issue/PR if you agree they should be worked on (we may be able to help!).

-- | Here the EnvironmentVariables effect contains a single action named ReadEnvVar, describing reading environment variables. It takes the environment variable name as String to read, and returns a Maybe String of the value (in case it is not defined) data EnvironmentVariables m a where ReadEnvVar :: String -> EnvironmentVariables m (Maybe String)

-- | Provides the readEnvVar action without us having to write boilerplate makeSem ''EnvironmentVariables

> - how should this effect be interpreted (conversion to pure value, conversion to IO, reinterpretation in terms of other effects, etc.)
```haskell
import           Polysemy
import           System.Environment (lookupEnv)

-- | The `Member (Embed IO) r` constraint ensures we are allowed to do IO (embedded in an effect) in the nterpreter implementation
-- The `Sem (EnvironmentVariables ': r) a -> Sem r a` type reads as "Give me a Polysemy monad which contains at least the EnvironmentVariables effect (and potentially others), and I will return a Polysemy monad without it, i.e. I have consumed this effect"
environmentVariablesToIO :: Member (Embed IO) r => Sem (EnvironmentVariables ': r) a -> Sem r a
environmentVariablesToIO =
  interpret $ \case
    -- We are allowed to use `lookupEnv envVariableName` thanks to the `Member IO` constraint. However to stick to our Polysemy monad, we need to wrap it back thanks to `embed`
    ReadEnvVar envVariableName -> embed $ lookupEnv envVariableName
  • use these effects in your business code!
    
    import           Polysemy

-- | The Member EnvironmentVariables r constraint means we are allowed to use the EnvironmentVariables effect, i.e. the readEnvVar function myBusinessFunction :: Member EnvironmentVariables r => Sem r String myBusinessFunction = do -- We use the EnvironmentVariables effect action readEnvVar maybeFoo <- readEnvVar "foo" -- Do whatever you want with this value now! return $ fromMaybe "default foo value" maybeFoo

-- | At some point you want to get out of the Polysemy/effect context, be it in your main, in some intermediary business function, in your tests, etc. -- This is where you use interpreters! They do exactly that: interpret effects to get the real stuff out of it myOtherCode :: IO String myOtherCode = runM . environmentVariablesToIO $ myBusinessFunction

* Test example: as we see it, one of the great benefits of Polysemy is simpler testing, by writing another interpreter for your tests. Maybe this should be stressed (and an example provided?) in the readme.
Example/proposal:
> Polysemy also eases code testability thanks to the effect declaration / effect interpretation separation. Your main code uses declared effects rather than concrete implementation, allowing you to write a different behavior in your tests (see: "mock").
> Reusing the EnvironmentVariables example above:
```haskell
import           Polysemy
import           Test.Tasty.HUnit
import           Test.Tasty.QuickCheck

-- | Another interpreter for the EnvironmentVariables effect. We don't want to toy around with real environment variables in our test, instead we can "mock" them
-- Here, we can mock the `foo` environment value with anything, and all other environment variables will seem as unset in our tests
mockEnvironmentVariables :: Maybe String -> Sem (EnvironmentVariables ': r) a -> Sem r a
mockEnvironmentVariables value =
  interpret $ \case
    ReadEnvVar "foo" -> pure value
    ReadEnvVar _ -> pure Nothing

-- | A unit test benefiting from the mock. Note the test is pure, there's no IO involved!
test_unsetFoo = 
  testCase "When foo env var is unset, then we return the default value" $ 
  -- Note we use `run` here because there are no other effect to interpret, so we can manipulate it as a pure value
  let result = run . mockEnvironmentVariables Nothing $ myBusinessFunction
   in result @?= "default foo value"

-- | A property based test also benefiting from the mock. This test is also pure ;)
test_setFoo = 
  testProperty "When foo env var is set, then we return its value" $ \envVarValue ->
    let result = run . mockEnvironmentVariables (Just envVarValue) $ myBusinessFunction
     in result === envVarValue

All that being said, so far we are falling in love with Polysemy, thank you for this library! I can't wait to see what it enables us to build in the coming months.

Cheers!

isovector commented 5 years ago

Thanks for filing this! Sounds like we should do a documentation milestone.

sir4ur0n commented 5 years ago

For what it's worth: I recently started a blog and my first 2 posts (and probably a few more in the coming months) are about Introduction to Polysemy:

I feel like some (or all if you wish) content of these posts could be reused in the Haddock of Polysemy.

Incidentally, if you like the blog post, feel free to refer to it in Polysemy documentation :sweat_smile:

isovector commented 5 years ago

@Sir4ur0n awesome, thanks! I'll have a look! I'm more than happy to merge any PRs that you think would improve documentation in the library's haddocks directly!

sir4ur0n commented 4 years ago

I tried to build Polysemy (using Stack) on my Windows machine but it gets stuck every single time on compiling Polysemy.Final:

$ stack build
polysemy       > configure (lib)
polysemy       > Configuring polysemy-1.2.3.0...
polysemy       > build (lib)
polysemy       > Preprocessing library for polysemy-1.2.3.0..
polysemy       > Building library for polysemy-1.2.3.0..
polysemy       > [ 1 of 41] Compiling Polysemy.Embed.Type
polysemy       > [ 2 of 41] Compiling Polysemy.Fail.Type
polysemy       > [ 3 of 41] Compiling Polysemy.Internal.CustomErrors.Redefined
polysemy       > [ 4 of 41] Compiling Polysemy.Internal.Fixpoint
polysemy       > [ 5 of 41] Compiling Polysemy.Internal.Kind
polysemy       > [ 6 of 41] Compiling Polysemy.Internal.CustomErrors
polysemy       > [ 7 of 41] Compiling Polysemy.Internal.NonDet
polysemy       > [ 8 of 41] Compiling Polysemy.Internal.PluginLookup
polysemy       > [ 9 of 41] Compiling Polysemy.Internal.Union
polysemy       > [10 of 41] Compiling Polysemy.Internal
polysemy       > [11 of 41] Compiling Polysemy.Internal.Tactics
polysemy       > [12 of 41] Compiling Polysemy.Internal.TH.Common
polysemy       > [13 of 41] Compiling Polysemy.Internal.TH.Effect
polysemy       > [14 of 41] Compiling Polysemy.Internal.Forklift
polysemy       > [15 of 41] Compiling Polysemy.Internal.Combinators
polysemy       > [16 of 41] Compiling Polysemy.Internal.Strategy
polysemy       > [17 of 41] Compiling Polysemy.Final

I know I technically don't need to compile to write doc, but I like to ensure doc is well-formatted.

Am I alone with this issue? šŸ¤”

KingoftheHomeless commented 4 years ago

Template Haskell is bugged on Windows with GHC 8.6.3, and often hangs the compiler when used. Modify stack.yaml to use a newer resolver than lts-13.0 (like lts-14.16, which is the most recent LTS), so you're using GHC 8.6.5+, and you should be good. This is actually what I personally do when working on this repo.

sir4ur0n commented 4 years ago

Well, now I feel silly, sorry for bothering you šŸ˜… Is there any reason why the Stack resolver on master isn't more recent than 13.0?

Alternatively, maybe the README should explain how to build/test, and mention this particularity for Windows users? I did not find any such piece of information.

Either way, thank you @KingoftheHomeless , your fix did the trick! I'll get back to (try to) hack Polysemy documentation.

KingoftheHomeless commented 4 years ago

@isovector You've wanted to stick with 13.0 in the past. Is there any particular reason why? Otherwise I'd like to upgrade it.

isovector commented 4 years ago

@KingoftheHomeless you're the boss now!

sshine commented 4 years ago

@KingoftheHomeless: I think you should upgrade it then!

KingoftheHomeless commented 4 years ago

I have.