Using $!
does work here, as long as evaluating eval e
to weak head normal form in the meta-language corresponds to evaluating e
to a value in the object language.
Let me demonstrate that by adding a new constructor to the Exp
data type to represent a diverging computation, and a value that we can actually print:
data Exp
= Abs (Exp -> Exp)
| App Exp Exp
| Bot
| Value
printExp :: Exp -> String
printExp Bot = "bottom"
printExp Value = "base value"
printExp (Abs _) = "function"
printExp (App _ _) = "thunk"
For simplicity, let’s just transform it to a call to error
in the evaluation function.
eval :: Exp -> Exp
eval exp = case exp of
Bot -> error "Evaluation would not terminate "
Value -> Value
Abs _ -> exp
App opr opd -> case eval opr of
Abs fun -> eval (fun $ eval opd)
Now we can see if this is call-by-value. The following code should diverge, as we are passing bottom to a function. Alas, it does not:
*Main> let e = App (Abs (\_ -> Value)) Bot
*Main> printExp e
"thunk"
*Main> printExp (eval e)
"base value"
Does it help to change $
to $!
? Yes, it does:
*Main> let e = App (Abs (const Value)) Bot
*Main> printExp (eval e)
"*** Exception: Evaluation would not terminate
But how reliable is that? Any change to the Exp
datatype has to be carefully checked to see if forcing the outermost constructor is what you want, e.g. introduction of pairs.
It might be a bit more reliable to use deepseq
, i.e. $!!
. But what I would really suggest is to add a isValue :: Exp -> Bool
predicate that explicitly checks if the argument is a base value of your object language, and check isValue (eval opd)
in eval
before calling fun
.