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

函数式编程的定义是:

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

它更强调函数的应用,而不像命令式编程更强调状态的改变。

无副作用(side effect)

命令式函数可能会改变程序的状态,这就会对其产生副作用。在命令式编程中,在执行程序不同的状态下同一个函数的返回结果会发生改变。例如,下面是一个使用JavaScript写的函数。

1
2
3
4
var state = true;
var getVal = function () {
     return state;
}
1
2
3
console.log(getVal()); //true
state = false;
console.log(getVal()); // false

注意我们调用了两次getVal(),但是其输出了不同的结果。而在Haskell中,对变量只有声明,没有赋值。即如果声明了一个值为true的state变量,就无法再将其修改为false。这叫做变量的不变性

而函数式编程中如果描述状态的变化那,就是将状态变化作为函数的参数进行传递。

延迟计算(lazy evaluation)

正因为函数式编程无副作用,所以延迟计算(又称为惰性求值)就成为可能。
延迟计算指将一个表达式的值推迟到直到被需要时才进行计算。(delays the evaluation of an expression until its value is needed )

它的优点是:

  • 避免了不需要的运算,从而提高的性能。
  • 使创建无限的数据结构成为可能。

例如,我们写一个将指定参数放置到一个无限长的数组中的函数。

1
2
repeat' :: a -> [a]
repeat' x = x : repeat' x

第一行是对repeat’参数及返回值的定义,它接收一个类型a,返回a的数组。
第二行是repeat’函数的实现,它将x放置到一个无穷大的数组中。

在没有延迟计算特性的编程语言中,这种函数是根本无法使用的。因为一旦调用就会陷入死循环。
即使在支持延迟计算的编程语言中,我们直接输出这个数组:print $ repeat' 10,也会进入死循环。那么如何使用它那?我们可以写一个take函数,其可以返回数组中前几位元素。

1
2
3
take' :: Int -> [a] -> [a]
take' 1 (x:xs) = [x]
take' index (x:xs) = x : (take' (index-1) xs)

我们这样调用它,

1
2
3
4
Prelude> take' 3 (repeat' 5)
[5,5,5]
Prelude> take' 3 (repeat' 'a')
"aaa"

Haskell对函数参数默认采取延迟计算的求值策略。所以这样在调用repeat’函数时并不是先将repeat’函数的结果数组计算出来,再进行take操作,而是take操作需要前几位元素,repeat’函数才会生成前几位元素。

高阶函数(Higher-order function)

一个函数成为高阶函数需要满足下面两条中的至少一条:

  1. 将一个或多个函数作为输入。
  2. 输出是一个函数。

换句话说,高阶函数就是将函数作为参数或者作为返回值的函数。其他函数都成为一阶函数(first order function)。其实这个概念最早来源于数学领域。

函数是Haskell世界中的一等公民,所以肯定支持高阶函数。举个例子,Haskell中有个map函数,它的定义是这样的:
map:: (a -> b) -> [a] -> [b]
它的作用是传入一个函数及一个数组,对该数组中的每一个元素应用此函数,从而转换为另一个数组。
我们可以自己实现一个map函数。

1
2
3
map' :: (a -> b) -> [a] -> [b]
map' f [] = []
map' f (x:xs) =f x : map' f xs

map’函数接收两个参数,第一个参数是一个函数,该函数输入值为a类型的值,输出值为b类型的值,第二个参数为源数组。
我们调用ma p’函数时,可以直接写一个lambda表达式,对源数组进行各种操作。

1
2
3
4
Prelude> map' (\x -> x + 5)  [1,2,3]
[6,7,8]
Prelude> map' (\x -> x * x)  [1,2,3]
[1,4,9]

如果我们有这样一个需求,我们想通过map’函数对数组的每个对象都加上一个值n,这个n我不想直接定义在此lambda表达式中,能实现吗?答案是可以。

