这整个“如果”与“否”的事情让我想到了表达式问题1。基本上,观察到使用 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 语言没有类型安全的代数数据类型有关,而不是与“如果statemsnts”不利于封装。由于类型安全的唯一方法是使用方法调用,因此鼓励您将使用 if 语句的程序转换为使用访问者模式4或更糟的程序:将应该使用访问者模式的程序转换为使用简单方法分派的程序,因此可扩展性容易朝错误的方向发展。
第二件事是我不喜欢仅仅因为你可以将事物分解成函数。特别是,我发现所有函数只有 5 行并调用大量其他函数的风格很难阅读。
最后,我认为您的示例并没有真正摆脱 if 语句。本质上,您正在做的是使用从整数到新数据类型的函数(有两种情况,一种用于大,一种用于小),然后在使用数据类型时仍然需要使用 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 - 本质上是代数数据类型的“教堂编码”