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 ExceptT monad transformer in the daml-ctl library

  • I used it similarly to simplify error handling for the TriggerA monad. 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!

1 Like

Thank you @Matteo_Limberto. That’s extremely helpful!

Kudos to @Luciano who taught me all of this stuff!

1 Like