这整个“如果”与“不如果”的事情让我想起了Expression Problem1。基本上,观察到使用 if 语句或不使用 if 语句进行编程是封装和可扩展性的问题,有时使用 if 语句2 更好,有时使用方法/函数的动态调度更好指针。
当我们想要对某事物建模时,需要担心两个轴:
- 我们需要处理的输入的不同情况(或类型)。
- 我们要对这些输入执行的不同操作。
实现这种事情的一种方法是使用 if 语句/模式匹配/访问者模式:
data List = Nil | Cons Int List
length xs = case xs of
Nil -> 0
Cons a as -> 1 + length x
concat xs ys = case ii of
Nil -> jj
Cons a as -> Cons a (concat as ys)
另一种方法是使用面向对象:
data List = {
length :: Int
concat :: (List -> List)
}
nil = List {
length = 0,
concat = (\ys -> ys)
}
cons x xs = List {
length = 1 + length xs,
concat = (\ys -> cons x (concat xs ys))
}
不难看出,使用 if 语句的第一个版本可以轻松地对我们的数据类型添加新操作:只需创建一个新函数并在其中进行案例分析。另一方面,这使得向我们的数据类型添加新案例变得困难,因为这意味着要返回程序并修改所有分支语句。
第二个版本正好相反。向数据类型添加新案例非常容易:只需创建一个新“类”并告诉我们需要实现的每个方法做什么。但是,现在很难向接口添加新操作,因为这意味着为所有实现该接口的旧类添加新方法。
语言有许多不同的方法来尝试解决表达式问题,并使向模型中添加新案例和新操作变得容易。但是,这些解决方案各有利弊3,所以总的来说,我认为在 OO 和 if 语句之间进行选择是一个很好的经验法则,具体取决于您想要更容易扩展内容的轴。
不管怎样,回到你的问题我想指出几点:
第一个是我认为摆脱所有 if 语句并用方法分派替换它们的 OO“口头禅”与大多数 OO 语言没有类型安全的代数数据类型有关,而不是它必须做的“if statemsnts”不利于封装。由于类型安全的唯一方法是使用方法调用,因此鼓励您将使用 if 语句的程序转换为使用 Visitor Pattern4 或更糟的程序:将应该使用访问者模式的程序转换为程序使用简单的方法分派,因此容易在错误的方向上进行扩展。
第二件事是我不喜欢仅仅因为你可以将事物分解为函数。特别是,我发现所有函数只有 5 行并调用大量其他函数的风格很难阅读。
最后,我认为您的示例并没有真正摆脱 if 语句。本质上,您所做的是将函数从 Integers 转换为新数据类型(有两种情况,一种用于 Big,另一种用于 Small),然后在处理数据类型时仍然需要使用 if 语句:
data Size = Big | Small
toSize :: Int -> Size
toSize n = if n < 10 then Small else Big
someOp :: Size -> String
someOp Small = "Wow, its small"
someOp Big = "Wow, its big"
回到表达式问题的角度,定义我们的toSize / isSmall函数的好处是我们把选择我们的数字适合什么情况的逻辑放在一个地方,我们的函数只能对之后的情况进行操作那。但是,这并不意味着我们已经从代码中删除了 if 语句!如果我们将 toSize 作为工厂函数,并且让 Big 和 Small 类共享一个接口,那么是的,我们将从代码中删除 if 语句。但是,如果我们的 isSmall 只返回一个布尔值或枚举,那么 if 语句的数量将与以前一样多。 (并且您应该选择使用哪种实现方式,具体取决于您是否希望将来更容易添加新方法或新案例——比如 Medium——)
1 - 问题的名称来自您拥有“表达式”数据类型(数字、变量、子表达式的加法/乘法等)并希望实现诸如评估函数和其他事物之类的问题。
2 - 或者在代数数据类型上进行模式匹配,如果你想要更安全的类型...
3 - 例如,您可能必须在“调度员”可以看到它们的“顶层”上定义所有多方法。与一般情况相比,这是一个限制,因为您可以使用深层嵌套在其他代码中的 if 语句(和 lambdas)。
4 - 本质上是代数数据类型的“教堂编码”