理解Haskell在“如果那么别的”情况下的懒惰

lc8prwob  于 2022-11-14  发布在  其他
关注(0)|答案(1)|浏览(171)

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

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

class HasVars a where
  var :: String -> a

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

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

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

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))

为了实际计算表达式,提供了以下函数:

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

因此,在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

因此,我的查询如下:
在Haskell的一个普通表达式中,表达式只在需要的时候才求值,这对我来说仍然不是很直观,但是我有点明白了。但是在上面的第一个表达式中,内部求值是如何对条件中的表达式工作的呢?
因为据我所知,第一个add正在发生,因此将检查if条件。并且必须将条件求值到True或完全求值到False的点。但是在||的第二个表达式中,它将尝试计算对应于f2 mMapmul表达式。现在mul中又出现了一个if条件,所以我很困惑,因为在表达式求值过程中,会出现反复出现的if条件。
PS:M.Map等是因为import qualified Data.Map as M

pod7payv

pod7payv1#

||的定义是惰性的,并且仅在需要时才计算第二个参数,即当第一个参数为假时。
您可以在GHCi中进行测试,观察以下内容是否不会触发错误。

> True || error "ouch!"
True

在您的示例中,isNothing (f1 mMap) || isNothing (f2 mMap)将首先计算isNothing (f1 mMap),如果这是真的,则将跳过对isNothing (f2 mMap)的计算。
注意,这本质上与布尔运算符||的“短路”语义相同,后者在C、C++、Java和许多其他语言中很常见。在这里,对f() || g()求值不会调用g,除非f()求值为false。

关于风格的小插曲

你不应该使用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)

(有一些库函数可以缩短这一点,但这是无关紧要的)。
上面的代码根本没有使用布尔值。它也只在需要的时候才计算f2 mMap,也就是说,只有当f1 mMap到达(_ , Nothing) -> Nothing行时,也就是说,只有当f1 mMap不是Nothing时。这提供了与前面使用布尔值和||的代码相同的懒惰语义。

相关问题