Haskell函数式编程之二-递归

在Haskell的世界中,没有变量赋值,流程跳转,如果要实现一些简单的功能,比如求一个数组中的最大值,都需要借助递归实现。

递归函数的定义:

A function may be partly defined in terms of itself.
即如果一个函数的定义中使用了其自身,这个函数就叫做递归函数。

普通递归(traditional recursion)

我们就写一个简单的对数组求和的函数。

1
2
3
sum' :: (Num a) => [a] -> a
sum' (x:xs) = x + sum' xs
sum' [] = 0

第一行定义了了一个名为sum'的函数的参数及返回值。这个函数接收一个类型为Num的数组并返回一个Num型的数值。(这里的'是函数名的一部分,因为Haskell允许'作为函数名的一部分。由于系统已经有了sum函数,所以我们加个'与标准sum函数区分开。)

第二行的(x:xs)就是我们传入的数组参数。我们这里使用了Haskell的pattern matching。x表示的是数组中的第一个元素,xs表示数组中的其它元素。我们可以描述求数组中值的和的行为为:数组中的第一个元素与数组中剩余元素的和。所以这就是我们的实现。

第三行则说明了如果给一个空的数组则直接返回0。这也叫做递归的退出条件,否则递归会没完没了。

第二行和第三行共同完成了这个sum'函数的定义。当你传递给它一个参数时,它会根据参数的情况自动选择调用那个实现。

假设我们这样调用它:sum' [1,2,3],程序的执行过程是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sum' [1,2,3] ->

1 + sum' [2,3] ->