1
2
outer = let n = 5 in map' (\x -> x + n) [1,2,3]
[6,7,8]

对于匿名函数(\x -> x + n)来说,n就是non-local variable。什么是non-local variable那?如果一个函数使用了一个变量,这个变量既不属于全局变量,也不属于在此函数中定义的变量,那这个变量对于此函数来说就是non-local variable
所谓的闭包就是使用了non-local variable的函数。

curry function

curry function还真比较难翻译,先看看wiki百科的翻译:

In mathematics and computer science, currying is the technique of transforming a function that takes multiple arguments (or a tuple of arguments) in such a way that it can be called as a chain of functions, each with a single argument (partial application).

在数学领域和计算机科学领域,currying是一项将接收多个参数(或参数元组)函数转换为函数的链式调用的技术,链条中的每个函数接收单个参数。

这句话看起来真费解。那么我用一个例子说明一个。

假设现在有一个函数为f(x,y) = x/y。那么f(2,3)的执行过程是什么样的那? 首先,我们将x替换为2.那么得到了f(2,y) = 2/y。我们定义一个新的函数g(y)= f(2,y) = 2/y。再将y替换成3,那么得到了g(3) = f(2,3) = 2/3。这个g(y)函数就是f(x,y)的一个curried function.

举个例子。上文中我们构造了一个map’函数,它接收一个函数及一个数组。如果我们想实现一个名为doubleMe的函数,它接收一个数组,将数组中每个元素都翻一倍。这个可以这样写:

1
doubleMe = map' (\x -> x * x)

注意看,定义doubleMe时我们使用了map’函数,但是给map’函数只传递了一个参数,并没有提供第二个参数。所以在调用doubleMe时,要给其传递一个数组。

1
2
Prelude> doubleMe [1,2,3]
[1,4,9]

doubleMe的函数完全等价于:

1
doubleMe ary = map' (\x-> x * x) ary

换句话说,如果一个函数接收多个参数,那么接收部分参数的该函数也是一个函数。

时间: 2024-10-26 13:20:39

Haskell函数式编程之三-纯函数式编程特点的相关文章

《高阶Perl》——1.6 函数式编程与面向对象式编程

1.6 函数式编程与面向对象式编程 现在停下来看看已经做的.已经有了一个有用的函数,total_size(),它包含了在其他应用中遍历目录结构的有用代码.所以为使total_size()更具普遍意义,把所有与计算大小相关的部分提出来,替换成用户指定的任意函数的调用.结果就是dir_walk().现在,对于任何需要遍历目录结构并做某事的程序,dir_walk()处理遍历部分,参数函数处理"做某事"部分.通过传递合适的函数对给dir_walk(),可以使它做任何想要它做的事情.已经获得了灵

实战JSP进阶编程之三:在Tomcat下配置Hibernate的开发环境

这是实战JSP进阶编程之三. 今天花了几个小时,终于将机房里面的Tomcat+Hibernate的开发.学习环境配置好了. 应用场景:Tomcat 5.5, Hibernate 2.1.7, Mysql 3.23.43, Mysql Driver:3.0.14, JDK: 1.4.2 OS: TurboLinux Server 8.0 用户环境:普通学生帐户--j2ee,位置: /home/j2ee/public_html 为了方便初学者,本教程特意作了简化处理. 1.将hibernate2.j

iOS多线程编程之三——GCD的应用

iOS多线程编程之三--GCD的应用 一.引言 在软件开发中使用多线程可以大大的提升用户体验度,增加工作效率.iOS系统中提供了多种分线程编程的方法,在前两篇博客都有提及: NSThread类进行多线程编程:http://my.oschina.net/u/2340880/blog/416524. NSOperation进行多线程操作编程:http://my.oschina.net/u/2340880/blog/416782. 上两个进行多线程编程的机制都是封装于Object-C的类与方法.这篇博

iOS网络编程之三——NSURLConnection的简单使用

