03-Free.purs

module Examples.NumberComparison.Free where

import Prelude
import Effect (Effect)
import Effect.Console as Console
import Effect.Random (randomInt)
import Control.Monad.Free (Free, liftF, foldFree)
import Data.Functor.Coproduct.Nested (Coproduct3, coproduct3)
import Data.Functor.Coproduct.Inject (inj)

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

-- define any domain-specif concepts
newtype HardCodedInt = HardCodedInt Int

-- and their rules and relationships to other concepts via
--  - functions
--  - type classes

-- since there are no 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 data types that have Functor instances
--      to define the effects our program requires to be run
program :: Free                       -- A program that...
            AllEffects                -- ... given an interpreter for each one of its 'effects'...
            Unit                      -- ... will produce no output.
                                      -- However, running it will produce side-effects
                                      -- that make this program useful
program = do
  -- use capability to generate random int
  randomInt <- generateRandomInt

  -- use Reader effect to get value
  (HardCodedInt hardInt) <- asks \envRecord -> envRecord.hardCodedInt

  -- use "let" syntax to do some intermediary pure computations:
  -- 1. do the comparison and convert the result into something more readable
  let comparisonResult = case compare hardInt randomInt of
          LT -> " < "
          GT -> " > "
          EQ -> " = "

  -- 2. create a message that clearly shows what the output will be
  let message = show hardInt <> comparisonResult <> show randomInt

  -- use capability to log comparison to console
  logToScreen message

-- - define a type alias that defines what all our effects/capabilities are
type AllEffects =
  Coproduct3              -- in total, we have 3 effects/capabilities
    LogToScreen           -- one of which enables logging to the screen
    GenerateRandomInt     -- one of which enables generating a random int
    (Reader Environment)  -- one which can get values/functions from the
                          --    global configuration type, Environment

-- - define an `Environment` record type alias that includes all things that are
--      available globally at all times. Note: This is only necessary because we're
--      using a ReaderT-like approach.
type Environment = { hardCodedInt :: HardCodedInt }

-- - declare what the capabilities/effects are as data types with
--     derived instances for `Functor` and write their smart constructors.
data LogToScreen a = LogToScreen String a
derive instance Functor LogToScreen

logToScreen :: String -> Free AllEffects Unit
logToScreen message = liftF $ inj $ LogToScreen message unit
-----
data GenerateRandomInt a = GenerateRandomInt (Int -> a)
derive instance Functor GenerateRandomInt

generateRandomInt :: Free AllEffects Int
generateRandomInt = liftF $ inj $ GenerateRandomInt identity
-----
newtype Reader e a = Reader (e -> a)
derive instance Functor (Reader e)

ask :: Free AllEffects Environment
ask = liftF $ inj $ Reader identity

asks :: forall a. (Environment -> a) -> Free AllEffects a
asks envToValue = liftF $ inj $ Reader envToValue

-----------------------------------------
-- API: define how the pure domain concepts and logic above translate
--        into pure effects and impure effects via
--        "data type interpreters" (i.e. F-Algebras)

-- - an "interpreter" from each data type used above to the base monad, Effect
--    via a Natural Transformation
logToScreenToEffect :: LogToScreen ~> Effect
logToScreenToEffect (LogToScreen msg next) = do
  Console.log msg
  pure next

generateRandomIntToEffect :: GenerateRandomInt ~> Effect
generateRandomIntToEffect (GenerateRandomInt reply) = do
  random <- randomInt bottom top
  pure $ reply random

-- Any values or functions that are needed by Reader are passed in
-- from the outside (i.e. `envRecord`)
readerToEffect :: Environment -> Reader Environment ~> Effect
readerToEffect environment (Reader reply) = do
  pure $ reply environment

-- - an interpreter from the (Free AllEffects) type to the base monad, Effect,
--      using `foldFree` and the above interprters.
--      Any values or functions that are needed by Reader are passed in
--      from the outside (i.e. `envRecord`)
runProgram :: Environment -> Free AllEffects ~> Effect
runProgram envRecord = foldFree go

  where
  -- - route each data type in the containing Coproduct to its correct
  --    interpreter using the convenience function, `coproduct3`
  go :: AllEffects ~> Effect
  go =
    coproduct3
      logToScreenToEffect
      generateRandomIntToEffect
      (readerToEffect envRecord)

-----------------------------------------
-- 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
  -- set up the environment
  let environment = { hardCodedInt: HardCodedInt 4 }

  -- run the program by passing in the environment and the final data type
  -- that includes all the instructions to run via their interpreters.
  runProgram environment program