Description
Control operators have two kinds of arguments: function arguments (which are arrows) and arrow arguments. For an example, consider a function like local
:
local :: ArrowReader r arr => arr e a -> arr (e, r) a
Like local
from MonadReader
, this runs the argument in a modified environment (though unlike the MonadReader
version, it accepts the environment directly instead of being given a function to apply to it). Using it in arrow notation looks like this:
y <- (| local (f -< x) |) r
This works okay when the argument to local
is small, but it becomes very confusing when the argument command is large. For example, I might want to run a whole block with a modified environment, so I would have to write something like this:
r <- ask -< ()
w <- (| local (do
y <- f -< x
z <- g -< y
h -< (y, z))
|) (foo $ bar r)
I think this looks totally backwards. Very often, when writing monadic code, I do something like this:
w <- flip local (foo . bar) $ do
y <- f x
z <- g y
h y z
But there isn’t any way to write code that way in arrow notation. This means that if I have a series of nested control operators, I end up with something like
(| withRecordInconsistency
((| withRecordDependencies
((| mapErrorA (f -< (e, s))
|) \e -> "in permission for role " <> roleName <<> ": " <> e)
|) metadataObject schemaObject)
|) metadataObject
which looks terrible and is hard to read. The equivalent monadic code it was adapted from looks like
withRecordInconsistency metadataObject $
withRecordDependencies metadataObject schemaObject $
modifyErr (\e -> "in permission for role " <> rn <<> ": " <> e) $
m
which is far better.
Activity