The wrapper type I was talking about would be defined and used like this:
module Main where
import Prelude
import Control.Monad.Reader.Class (class MonadAsk, ask)
import Control.Monad.State (State, runState)
import Control.Monad.State.Trans (get, modify_)
import Control.Monad.Writer.Trans (class MonadTell, tell)
import Data.Array (length)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Console (logShow)
-- Define a generic wrapper around State that will provide separate
-- MonadAsk and MonadTell interfaces
newtype SeparableState s a = SeparableState (State s a)
-- A bunch of boilerplate to establish that SeparableState is a monad
-- because State is
derive newtype instance Functor (SeparableState s)
derive newtype instance Apply (SeparableState s)
derive newtype instance Applicative (SeparableState s)
derive newtype instance Bind (SeparableState s)
derive newtype instance Monad (SeparableState s)
-- The implementation of MonadAsk delegates to get
instance MonadAsk s (SeparableState s) where
ask = SeparableState get
-- The implementation of MonadTell delegates to modify_
instance Semigroup s => MonadTell s (SeparableState s) where
tell = SeparableState <<< modify_ <<< flip append
-- Every practical monad needs a way to run it
runSeparableState :: forall s a. SeparableState s a -> s -> Tuple a s
runSeparableState (SeparableState inner) = runState inner
-- Now you have your read-only function...
canOnlyRead :: forall m. MonadAsk (Array String) m => m Int
canOnlyRead = do
s <- ask
-- tell doesn't work here
pure $ length s
-- ...and your write-only function
canOnlyWrite :: forall m. MonadTell (Array String) m => String -> m Unit
canOnlyWrite msg = do
tell ["Hello there"]
tell ["I was given:", msg]
-- ask doesn't work here
program :: SeparableState (Array String) Unit
program = do
len <- canOnlyRead
canOnlyWrite (show len)
main :: Effect Unit
main = do
let Tuple _ s = runSeparableState program []
logShow s
However, this approach wouldn’t work for the toy example you posted. To use both canOnlyRead
and canOnlyWrite
in program
, they need to read and write to the same type (Array String
). Your example tracks two different types of state, which the MonadAsk
/MonadTell
pattern isn’t flexible enough to differentiate between. If you tried that by putting both your state variable and your log variable inside a record, you would have a function that only writes both types and a function that only reads both types. But your read
example wants to read from one side and write to the other. This is a great example of why I said that application-specific classes are likely a better choice then the generic MonadAsk
and MonadTell
interfaces for even moderately complex application logic.
To explain another way: in OOP, you might be inclined to solve this problem by passing around two objects—a logger that might implement Writable<Log>
, and a state manager that might implement both Writable<String>
and Readable<String>
. In FP, you’re only programming in one monad at a time. We have different tricks to make that one monad flexible enough to handle multiple logical domains, but most of them boil down to the OOP equivalent of having a single object that implements every interface—if you have one interface that you want to implement two different ways, it doesn’t work. So the usual approach is to define distinct interfaces instead.
Here’s how I’d design your toy example. I’m not using the generic SeparableState
wrapper anymore; I’m using an application-specific monad type and application-specific classes that are instantiated for that type. But you’ll still notice similarities between this example and the SeparableState
example—namely, how ‘most’ of the program (read
and write
, which in this example is actually a very small fraction of the program but stands in for much larger units) is kept in the dark about what the application-specific monad is exactly, and is only told specific things about what that monad can do.
module Main where
import Prelude
import Control.Monad.Reader.Trans (ReaderT, ask, runReaderT)
import Control.Monad.State (put)
import Control.Monad.State.Trans (StateT, get, runStateT)
import Control.Monad.ST (ST)
import Control.Monad.ST.Ref (STRef)
import Control.Monad.ST.Ref as STRef
import Control.Monad.ST.Global (Global, toEffect)
import Control.Monad.Writer (Writer, tell, runWriter)
import Control.Monad.Trans.Class (lift)
import Data.Array.ST (STArray)
import Data.Array.ST as STArray
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Console (logShow)
class Monad m <= MonadAppLog m where
appLog :: String -> m Unit
class Monad m <= MonadAppStateRead m where
getAppState :: m String
class Monad m <= MonadAppStateWrite m where
putAppState :: String -> m Unit
-- Implementation 1:
newtype ApplicationState a = ApplicationState (StateT String (Writer (Array String)) a)
instance MonadAppLog ApplicationState where
appLog msg = ApplicationState (tell [msg])
instance MonadAppStateRead ApplicationState where
getAppState = ApplicationState get
instance MonadAppStateWrite ApplicationState where
putAppState = ApplicationState <<< put
runApplicationState :: forall a. ApplicationState a -> { finalState :: String, log :: Array String, result :: a }
runApplicationState (ApplicationState st) = { finalState, log, result }
where
Tuple (Tuple result finalState) log = runWriter (runStateT st "foo")
-- Implementation 2:
{-
newtype ApplicationState a = ApplicationState
(ReaderT { stateRef :: STRef Global String, logRef :: STArray Global String }
(ST Global) a)
instance MonadAppLog ApplicationState where
appLog msg = ApplicationState do
{ logRef } <- ask
lift $ void $ STArray.push msg logRef
instance MonadAppStateRead ApplicationState where
getAppState = ApplicationState do
{ stateRef } <- ask
lift $ STRef.read stateRef
instance MonadAppStateWrite ApplicationState where
putAppState st = ApplicationState do
{ stateRef } <- ask
lift $ void $ STRef.write st stateRef
-- Note that this type changed because everything is happening in Effect.
-- You could get around this by parameterizing ApplicationState by a non-Global
-- region, but this would mean changing more types.
runApplicationState :: forall a. ApplicationState a -> Effect { finalState :: String, log :: Array String, result :: a }
runApplicationState (ApplicationState st) = toEffect do
stateRef <- STRef.new "foo"
logRef <- STArray.new
result <- runReaderT st { stateRef, logRef }
finalState <- STRef.read stateRef
log <- STArray.freeze logRef
pure { finalState, log, result }
-}
derive newtype instance Functor ApplicationState
derive newtype instance Apply ApplicationState
derive newtype instance Applicative ApplicationState
derive newtype instance Bind ApplicationState
derive newtype instance Monad ApplicationState
read :: forall m. MonadAppLog m => MonadAppStateRead m => m Unit
read = do
s <- getAppState
appLog $ "Current state is " <> s
write :: forall m. MonadAppLog m => MonadAppStateWrite m => m Unit
write = do
appLog "Put new state bar"
putAppState "bar"
program :: ApplicationState Unit
program = do
read
write
read
main :: Effect Unit
main = do
let { log } = runApplicationState program
-- or, if runApplicationState returns an Effect:
-- { log } <- runApplicationState program
logShow log