理解“if then else”情况下的 Haskell 惰性

gau*_*ing 0 haskell functional-programming lazy-evaluation

目前,我正在尝试通过宾夕法尼亚大学的 FP in Haskell 课程来学习 Haskell。在其中一项作业中,我必须定义以下类型类来实现表达式求值计算器:

class Expr a where
  mul :: a -> a -> a
  add :: a -> a -> a
  lit :: Integer -> a

class HasVars a where
  var :: String -> a
Run Code Online (Sandbox Code Playgroud)

一种模仿数学表达式的数据类型,可以包含整数、加法、乘法,还可以在表达式中保存变量。

data VarExprT = VarLit Integer
              | VarAdd VarExprT VarExprT
              | VarMul VarExprT VarExprT
              | Var String
              deriving (Show, Eq)

instance HasVars VarExprT where
  var = Var

instance Expr VarExprT where
  lit = VarLit
  add = VarAdd
  mul = VarMul

Run Code Online (Sandbox Code Playgroud)

现在,为了模拟表达式中变量的加法、乘法操作,我必须创建上述类型类的实例,如下所示:

instance HasVars (M.Map String Integer -> Maybe Integer) where
  var str = \mMap -> M.lookup str mMap

instance Expr (M.Map String Integer -> Maybe Integer) where
  lit x = \mMap -> Just x
  add f1 f2 = \mMap -> if isNothing (f1 mMap) || isNothing (f2 mMap)
                        then 
                          Nothing
                        else
                          Just (fromJust (f1 mMap) + fromJust (f2 mMap))
  mul f1 f2 = \mMap -> if isNothing (f1 mMap) || isNothing (f2 mMap)
                        then 
                          Nothing
                        else
                          Just (fromJust (f1 mMap) * fromJust (f2 mMap))

Run Code Online (Sandbox Code Playgroud)

为了实际评估表达式,提供了以下函数:

withVars :: [(String, Integer)] -> (M.Map String Integer -> Maybe Integer)-> Maybe Integer
withVars vs exp = exp $ M.fromList vs
Run Code Online (Sandbox Code Playgroud)

因此,在 ghci 中使用上述函数如下所示:

*Calc> withVars [("x", 6)] $ add (lit 3) (mul (lit 6) (var "x"))
Just 39
*Calc> withVars [("x", 6)] $ add (lit 3) (var "y")
Nothing
Run Code Online (Sandbox Code Playgroud)

所以我的查询如下:

在 Haskell 的普通表达式中,表达式仅在需要时才被求值,这对我来说仍然不是很直观,但我有点明白了。但是在上面的第一个表达式中,条件中的表达式的内部评估将如何工作?

因为据我了解,首先add正在发生,因此if将检查条件。并且条件必须被评估到达到True或完全评估到的程度False。但在 的第二个表达式中,||它将尝试计算对应于 的mul表达式。现在又出现了一个条件。因此,由于表达式求值过程中出现反复出现的条件,我对求值究竟如何进行感到困惑。(mul (lit 6) (var "x")) mMapf2 mMapifmulif

PS:M.Map等是因为import qualified Data.Map as M

chi*_*chi 6

的定义||是惰性的,并且仅在需要时(即第一个参数为 false 时)才计算第二个参数。

您可以在 GHCi 中测试它,观察以下内容不会触发错误。

> True || error "ouch!"
True
Run Code Online (Sandbox Code Playgroud)

在您的情况下,isNothing (f1 mMap) || isNothing (f2 mMap)将首先评估isNothing (f1 mMap),如果这是真的,它将跳过 的评估isNothing (f2 mMap)

||请注意,这本质上与 C、C++、Java 和许多其他语言中常见的布尔运算符的“短路”语义相同。在那里,除非评估结果为 false,否则评估f() || g()不会调用。gf()

关于风格的小题外话

你不应该使用fromJust-- 这是一个部分函数,​​如果你忘记检查Nothing. 在您的代码中,您确实会对此进行检查,但这不是推荐的样式。

您的代码患有“布尔盲性”:您有两个Maybe Integer值,而不是直接测试它们,而是首先将它们转换为布尔值,从而丢失了宝贵的信息(内部的整数)。由于您在测试中丢失了该信息,因此您需要诉诸危险的工具,例如fromJust恢复在测试中丢失的信息。

这里的常见做法是避免布尔值,避免,并使用模式匹配直接if测试值。Maybe Integer

add f1 f2 = \mMap -> case (f1 mMap, f2 mMap) of
   (Nothing, _      ) -> Nothing
   (_      , Nothing) -> Nothing
   (Just i1, Just i2) -> Just (i1 + i2)
Run Code Online (Sandbox Code Playgroud)

(有库函数可以缩短它,但这无关紧要)。

上面的代码根本没有使用布尔值。它还将f2 mMap仅在需要时进行评估,即仅在(_ , Nothing) -> Nothing到达该行时进行评估,即仅在f1 mMap不是 时进行评估Nothing。这提供了与之前使用布尔值和 的代码相同的惰性语义||