iOS网络编程之三--NSURLConnection的简单使用 一.引言     在iOS7后,NSURLSession基本代替了NSURLConnection进行网络开发,在iOS9后,NSURLConnection相关方法被完全的弃用,iOS系统有向下兼容的特性,尽管NSURLConnection已经被弃用,但在开发中,其方法依然可以被使用,并且如果需要兼容到很低版本的iOS系统,有时就必须使用NSURLConnection类了. 二.使用NSURLConnection进行同步请求     

C#并发编程经典实例--并发编程概述

来自 "C#并发编程经典实例" 优秀软件的一个关键特征就是具有并发性.过去的几十年,我们可以进行并发编程,但是难度很大.以前,并发性软件的编写.调试和维护都很难,这导致很多开发人员为图省事放弃了并发编程.新版.NET 中的程序库和语言特征,已经让并发编程变得简单多了.随着Visual Studio 2012 的发布,微软明显降低了并发编程的门槛.以前只有专家才能做并发编程,而今天,每一个开发人员都能够(而且应该)接受并发编程. 1.1 并发编程简介 首先,我来解释几个贯穿本书始终的术语

iOS界面布局之三——纯代码的autoLayout及布局动画

iOS界面布局之三--纯代码的autoLayout及布局动画 一.引言         关于界面布局,apple的策略已经趋于成熟,autolayout的优势在开发中也已经展现的淋漓尽致.除了使用storyBoard进行布局约束的拖拽,有时我们也需要在代码中进行autolayout的布局设置,Masonry库可以方便的创建约束属性,实际上,我们也没有必要再使用系统原生的代码来创建和设置约束,这篇博客只作为使用的方法备忘.前几篇布局介绍的链接如下: 使用autoresizing进行界面布局:htt

Linux下C编程:底层终端编程实例

Linux 系统的终端处理是一个非常大的系统,需要处理许多不同类型的设备和需求.涉及的内容包括:调制解调器.终端仿真.伪终端等. Linux 系统处理终端的方法是通过串行接口连接的控制台与系统通信并运行程序.由于越来越多的厂商都参与到终端的生产,而且每个厂商都为自己的终端设计自己的命令集,所以需要有一种方法对终端的访问进行一般化处理.Linux 系统使用一个能力数据库terminfo来描述每个终端的能力以及调用这些功能的方法. 在某些情况下,程序员希望能够对某些并不是终端的设备提供终端驱动程序功

yui3的AOP(面向切面编程)和OOP(面向对象编程)

  这篇文章主要介绍了yui3的AOP(面向切面编程)和OOP(面向对象编程),需要的朋友可以参考下 首先请把手放胸前成沉思状:我上了生活,还是被生活上了自己? 没想出答案把,恩,可以读下文了.从语义角度讲,同一事物的不同表述可以反映人的主观视角的不同,从哲学角度将,世界观影响方法论,我们看事物的角度不同,有时会得出截然相悖的结论,从而会影响我们的做事方式和行为准则,现实生活如此,在丰富多彩的编程语言中更是如此,编程模式充满了对现实世界的各种模拟,包括是面向过程,面向对象,还有面向切面.我们大概

yui3的AOP(面向切面编程)和OOP(面向对象编程)_YUI.Ext相关

首先请把手放胸前成沉思状:我上了生活,还是被生活上了自己? 没想出答案把,恩,可以读下文了.从语义角度讲,同一事物的不同表述可以反映人的主观视角的不同,从哲学角度将,世界观影响方法论,我们看事物的角度不同,有时会得出截然相悖的结论,从而会影响我们的做事方式和行为准则,现实生活如此,在丰富多彩的编程语言中更是如此,编程模式充满了对现实世界的各种模拟,包括是面向过程,面向对象,还有面向切面.我们大概已经非常熟悉面向过程和面向对象,切面的英文是Aspects(有时译作方面,我感觉用切面更能贴切的表达A