让我们从功能上思考,让类型来指导我们。
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate xs = ???
我们列出一份清单,然后……做点什么。那么,第一个问题是:如果列表为空,我们希望发生什么?有几种方法可以处理这个问题,但为了我们的目的,我们只会说这是一个错误并发出错误信号
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate [] = error "Empty list of dates!"
maxDate (x : xs) = ???
好的,下一个最简单的情况是单元素列表。在那种情况下,最大的日期显然是唯一的。毕竟,我们只有一个选择。
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate [] = error "Empty list of dates!"
maxDate [x] = x
maxDate (x : xs) = ???
现在我们只剩下一般情况了:我们的列表中有几个元素。我使用了模式匹配x : xs 来提取第一个。我们现在要做的是找出哪个更大:第一个元素或列表的其余部分。我们可以通过递归找出列表其余部分中最大的元素是什么:maxDate xs。然后我们需要弄清楚那个东西是大于还是小于第一个元素x。您已经为此编写了一个函数,所以让我们使用它。
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate [] = error "Empty list of dates!"
maxDate [x] = x
maxDate (x : xs) = biggerDate x (maxDate xs)
现在我们有了一个有效的实现。但我们不要止步于此。让我们变得更好。
首先,请注意我们可以对(a3, a2, a1) 上的任何二进制函数执行此操作,而不仅仅是biggerDate。让我们稍微抽象一下我们的函数并接受一个额外的参数
foldDate :: (Ord a1, Ord a2, Ord a3) => ((a3, a2, a1) -> (a3, a2, a1) -> (a3, a2, a1)) -> [(a3, a2, a1)] -> (a3, a2, a1)
foldDate _ [] = error "Empty list!"
foldDate _ [x] = x
foldDate f (x : xs) = f x (foldDate f xs)
我添加了一个额外的参数,以使我们的函数在一般情况下更有用。现在,maxDate 只是foldDate 的一个特例。
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate = foldDate biggerDate
但我们可以更进一步,事实证明,foldDate 根本不需要它的参数是日期。它适用于任何类型。
fold :: (a -> a -> a) -> [a] -> a
fold _ [] = error "Empty list!"
fold _ [x] = x
fold f (x : xs) = f x (fold f xs)
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate = fold biggerDate
现在,Haskell 中有一个非常有用的工具,叫做Hoogle。您可以将类型签名插入其中,它会告诉您与该签名匹配的内容是内置的还是在知名的 Haskell 库中。让我们插入抽象的fold 函数类型:(a -> a -> a) -> [a] -> a。第一个结果是一个名为foldr1 的函数。虽然源代码比我们在这里写的要复杂一些,但事实证明foldr1 确实是我们想要的函数。我们甚至不需要自己编写fold 函数;它已经内置了。
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate = foldr1 biggerDate
太棒了! maxDate 现在只有两个字。这很时髦。但我们也可以在biggerDate 上工作。看,事实证明,Ord 有一个三元组的实例,它自动从第一个元素开始排序,然后是第二个,然后是第三个。因此,如果您愿意按 YMD 顺序(而不是像现在这样的 DMY)来定位您的日期,您的 biggerDate 也可以变得更简单。
biggerDate :: (Ord a1,Ord a2,Ord a3) => (a3, a2, a1) -> (a3, a2, a1) -> (a3, a2, a1)
biggerDate x y = if x > y then x else y
如果您仍然希望以 DMY 格式打印日期,您始终可以使用自己的 Show 实例定义自定义数据类型 (data Date = Date Int Int Int),但现在我们将只使用 YMD,因为它使一些事情变得更简单.
好的,但再次,让我们抽象一下。该函数对元组没有任何作用,所以它应该适用于任何Ord 的东西,对吧?
bigger :: Ord a => a -> a -> a
bigger x y = if x > y then x else y
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate = foldr1 bigger
好的,这个类型签名现在看起来很抽象。让我们再次跳到Hoogle。瞧,bigger 函数也是内置的:它被称为max。所以我们甚至不需要bigger。
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate = foldr1 max
让我们再次抽象一下。这个版本的maxDate 也没有真正使用元组,所以它的类型签名可能和Ord a => [a] -> a 一样通用。还有一个trip through Hoogle 告诉我们,即使那个函数也是available in Haskell。所以我们最终的定义是
maxDate :: (Ord a1, Ord a2, Ord a3) => [(a3, a2, a1)] -> (a3, a2, a1)
maxDate = maximum
maxDate 函数字面意思是 Haskell 内置的,如果您知道如何使用可用的抽象技术。
我如此详细地介绍所有这些的原因正是如此。准备好自己编写这些递归定义是很有价值的,尤其是在开始时,我建议你这样做。事实上,仅仅通过标准库(尤其是Data.List 是一个金矿)并手动实现你在那里看到的功能是学习 Haskell 的一个很好的练习。
但同样重要的是要注意,标准库很好地捕获了许多常见的设计模式。你的“我有一个二进制函数,我想折叠一个列表”的模式被foldr1(及其折叠函数系列)捕获,更一般地说,你的模式“我想要这个列表中最大的东西, maximum 函数捕获了一些“最大”的定义。这就是人们使用 Haskell 的原因。它的多态能力做了很多工作,并提供了一个相对较小的标准库(与 Java 相比),基于它的大小,它比乍一看更有用,并且捕获了很多更高的- 非常优雅地排序编程模式。