Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Resources on managing, phasing, composing monads (in Scala or Haskell)

I am looking for resources discussing good practice for composing monads. My most pressing problem is that I'm writing a system that is making use of a series of state monads over different state types, and it seems as if the best way to handle this situation is to just create one big product type (perhaps prettied up as a record/class) encompassing all the components I'm interested in even though phase 1 is not interested in component B, and phase 2 is only interested in component A.1.

I'd appreciate pointers to nice discussions of alternatives when writing code in this sort of area. My own code-base is in Scala, but I'm happy to read discussions about the same issues in Haskell.

like image 216
Michael Norrish Avatar asked Feb 13 '23 15:02

Michael Norrish


1 Answers

It's a bit challenging to use stacks of StateT because it becomes confusing as to which layer you're talking to when you write get or put. If you use an explicit stack in transformers style then you have to use a bunch of lifts and if you use the class-based method of mtl you get stuck entirely.

-- using transformers explicit stack style
type Z a = StateT Int (StateT String IO) a

go :: Z ()
go = do int <- get
        str <- lift get
        replicateM int (liftIO $ putStrLn str)

We might want to avoid that mess with an explicit product type of states. Since we end up with functions from our product state to each individual component it's easy to get into those individual components using gets

data ZState = ZState { int :: Int, str :: String }
type Z a = StateT ZState IO a

go :: Z ()
go = do i <- gets int
        s <- gets str
        replicateM i (liftIO $ putStrLn s)

But this might be considered ugly still for two reasons: (1) put and modification in general doesn't have anywhere nearly as nice a story and (2) we can't easily look at the type of a function which only impacts, say, the int state and know that it doesn't touch str. We'd much prefer to maintain that type-ensured modularity.

If you're lens-savvy there's a solution called zoom

-- the real type is MUCH more general
zoom :: Lens' mother child -> StateT child m a -> StateT mother m a 

which "lifts" a stateful computation on a subpart of some larger state space up to the entire state space. Or, pragmatically, we use it like this:

data ZState = ZState { _int :: Int, _str :: String }
makeLenses ''ZState

type Z = StateT ZState IO a

inc :: MonadState Int m => m ()
inc = modify (+1)

yell :: MonadState String m => m ()
yell = modify (map toUpper)

go :: Z ()
go = do zoom int $ do inc
                      inc
                      inc
        zoom str yell
        i <- use int
        s <- use str
        replicateM i (liftIO $ putStrLn s)

And now most of the problems should have vanished—we can zoom in to isolate stateful operations which only depend upon a subset of the total state like inc and yell and determine their isolation in their type. We also can still get inner state components using use.

More than this, zoom can be used to zoom in on state buried deep inside various tranformer stacks. The fully general type works just fine in a situation like this

type Z a = EitherT String (ListT (StateT ZState IO)) a

>>> :t zoom int :: EitherT String (ListT (StateT Int IO)) a -> Z a
zoom int :: EitherT String (ListT (StateT Int IO)) a -> Z a

But while this is really nice, the fully general zoom requires some heavy trickery and you can only zoom over some transformer layers. (It's today heavily unclear to me how you add that functionality to your own layer, though presumably it's possible.)

like image 71
J. Abrahamson Avatar answered Apr 30 '23 10:04

J. Abrahamson