Peter Hall
Peter Hall

Reputation: 58725

fail handled in the wrong monad

At least I think that's what's going on.

Main.hs:

module Main (
    main
) where

import Arithmetic
import Data.Maybe
import Data.Either
import Control.Monad.Error

testExpr :: Expr Float
testExpr = 
        (MultExpr "*"
            (AddExpr "XXX"
                (NumExpr 1) 
                (AddExpr "-"
                    (NumExpr 24)
                    (NumExpr 21)
                )
            ) 
            (NumExpr 5) 
        )

main :: IO ()
main = do 
    putStrLn $ case eval testExpr of
            Left msg -> "Error: " ++ msg
            Right result -> show result

Arithmetic.hs:

{-# LANGUAGE GADTs #-}

module Arithmetic  where

type Op = String

data Expr a where
    NumExpr :: Float -> Expr Float
    AddExpr :: Op -> Expr Float -> Expr Float -> Expr Float
    MultExpr :: Op -> Expr Float -> Expr Float -> Expr Float

eval :: (Monad m) => Expr Float -> m Float
eval (NumExpr n) = return n
eval (AddExpr "+" e1 e2) = evalBin (+) e1 e2
eval (AddExpr "-" e1 e2) = evalBin (-) e1 e2
eval (AddExpr "%" e1 e2) = evalBin (%) e1 e2
eval (AddExpr _ _ _ ) = fail "Invalid operator. Expected +, - or %"
eval (MultExpr "*" e1 e2) = evalBin (*) e1 e2
eval (MultExpr "/" e1 e2) = evalBin (/) e1 e2
eval (MultExpr _ _ _ ) = fail "Invalid operator. Expected * or /"

evalBin :: (Monad m) => (Float -> Float -> Float) -> Expr Float -> Expr Float -> m Float
evalBin op e1 e2 = do 
  v1 <- eval e1
  v2 <- eval e2
  return $ op v1 v2

infixl 6 %
(%) :: Float -> Float -> Float
a % b = a - b * (fromIntegral $ floor (a / b))

But, when eval fails, I get an error in IO, without the "Error: " string appended.

Upvotes: 1

Views: 193

Answers (2)

ehird
ehird

Reputation: 40787

Ah, I see the problem now!

You are importing Control.Monad.Error, but using the Either monad, whose fail definition calls error rather than returning Left.

What you need to do is change eval testExpr to runIdentity . runErrorT $ eval testExpr. You'll need to import Data.Functor.Identity.

In an old version of the mtl (monad transformer library), Either's fail method did indeed return Left. However, the problem is that this only allowed Either e to be a monad when e was an instance of the Error class. I believe this was considered especially undesirable because fail is generally thought to be a mistake; many people think that it should be moved out of the Monad typeclass.

You could of course opt for a different method of error handling entirely, but this is the closest analogue to what you already have that works with the newest versions of the libraries.

I would suggest you specialise your code in the Arithmetic module to use ErrorT and throwError directly; as a bonus, this will also let you catch the errors you throw within your interpreter.

You could also define your own error type, and in that case I suggest defining your own monad that uses Either:

newtype Eval a = Eval { runEval :: Either EvalError a }
  deriving (Functor, Applicative, Monad)

evalError :: EvalError -> Eval a
evalError e = Eval (Left e)

Either's monad instance will work just fine here; the only thing that has changed is its definition of fail. Note that you'll need the GeneralizedNewtypeDeriving extension to derive those instances.

You could of course use String instead of EvalError here, but that offers no benefits over a simple ErrorT; the advantage with using your own monad with a custom error type is that you don't have to define an instance of Error, which would require defining a "catch-all" error value for noMsg/strMsg.

Upvotes: 1

dave4420
dave4420

Reputation: 47052

Which version of base are you using? fail is no longer defined to return a Left in the latest version of the Either e monad, instead it uses the default definition (which calls error, which throws an exception that can only be caught in IO).

I don't know why this changed.

Upvotes: 3

Related Questions