1 + (2 + sum' [3]) ->

1 + (2 + (3 + sum' [])) ->

1 + (2 + (3 + 0)) ->

1 + (2 + 3) ->

1 + 5 ->

6

这种递归有个问题就是我们一直到等到递归结束才进行算术运算,这样在执行过程既要保存函数调用的堆栈,还要保存中间计算结果的堆栈,如果递归过深,很容易引起stackOverFlow.

尾递归(tail recursion)

针对上述问题,我们可以换种写法。

1
2
3
4
5
sum' :: (Num a) => [a] -> a -> a

sum' (x:xs) temp = sum' xs x+ temp

sum' [] temp = temp

我们这样调用它: sum' [1,2,3] 0

它的执行顺序是这样的:

1
2
3
4
5
6
7
8
9
sum' [1,2,3] 0 ->

sum' [2,3] 1 ->

sum' [3] 3 ->

sum' []  6 ->

6

第二种写法其实就是尾递归。

尾递归的定义:

A recursive function is tail recursive if the final result of the recursive call is the final result of the function itself.

即:如果一个递归函数,它的最终的递归调用结果就是这个函数的最终结果,那它就是尾递归的。

所以我们可以明显看出,第一个不是尾递归,第二个是。

尾递归优化(tail recursion optimization)

在大多数编程语言中,调用函数需要消费堆栈空间,一个实现了尾递归的递归函数在进行递归调用时,其实只关心递归调用的结果,所以当我们调用下层函数时,可以舍去上层函数的堆栈调用情况,下层递归调用可以重用这个堆栈空间,这种就叫做尾递归优化。一个可能的实现方式是:只需要把汇编代码call改成jmp, 并放弃所有 局部变量压栈处理,就可以了。

尽管尾递归比递归更节省堆栈空间,但并非所有的递归算法都可以转成尾递归的,因为尾递归本质上执行的是迭代的计算过程。这与并非所有的递归算法都可以转成迭代算法的原因是一样的。

互递归(mutual recursion)

互递归就是多个递归函数之间相互调用。互递归的一个简单的例子就是判断一个自然数是偶数还是还是奇数。

1
2
3
4
5
6
7
8
9
10
isOdd :: Int -> Bool
isOdd x
 | x == 0 = False
 | otherwise = isEven (x-1)

isEven :: Int -> Bool
isEven x
 | x == 0 = True
 | otherwise = isOdd (x-1)

这个实现很有意思。

任何一个互递归都可以被转变为直接递归(direct recursion),即将另一个调用inline到当前递归函数中。

下面是isOdd和isEven的直接递归版本。

1
2
3
4
5
6
7
8
9
10
11
12
isOdd :: Int -> Bool
isOdd x
 | x == 0 = False
 | x == 1 = True
 | otherwise = isOdd (x-2)

isEven :: Int -> Bool
isEven x
 | x == 0 = True
 | x == 1 = False
 | otherwise = isEven (x-2)
时间: 2024-10-29 06:39:02

Haskell函数式编程之二-递归的相关文章

Android 开发者如何使用函数式编程 (二)

本文讲的是Android 开发者如何使用函数式编程 (二), 如果你没有读过第一部分,请到这里读: Android 开发者如何使用函数式编程 (一) 在上一篇帖子中,我们学习了纯粹性*.副作用和排序**.在本部分中,我们将讨论不变性和并发. 不变性 不变性是指一旦一个值被创建,它就不可以被修改. 假设我有一个像这样的 Car 类: public final class Car { private String name; public Car(final String name) { this.

Haskell函数式编程之四-List操作

List在函数式语言中是一个重要的抽象,很多事情离了它就很难做到.函数式语言的鼻祖Lisp名称就来自List processing. Haskell本身也给List操作提供了一系列的操作符以及库函数. 对列表操作的运算符 :将一个元素放置到列表的前端. 1 2 3 4 5 6 7 8 Prelude> 1 : [] [1] Prelude> 2 : [3,4,5] [2,3,4,5] Prelude> 'a' : ['g','h','d'] "aghd" Prelud

了解JavaScript函数式编程(二)

上一篇文章里我们提到了纯函数的概念,所谓的纯函数就是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态(我偷懒复制过来的). 但是实际的编程中,特别是前端的编程范畴里,"不依赖外部环境"这个条件是根本不可能的,我们总是不可避免地接触到 DOM.AJAX 这些状态随时都在变化的东西.所以我们需要用更强大的技术来干这些脏活. 一.容器.Functor 如果你熟悉 jQuery 的话,应该还记得,$(...) 返回的对象并不是一个原生的 DOM 对象,

Haskell函数式编程之一-语言初体验

如果你是使用面向对像语言进行编程的程序员,那么你应该去了解掌握一门动态语言.而动态语言的魔力之一就是函数式编程.而要学习了解函数式编程,那么haskell是一个不错的选择. Haskell是是一门纯函数式编程语言(purely functional programming language).在其世界中函数是第一等对象.并且在haskell中没有赋值,例如你指派a的值为5,然后你无法再给a分配其它的值.所以你不能像命令式语言那样命令电脑"要做什么",而是通过函数来描述出问题"

《Haskell函数式编程入门》—— 第1章,第1.6节本章小结

本章小结 学习本章后,相信读者对Haskell应该有了大致的了解.GHCi是一个常用的测试代码的工具,希望读者可以花更多的时间来熟悉.细心的读者可能会发现Haskell与C语言编译后可执行文件的大小有很大差异.其实,Haskell使用内存空间和硬盘空间的效率是有些低的,这也是早期函数式编程没有比C一类的语言更流行的原因之一.但是,如今计算机硬件已经发展到内存和硬盘不会像以前那样限制函数式编程语言能力了,在时间和空间的效率上也可以手动或自动调试优化.因此,相信在不久的未来,函数式编程会以它精炼.缜

《Haskell函数式编程入门》—— 第1章,第1.4节.hs和.lhs文件、注释与库函数

1.4 .hs和.lhs文件.注释与库函数GHCi和Hugs可以解析扩展名为.hs和.lhs的文件.两者所写程序在语法上完全相同,它们的差别是.lhs(literate Haskell script)文件是为了能让Haskell的代码生成文档.有效程序代码可以用"大于号(>)"和空格开头.比如: add :: Int -> Int -> Intadd x y = x + y不以大于号和空格开头的内容将被视作注释处理,且注释与代码间必须有一行以上的间隔.还有一些.lhs

Haskell函数式编程之三-纯函数式编程特点

函数式编程的定义是: In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids stateand mutable data. 即:函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了引入状态及可变数据. 它更强调函数的应用,而不像命令式编

《Haskell函数式编程入门》—— 第1章,第1.5节第一个Haskell程序HelloWorld!

1.5 第一个Haskell程序HelloWorld! HelloWorld程序虽然不能完全展示Haskell编程的风格与优势,但学习计算机编程都是从这里开始的.现在写一个HelloWorld程序,当作开始学习Haskell的第一步吧! 文本编辑器中输入: main = putStrLn "Hello,World!" 保存并命名为Helloworld.hs. 这里可以看到,Haskell和C.Java一样,都以一个名叫main的函数作为程序的开始运行的入口.保存代码文件,打开命令行窗口

《Haskell函数式编程入门》——导读

第1章Haskell简介 第1章第1节Haskell的由来第1章第2节Haskell编译器的安装以及编写环境第1章第3节GHCi的使用第1章第4节.hs和.lhs文件.注释与库函数第1章第5节第一个Haskell程序HelloWorld!第1章第6节小结第2章类型系统和函数第3章基于布尔值的函数第4章库函数及其应用第5章递归函数第6章列表内包第7章高阶函数与复合函数第8章定义数据类型第9章定义类型类第10章Monad初步第11章系统编程及输入/输出第12章记录器Monad.读取器Monad.状态