CommuSoft 的出色回答。我将对此进行反复讨论,并进行更高级/哲学的讨论。
在为它们设计语言和解释器时要遵循的一个关键准则是principle of compositionality:
- 复杂表达式的含义应该是其各部分含义及其组合方式的函数。
在这种情况下,“含义”是表达式求值的Bools。应用于您的评估者,组合性意味着:
- 您的评估者应该只查看子表达式的含义(
Bool 评估结果),而不是它们的语法。
eval 的这个方程违反了该规则,因为它“窥视”子表达式内部以检查 Literal 构造函数是否存在:
eval (Operation AND (Literal x) (Literal y))
| x == True && y == True = True
| otherwise = False
一个有用的技巧可以确保你以组合方式做事:
- 在语法树类型上编写通用折叠函数。 (有关此概念的一些指导,请参阅 this question。)
- 根据折叠功能编写您的评估器。
在这种情况下:
data Expression = Literal Bool | Operation Operator Expression Expression
data Operator = AND | OR
foldExpression :: (Bool -> r) -> (Operator -> r -> r -> r) -> r
foldExpression f _ (Literal x) = f x
foldExpression f g (Operation op l r) = g op (subfold l) (subfold r)
where subfold = foldExpression f g
诀窍在于,当您使用foldExpression 时,它会阻止您查看当前表达式下方节点的构造函数,因此您必须根据结果类型r 进行处理。所以这里是:
eval :: Expression -> Bool
eval = foldExpression evalLiteral evalOperation
where
-- A literal just evals to the `Bool` it carries.
evalLiteral :: Bool -> Bool
evalLiteral b = b
evalOperation :: Operator -> Bool -> Bool -> Bool
-- An `AND` operation evaluates to the `&&` of its subexpressions' values
evalOperation (AND l r) = l && r
-- An `OR` operation evaluates to the `||` of its subexpressions' values
evalOperation (OR l r) = l || r
注意evalOperation 与CommuSoft 的handler 函数是一样的。这只是将解决方案写成折叠自然而然地失败了。
更多的工作,包括一元操作和大规模可理解性:
data Unary = Not
data Binary = And | Or | If
data Expr = Lit Bool | Expr1 Unary Expr | Expr2 Binary Expr Expr
-- Package all the functions used in a fold into a record
-- so that we don't need to remember the argument order.
-- For complex tree types with many types of nodes you will
-- want this!
data ExprFold r
= ExprFold { literal :: Bool -> r
, unary :: Unary -> r -> r
, binary :: Binary -> r -> r -> r
}
foldExpr :: ExprFold r -> Expr -> r
foldExpr f (Lit b) = literal f b
foldExpr f (Expr1 e) = unary f (foldExpr f e)
foldExpr f (Expr2 e e') = binary f (foldExpr f e) (foldExpr f e')
evaluator :: ExprFold Bool
evaluator = ExprFold { literal = evalLit
, unary = evalExpr1
, binary = evalExpr2 }
where
evalLit b = b
evalExpr1 (Not b) = not b
evalExpr2 (And b b') = b && b'
evalExpr2 (Or b b') = b || b'
evalExpr2 (If b b') = not b || b'