02-ReaderT.purs

module Examples.HelloWorld.ReaderT where

import Prelude
import Effect (Effect)
import Effect.Console as Console
import Effect.Class (class MonadEffect, liftEffect)
import Control.Monad.Reader.Trans (ReaderT, runReaderT)

-----------------------------------------
-- Core: Define any domain-specific concepts and their rules/relationships to
--         other domain-specific concepts

-- since there are no domain concepts or rules/relationships,
--    we won't include anything here...

-----------------------------------------
-- Domain: define business logic and capabilities need to run it:

-- - define our business logic as one pure function (program)
--      that uses type class constraints to define the effects our
--      program requires to be run
program :: forall m.                -- for any monad supporting these capabilities/effects--
           LogToScreen m =>         --   one of which is logging a message
           m Unit                   -- --running this monad will produce no output.
                                    -- However, running it will produce side-effects
                                    -- that make running this code useful
program = do
  -- use capability to log a message to the console
  logToScreen "Hello World!"

-- - declare what those capabilities are as type classes that require monadic types.
class (Monad m) <= LogToScreen m where
  logToScreen :: String -> m Unit

-----------------------------------------
-- API: define how the pure domain concepts and logic above translate
--        down into pure effects and impure effects via a `ReaderT`-based monad

-- - a newtyped ReaderT monad called "AppM" that hard-codes the Environent
--      type ("Unit" in this case because there is no global config value)
--      and the base monad (Effect in this case)
newtype AppM a = AppM (ReaderT Unit Effect a)

-- - a 'runAppM' function that unwraps the AppM newtype and runs the program
--      in the `Effect` monad, which is transformed/augmented by the ReaderT function
--      Since we don't have any global config being passed into our program,
--        we just pass in "unit"
runAppM :: AppM ~> Effect
runAppM (AppM m) = runReaderT m unit

-- - Since there is no global configuration value for this program
--      we do not need AppM to have an instance for MonadAsk
-- derive newtype instance MonadAsk e AppM

-- - write instances for capabilities above, so that AppM can use them
instance LogToScreen AppM where
  logToScreen :: String -> AppM Unit
  logToScreen message = liftEffect $ Console.log message


-- - derive instances for AppM, so that it is a Monad via ReaderT
derive newtype instance Functor AppM
derive newtype instance Applicative AppM
derive newtype instance Apply AppM
derive newtype instance Bind AppM
derive newtype instance Monad AppM

-- - enable functions that return `Effect a` to be run inside our `AppM` program
--      such as Effect.Console.log
derive newtype instance MonadEffect AppM

-----------------------------------------
-- Infrastructure: any other code (i.e. databases, frameworks, libraries)
--                   that provides effects that do not appear in `Effect.*` modules

-- We aren't using other libraries (Node.ReadLine, Halogen, etc.).
-- Thus, nothing needs to go here for right now

-----------------------------------------
-- Machine Code: set up everything we need and then run the program

main :: Effect Unit
main = do
  -- no global config to set up here

  -- run the program by passing the domain logic into the `runAppM` function
  runAppM program