Haskell递归
maximum 函数取一组可排序的 List(属于 Ord Typeclass) 做参数,并回传其中的最大值。想想,在命令式风格中这一函数该怎么实现。很可能你会设一个变量来存储当前的最大值,然后用循环遍历该 List,若存在比这个值更大的元素,则修改变量为这一元素的值。到最后,变量的值就是运算结果。唔!描述如此简单的算法还颇费了点口舌呢!
现在看看递归的思路是如何:我们先定下一个边界条件,即处理单个元素的 List 时,回传该元素。如果该 List 的头部大于尾部的最大值,我们就可以假定较长的 List 的最大值就是它的头部。而尾部若存在比它更大的元素,它就是尾部的最大值。就这么简单!现在,我们在 Haskell 中实现它
maximum' :: (Ord a) => [a] -> a
maximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs)
| x > maxTail = x
| otherwise = maxTail
where maxTail = maximum' xs
如你所见,模式匹配与递归简直就是天造地设!大多数命令式语言中都没有模式匹配,于是你就得造一堆 if-else 来测试边界条件。而在这里,我们仅需要使用模式将其表示出来。第一个模式说,如果该 List 为空,崩溃!就该这样,一个空 List 的最大值能是啥?我不知道。第二个模式也表示一个边缘条件,它说, 如果这个 List 仅包含单个元素,就回传该元素的值。
改用 max 函数会使代码更加清晰。如果你还记得,max 函数取两个值做参数并回传其中较大的值。如下便是用 max 函数重写的 maximun'
maximum' :: (Ord a) => [a] -> a
maximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:xs) = max x (maximum' xs)
太漂亮了!一个 List 的最大值就是它的首个元素与它尾部中最大值相比较所得的结果,简明扼要。

现在我们已经了解了递归的思路,接下来就使用递归来实现几个函数. 先实现下 replicate 函数, 它取一个 Int 值和一个元素做参数, 回传一个包含多个重复元素的 List, 如 replicate 3 5 回传 [5,5,5]. 考虑一下, 我觉得它的边界条件应该是负数. 如果要 replicate 重复某元素零次, 那就是空 List. 负数也是同样, 不靠谱.
replicate' :: (Num i, Ord i) => i -> a -> [a]
replicate' n x
| n <= 0 = []
| otherwise = x:replicate' (n-1) x
在这里我们使用了 guard 而非模式匹配, 是因为这里做的是布林判断. 如果 n 小于 0 就回传一个空 List, 否则, 回传以 x 作首个元素并后接重复 n-1 次 x 的 List. 最后, (n-1) 的那部分就会令函数抵达边缘条件.
Note: Num 不是 Ord 的子集, 表示数字不一定得拘泥于排序, 这就是在做加减法比较时要将 Num 与 Ord 类型约束区别开来的原因.
接下来实现 take 函数, 它可以从一个 List 取出一定数量的元素. 如 take 3 [5,4,3,2,1], 得 [5,4,3]. 若要取零或负数个的话就会得到一个空 List. 同样, 若是从一个空 List中取值, 它会得到一个空 List. 注意, 这儿有两个边界条件, 写出来:
take' :: (Num i, Ord i) => i -> [a] -> [a]
take' n _
| n <= 0 = []
take' _ [] = []
take' n (x:xs) = x : take' (n-1) xs 首个模式辨认若为 0 或负数, 回传空 List. 同时注意这里用了一个 guard 却没有指定otherwise部分, 这就表示n若大于 0, 会转入下一模式. 第二个模式指明了若试图从一个空 List 中取值, 则回传空 List. 第三个模式将 List 分割为头部和尾部, 然后表明从一个 List 中取多个元素等同于令x作头部后接从尾部取n-1个元素所得的 List. 假如我们要从[4,3,2,1]中取 3 个元素, 试着从纸上写出它的推导过程
reverse' :: [a] -> [a]
reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]
继续下去!
Haskell 支持无限 List,所以我们的递归就不必添加边界条件。这样一来,它可以对某值计算个没完, 也可以产生一个无限的数据结构,如无限 List。而无限 List 的好处就在于我们可以在任意位置将它断开.
repeat 函数取一个元素作参数, 回传一个仅包含该元素的无限 List. 它的递归实现简单的很, 看:
repeat' :: a -> [a]
repeat' x = x:repeat' x
zip 取两个 List 作参数并将其捆在一起。zip [1,2,3] [2,3] 回传 [(1,2),(2,3)], 它会把较长的 List 从中间断开, 以匹配较短的 List. 用 zip 处理一个 List 与空 List 又会怎样? 嗯, 会得一个空 List, 这便是我们的限制条件, 由于 zip 取两个参数, 所以要有两个边缘条件
zip' :: [a] -> [b] -> [(a,b)]
zip' _ [] = []
zip' [] _ = []
zip' (x:xs) (y:ys) = (x,y):zip' xs ys
前两个模式表示两个 List 中若存在空 List, 则回传空 List. 第三个模式表示将两个 List 捆绑的行为, 即将其头部配对并后跟捆绑的尾部. 用 zip 处理 [1,2,3] 与 ['a','b'] 的话, 就会在 [3] 与 [] 时触及边界条件, 得到(1,'a'):(2,'b'):[] 的结果,与 [(1,'a'),(2,'b')] 等价.
再实现一个标准库函数 -- elem! 它取一个元素与一个 List 作参数, 并检测该元素是否包含于此 List. 而边缘条件就与大多数情况相同, 空 List. 大家都知道空 List 中不包含任何元素, 便不必再做任何判断
elem' :: (Eq a) => a -> [a] -> Bool
elem' a [] = False
elem' a (x:xs)
| a == x = True
| otherwise = a `elem'` xs
这很简单明了。若头部不是该元素, 就检测尾部, 若为空 List 就回传 False.
快速"排序
假定我们有一个可排序的 List, 其中元素的类型为 Ord Typeclass 的成员. 现在我们要给它排序! 有个排序算法非常的酷, 就是快速排序 (quick sort), 睿智的排序方法. 尽管它在命令式语言中也不过 10 行, 但在 Haskell 下边要更短, 更漂亮, 俨然已经成了 Haskell 的招牌了. 嗯, 我们在这里也实现一下. 或许会显得很俗气, 因为每个人都用它来展示 Haskell 究竟有多优雅!
它的类型声明应为 quicksort :: (Ord a) => [a] -> [a], 没啥奇怪的. 边界条件呢? 如料,空 List。排过序的空 List 还是空 List。接下来便是算法的定义:排过序的 List 就是令所有小于等于头部的元素在先(它们已经排过了序), 后跟大于头部的元素(它们同样已经拍过了序)。 注意定义中有两次排序,所以就得递归两次!同时也需要注意算法定义的动词为"是"什么而非"做"这个, "做"那个, 再"做"那个...这便是函数式编程之美!如何才能从 List 中取得比头部小的那些元素呢?List Comprehension。好,动手写出这个函数!
quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
let smallerSorted = quicksort [a | a <- xs, a <= x]
biggerSorted = quicksort [a | a <- xs, a > x]
in smallerSorted ++ [x] ++ biggerSorted
我们已经写了不少递归了,也许你已经发觉了其中的固定模式:先定义一个边界条件,再定义个函数,让它从一堆元素中取一个并做点事情后,把余下的元素重新交给这个函数。 这一模式对 List、Tree 等数据结构都是适用的。例如,sum 函数就是一个 List 头部与其尾部的 sum 的和。一个 List 的积便是该 List 的头与其尾部的积相乘的积,一个 List 的长度就是 1 与其尾部长度的和. 等等
再者就是边界条件。一般而言,边界条件就是为避免进程出错而设置的保护措施,处理 List 时的边界条件大部分都是空 List,而处理 Tree 时的边界条件就是没有子元素的节点。
处理数字时也与之相似。函数一般都得接受一个值并修改它。早些时候我们编写过一个计算 Fibonacci 的函数,它便是某数与它减一的 Fibonacci 数的积。让它乘以零就不行了, Fibonacci 数又都是非负数,边界条件便可以定为 1,即乘法的单比特。 因为任何数乘以 1 的结果还是这个数。而在 sum 中,加法的单比特就是 0。在快速排序中,边界条件和单比特都是空 List,因为任一 List 与空 List 相加的结果依然是原 List。
使用递归来解决问题时应当先考虑递归会在什么样的条件下不可用, 然后再找出它的边界条件和单比特, 考虑参数应该在何时切开(如对 List 使用模式匹配), 以及在何处执行递归.
转自:http://learnyouahaskell-zh-tw.csie.org/zh-cn/recursion.html
Haskell递归的更多相关文章
- 《Two Dozen Short Lessons in Haskell》(二十二)递归
<Two Dozen Short Lessons in Haskell>(Copyright © 1995, 1996, 1997 by Rex Page,有人翻译为Haskell二十四学 ...
- Haskell 笔记 ③
①循环?NO!请递归思考问题! 手艹一个求列表中最大值代码,C语言中习惯性for扫一下比较出最大值.但是可以用递归! maximum'::(Ord a)=>[a]->a maximum' ...
- haskell中的cps
cps全称叫continuation passing style,简要来讲就是告诉函数下一步做什么的递归方式,由于普通递归有栈溢出的问题,而cps都是尾递归(tail recursion),尾递归则是 ...
- haskell中的monad
monad本意是单子.在haskell中,第一个接触的基本都是IO action,通过把IO动作包装起来我们能很方便的与现实世界进行数据交换.但其实monad的用途不止如此,monad还能讲一系列操作 ...
- haskell debug
最近在学习haskell这门神奇的语言,但是由于print不方便,程序出错的时候都不知道是怎么回事.网上搜了一把发现有这么一个好东西 import Debug.Trace funct :: Integ ...
- 逆转序列的递归/尾递归(+destructuring assignment)实现(JavaScript + ES6)
这里是用 JavaScript 做的逆转序列(数组/字符串)的递归/尾递归实现.另外还尝鲜用了一下 ES6 的destructuring assignment + spread operator 做了 ...
- 《Haskell趣学指南 Learn You a Haskell for Great Good!》-代码实验
doubleMe x = x + x doubleUs x y = doubleMe x + doubleMe y doubleSmallNumber x = then x else x * doub ...
- C++模板元编程 - 3 逻辑结构,递归,一点列表的零碎,一点SFINAE
本来想把scanr,foldr什么的都写了的,一想太麻烦了,就算了,模板元编程差不多也该结束了,离开学还有10天,之前几天部门还要纳新什么的,写不了几天代码了,所以赶紧把这个结束掉,明天继续抄轮子叔的 ...
- Haskell 趣学指南 入门笔记(二)
显示类型声明,Haskell是不用定义类型的原因,很像python 想要确定某个表达式的类型 *Main> :t 'a' 'a' :: Char *Main> :t True True : ...
随机推荐
- IDEA下安装/配置Jrebel
IDEA下安装/配置Jrebel6.X 1. 为什么要使用Jrebel 在日常开发过程中, 一旦修改配置/在类中增加静态变量/增加方法/修改方法名等情况, tomcat不会自动加载, 需要重启tomc ...
- Factory Method 工厂方法模式
Factory method工厂方法模式是一种实现了“工厂”概念的面向对象设计模式.就像其他创建型模式一样,它也是处理在不指定对象具体类型的情况下创建对象的问题.工厂方法模式的实质是“定义一个创建对象 ...
- codeforces 505C Mr. Kitayuta, the Treasure Hunter(dp)
题意:有30001个岛,在一条线上,从左到右编号一次为0到30000.某些岛屿上有些宝石.初始的时候有个人在岛屿0,他将跳到岛屿d,他跳跃的距离为d.如果当前他跳跃的距离为L,他下一次跳跃的距离只能为 ...
- mRemote配置
配置完mRemote后 备份C:\Users\Administrator\AppData\Local\Felix_Deimel\mRemote\confCons.xml文件 覆盖到其他电脑可以直接使用
- 【锋利的jQuery】学习笔记01
第一章 认识jQuery 一.常见的JS库 Prototype 最早的js库之一.把好用JS方法组合,缺点结构松散. Dojo 离线存储API.生成图标组件.矢量图形库.Comet支持.企业级JS库, ...
- ArcGIS 设置地图显示范围大小
Arcmap的FullExtent默认是地图加载的时候的extent.其实这个fullExtent是可以设置的. 打开ArcMap,选择左边图例的Layers ,右键点击,选择“Properties. ...
- C# 读取txt文本内容写入到excel
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...
- Struts2文件配置 登陆页面
Struts 版本号 struts-2.3.16.3 web.xml 配置 <?xml version=”1.0″ encoding=”UTF-8″?> <web-app versi ...
- PYTHON开发--面向对象基础二
一.成员修饰符 共有成员 私有成员, __字段名 - 无法直接访问,只能间接访问 1. 私有成员 1.1 普通方法种的私有成员 class Foo: def __init__(self, n ...
- 浅析tornado web框架
tornado简介 1.tornado概述 Tornado就是我们在 FriendFeed 的 Web 服务器及其常用工具的开源版本.Tornado 和现在的主流 Web 服务器框架(包括大多数 Py ...