5

Suppose I'm creating a simple interpreter that can throw errors, e.g.

type Error = String

data Term = Con Int | Div Term Term

eval :: (MonadError Error m) => Term -> m Int
eval (Con a) = return a
eval (Div u v) = do
  a <- eval u
  b <- eval v
  if b == 0 then
    throwError "Division by zero"
  else
    return $ a `div` b

A typical choice of a concrete error handler, would be Either Error.

runEval :: Term -> Either Error Int
runEval = eval

Suppose now that I want to extend this interpreter to handle non-determinism. For example, I can add a term Choice Term Term that can either evaluate to its first or second argument.

data Term = Con Int | Div Term Term | Choice Term Term

I could then represent a concrete evaluation as [Either Error Int], where each item in the list represents a possible evaluation. However, I'm struggling how I can add the Choice case to my eval function without modifying the Con and Div cases.

What I tried:

eval :: (MonadError Error m, MonadPlus m) => Term -> m Int
-- The Con and Div cases remain unchanged.
eval (Choice u v) = do
  w <- return u `mplus` return v  -- pick either u or v
  eval w

runEval :: Term -> [Either Error Int]
runEval = runExceptT . eval

However, this only returns the first possible outcome, and doesn't backtrack.

> let t = Con 1 `Div` (Choice (Con 0) (Con 1))
> runEval t
[Left "Division by zero"]

What I expected: [Left "Division by zero", Right 1].

What is the right way to combine non-determinism and error-handling?

Safron
  • 802
  • 1
  • 11
  • 23
  • You can take a look at *monad transformers*. This is more or less used to create a "stack" of monad contexts. – Willem Van Onsem Nov 01 '20 at 13:46
  • 2
    @WillemVanOnsem Thank you for the remark. Indeed, the goal would be to combine the error monad and the list monad using a transformer. That's actually what I do in `runEval`: there I use `runExceptT` which I think means that Haskell will effectively use the monad `ExceptT Error [] Int` for my computation. However, as my question explains, it doesn't work as I would expect. – Safron Nov 01 '20 at 13:51
  • Currently, your `eval` does `ExceptT Error [] Int`, but what you want is `ListT (ExceptT Error) Int`, I think. – Artem Pelenitsyn Nov 01 '20 at 14:38
  • 2
    @ArtemPelenitsyn With `ListT (ExceptT Error)` a single error would stop the computation. `runEval` would have a signature like `Term -> Either Error [Int]`. – danidiaz Nov 01 '20 at 14:41

1 Answers1

4

The root of the problem is the MonadPlus instance for ExceptT.

(Monad m, Monoid e) => MonadPlus (ExceptT e m)

It doesn't piggyback on a MonadPlus instance of the base monad m. Instead of that, it requires a Monoid instance from the error e.

And mplus doesn't return the collection of all failures and successes. Instead of that, it returns either the first success or the monoidal combination of all the failures:

ghci> throwError ['a'] `mplus` throwError ['b'] :: Except String ()
ExceptT (Identity (Left "ab"))
ghci> throwError ['a'] `mplus` throwError ['b'] `mplus` return () :: Except String ()
ExceptT (Identity (Right ()))
ghci> return 'a' `mplus` return 'b' :: ExceptT () [] Char
ExceptT [Right 'a']

What we could try is to define our own monad having the MonadPlus instance we want (while reusing all other instances from ExceptT through deriving, to avoid boilerplate).

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Control.Applicative
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Except

newtype OopsT e m a = OopsT { runOopsT :: ExceptT e m a }
    deriving stock (Show)
    deriving newtype (Show,Functor,Applicative,Monad,MonadError e,MonadTrans)

-- We delegate on the Alternative/Monadplus instance of the base monad m
instance MonadPlus m => Alternative (OopsT e m) where
    empty = OopsT (ExceptT empty)       
    OopsT (ExceptT xs) <|> OopsT (ExceptT ys) = OopsT (ExceptT (xs <|> ys)

instance MonadPlus m => MonadPlus (OopsT e m) where
    mzero = empty       
    mplus = (<|>)

runEval :: Term -> [Either Error Int]
runEval = runExceptT . runOopsT . eval

Now it seems to work as intended:

ghci> let t = Con 1 `Div` (Choice (Con 0) (Con 1))
ghci> runEval t
[Left "Division by zero",Right 1]

One potentially troubling aspect of the MonadPlus instance for OopsT is that it doesn't seem to satisfy the v >> mzero = mzero law mentioned in the documentation. For example:

ghci> (mzero :: OopsT Char [] Int)
OopsT {runOopsT = ExceptT []}
ghci> throwError 'c' >> (mzero :: OopsT Char [] Int)
OopsT {runOopsT = ExceptT [Left 'c']}

Perhaps we could use the equivalent Alternative instance instead, which doesn't seem to require that law?

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • Nice explanation. Can I ask: what is the intended full form of `Oops`? – Safron Nov 01 '20 at 15:34
  • @Safron I'm not sure what you mean by "full form". `Oops` is an example of a common pattern: an auxiliary newtype on which we selectively derive or define instances. We have derived a lot of useful instances (from `Functor` to `MonadTrans`) from the underlying type, which saved us work, and had to define two instances by hand (`Alternative` and `MonadPlus`) to suit our needs. – danidiaz Nov 01 '20 at 15:46
  • 1
    Right, I was thinking it was some kind of abbrevation, but now I see that it probably is just meant as a synonym for "something that can fail" aka "oops". My bad :-) – Safron Nov 01 '20 at 15:52
  • 1
    A question: what is the outcome of `runEval $ Div (Div (Con 1) (Con 0)) (Choice (Con 1) (Con 2))`? Is there one or two errors in the result? (A lawful instance should have 2) – Jesper Nov 01 '20 at 22:29
  • @Jesper It returns only one error `[Left "Division by zero"]`. According to what law should it return 2? – danidiaz Nov 02 '20 at 08:24
  • I was working under the assumption of some distribution law `a >> (b <|> c) == (a >> b) <|> (a >> c)`. But now I can't find it in the laws for `MonadPlus`, so maybe I was wrong. – Jesper Nov 02 '20 at 08:38
  • @Jesper No, it doesn't seem to be distributive: `(throwError 'c' :: OopsT Char [] Int) >> (pure 2 <|> pure 3)` is different from `((throwError 'c' :: OopsT Char [] Int) >> pure 2) <|> (throwError 'c' >> pure 3)`. It doesn't satisfy `v >> mzero = mzero` either. Perhaps, instead of defining a not-very-lawful `MonadPlus` instance, the best approach could be to work with a concrete `ExceptT Error []` stack and lift list operations as needed without committing to those laws, as defining a `MonadPlus` instance seems to do. – danidiaz Nov 02 '20 at 09:01
  • Indeed, maybe the most general solution would be to replace the `MonadPlus m` constraint with `Alternative m`, then get rid of the unlawful `MonadPlus` instance and replace ``w <- return u `mplus` return v`` with ``w <- pure u <|> pure v``. – Safron Nov 02 '20 at 09:16
  • 1
    A related SO question https://stackoverflow.com/questions/10167879/distinction-between-typeclasses-monadplus-alternative-and-monoid and an entry from the Haskell Wikibook discussing `Alternative` and `MonadPlus` https://en.wikibooks.org/wiki/Haskell/Alternative_and_MonadPlus – danidiaz Nov 02 '20 at 11:13