语言设计:混淆为什么分配的变量 returns 在算术表达式中是错误的答案

Language Design: Confusion as to why an assigned variable returns the wrong answer in arithmetic expressions

编辑:为澄清起见,所有 integer op integer 形式的算术表达式都可以正常工作,但 variable op integer 会导致令人困惑的结果。

在我编写的简单语言中,我正处于尝试实现用户定义变量的阶段。我正在按照 'Write Yourself a Scheme in 48 Hours' 指南构建我的开发过程。我已经成功地实现了变量实例化,这样我的表达式 x:=4 的行为就像 WYS48 的 (define x 4) 一样,因为它们都是 return 4.

然而,当我尝试在算术表达式中使用 x 时,我得到了一个不寻常的结果。

Henry > x:=4
4
Henry > <x+4>
76

起初我以为它是将 x 的 ASCII 值加到 4,但事实并非如此,因为 x 的 ASCII 值是 120 而它的十六进制是 78,所以我知道不是这样。但是,我看不出程序中的错误在哪里。我怀疑它可能在我的函数 str2Int 中,主要是因为它减去 48。下面是我用来评估算术、表达式和函数的代码,我用来评估我的 x。当表达式为 x:=0 时减去 71 似乎适用于这种情况算术对于 x 的实例化很好,但它不是一个特别最佳的修复。

eval :: Env -> HenryVal -> IOThrowsError HenryVal
eval env val@(Atom _) = return val
eval env val@(ABinOp _) = return val
eval env (Assign var val) = eval env val >>= defineVar env var
eval env (ABinary op x y) = return $ evalABinOp env x op y

evalABinOp :: Env -> HenryVal -> ABinOp -> HenryVal -> HenryVal
evalABinOp env (Atom a) Add (Integer b) = Integer ((toInteger (str2Int'(str2HenryStr (a)))) + b )

str2Int' :: HenryVal -> Integer
str2Int' n = toInteger $ (ord (show n !! 1)) - 48

str2HenryStr :: String -> HenryVal
str2HenryStr s = String $ s

我不确定它是否相关,但下面是我用来实现变量赋值的代码

type Env = IORef [(String, IORef HenryVal)] 
type ThrowsError = Either HenryError
type IOThrowsError = ExceptT HenryError IO

nullEnv :: IO Env
nullEnv = newIORef []

liftThrows :: ThrowsError a -> IOThrowsError a
liftThrows (Left err) = throwError err
liftThrows (Right val) = return val

runIOThrows :: IOThrowsError String -> IO String
runIOThrows action = runExceptT (trapError action) >>= return . extractValue

isBound :: Env -> String -> IO Bool
isBound envRef var = readIORef envRef >>= return . maybe False (const True) . lookup var

getVar :: Env -> String -> IOThrowsError HenryVal
getVar envRef var  =  do env <- liftIO $ readIORef envRef
                         maybe (throwError $ UnboundVar "Getting an unbound variable" var)
                               (liftIO . readIORef)
                               (lookup var env)

setVar :: Env -> String -> HenryVal -> IOThrowsError HenryVal
setVar envRef var value = do env <- liftIO $ readIORef envRef
                             maybe (throwError $ UnboundVar "Setting an unbound variable" var)
                                   (liftIO . (flip writeIORef value))
                                   (lookup var env)
                             return value

defineVar :: Env -> String -> HenryVal -> IOThrowsError HenryVal
defineVar envRef var value = do
     alreadyDefined <- liftIO $ isBound envRef var
     if alreadyDefined
        then setVar envRef var value >> return value
        else liftIO $ do
             valueRef <- newIORef value
             env <- readIORef envRef
             writeIORef envRef ((var, valueRef) : env)
             return value

bindVars :: Env -> [(String, HenryVal)] -> IO Env
bindVars envRef bindings = readIORef envRef >>= extendEnv bindings >>= newIORef
     where extendEnv bindings env = liftM (++ env) (mapM addBinding bindings)
           addBinding (var, value) = do ref <- newIORef value
                                        return (var, ref)

除非您有 eval 的任何其他实现,否则任何变量都不会被计算为二元运算的值(赋值除外)。让我们看看 evalevalABinOp:

eval :: Env -> HenryVal -> IOThrowsError HenryVal
eval env val@(Atom _)     = return val
eval env val@(ABinOp _)   = return val
eval env (Assign var val) = eval env val >>= defineVar env var  -- defineVar returns val
eval env (ABinary op x y) = return $ evalABinOp env x op y      -- evalABinOp uses env?

evalABinOp :: Env -> HenryVal -> ABinOp -> HenryVal -> HenryVal
evalABinOp env (Atom a) Add (Integer b) = Integer ((toInteger (str2Int'(str2HenryStr (a)))) + b )
                                          -- env is not used on the right hand side!

由于您从不在右侧使用 env,我们可以 100% 确定 Atom a 不能解释为变量,而必须解释为数字或字符串 (或作为未定义的变量)。在您的代码中,您实际上没有在当前环境中查找 a 的值。相反,您将 x 转换为整数:

str2Int' :: HenryVal -> Integer
str2Int' n = toInteger $ (ord (show n !! 1)) - 48

str2HenryStr :: String -> HenryVal
str2HenryStr s = String $ s

我手边既没有 HenryVal 的定义,也没有它的 Show 实例,所以我只能假设 show (String "x") 导致 "\"x\"",因此 [=24] =]. ord 'x' - 48120 - 48 = 72。添加 4,最后得到 76。算法按预期工作。 您只是根本不使用分配的值。相反,您甚至不将 Atom 解释为变量,而是解释为一位数的 ASCII 数字。

话虽如此,解决方案会是什么样子?好吧,类似的东西:

evalABinOp :: Env -> HenryVal -> ABinOp -> HenryVal -> IOThrowsError HenryVal
evalABinOp env (Integer a) Add (Integer b)   = return $ Integer $ a + b
evalABinOp env (Atom a)    op  b@(Integer _) = getVar env a >>= (\c -> evalABinOp env c op b)

我们没有立即使用 a,而是先查找值。请注意,这仅在您的解析器将数字解释为 HenryVal 语言中的 Integer 时才有效。请注意,没有 str2Int 魔术:所有这些都应该在我们使用 x 之前处理好,除非我们想允许添加字符串。