Can I use monad transformers inside Daml choices
To simplify error handling without littering the code with arrowheads like
nonconsuming choice Foo : Either Text ()
controller Bar
do
case foo of
Left err -> pure $ Left err
Right x ->
-- do something
case bar of
Left err -> pure $ Left err
Right y ->
-- do something else
I have created an EitherT monad transformer to hide some of the clutter with the Left branches.
newtype EitherT m a b = EitherT { runEitherT : m (Either a b) }
instance Functor m => Functor (EitherT m a) where
fmap f = EitherT . fmap (fmap f) . runEitherT
instance Action m => Applicative (EitherT m a) where
pure = EitherT . pure . pure
EitherT meF <*> EitherT meX = EitherT $ do
eF <- meF
case eF of
Left err -> return (Left err)
Right f -> do
eX <- meX
case eX of
Left err -> return (Left err)
Right x -> return $ Right (f x)
instance Action m => Action (EitherT m a) where
EitherT meX >>= f = EitherT $ do
eX <- meX
case eX of
Left err -> return (Left err)
Right x -> runEitherT (f x)
However, this doesn’t seem to work in choices as this gives me a compile error:
nonconsuming choice Foo : Either Text ()
controller Bar
do
let lookupOptUpdate = lookupByKey @MyTemplate myKey
lookupEitherUpdate = fmap (optionalToEither "some text") lookupOptUpdate
lookupEitherT = EitherT lookupEitherUpdate
lookup <- lookupEitherT -- Compile error
With the message:
Couldn't match type ‘EitherT Update Text’ with ‘Update’
Expected type: Update (ContractId MyTemplate)
Actual type: EitherT Update Text (ContractId MyTemplate)
Am I doing something wrong?
Hi @rexng.
Your binding lookupEitherT is of type EitherT Update Text (ContractId MyTemplate). However, a choice implementation is a computation within an Update monad. Hence, when you are using the <- operator, the object you are unwrapping must be of type Update ... .
In order to make it work, you first need to expose the Update by using runEitherT.
Two additional pointers for you:
-
you can find an implementation of the
ExceptTmonad transformer in the daml-ctl library -
I used it similarly to simplify error handling for the
TriggerAmonad. You can have a look at the code here
In my example, I write all logic within the newly defined TriggerE monad, where type TriggerE s = ExceptT Text (TriggerA s). Only at the very end, I unpack the result using
-- | Runs a TriggerE rule, using `debug` to print the exception
runAndDebug : (Party -> TriggerE s ()) -> Party -> TriggerA s ()
runAndDebug rule p =
runExceptT (rule p) >>= f
where
f (Left msg) = debug msg
f (Right ()) = pure ()
In your case you probably want to use abort instead of debug.
Hope this helps!
Thank you @Matteo_Limberto. That’s extremely helpful!
Kudos to @Luciano who taught me all of this stuff!