Haskell 函数式编程
Haskell是纯函数式编程,它强调不函数不改变外部世界状态,即,一个函数的输出只由函数接收的输入决定。那如何与外面沟通呢,比如读取一个文件内容并输出这个文件内容(字符串),显然这种函数非纯函数,因为它的输出是会随着文件内容改变而改变。Haskell在纯函数与外部世界之间建立了一扇门,即Monad,这部分内容以后再介绍。
(本文例子多来自real world haskell,原因是这本书给的例子太经典)
1. 与外部世界交流
首先给出一个例子如下,这个函数用于读取输入文件,并作一些处理,然后输出到另一个文件中
--file: Interact.hs
import System.Environment (getArgs) interactWith function inputFile outputFile = do
input <- readFile inputFile
writeFile outputFile (function input) main = mainWith myFunction
where mainWith function = do
args <- getArgs
case args of
[input, output] -> interactWith function input output
_ -> putStrLn "error: two parameters needed"
myFunction = tail
注意每一行的字符缩进。
这段代码中myFunction就是对输入文件的内容进行处理的函数,这里是tail函数,也可以改成其他的,只要函数原型满足String -> String即可。
测试这个源代码
> ghc --make Interact
> cd [target path]
> Interact in.txt out.txt
如果目标同级目录中in.txt文件内容为"中文测试",那执行后,out.txt文件内容为"文测试"。
2. 通用分行
我们知道Windows和Unix类系统的换行符不同,分别为”\r\n”,”\n”或”\r”,而Windows上编写的Haskell源码可能会拿到Unix类系统上编译,那么,比如分行处理就会出问题。
现在我们写一个能解决这个困境的方法,思想是遇到关键字符时进行切断,代码如下
-- file: SplitLines.hssplitLines [] = []
splitLines cs = let (pre, suf) = break isLineTerminator cs in pre : case suf of (‘\r’:’\n’: rest) –> splitLines rest (‘\r’: rest) –> splitLines rest (‘\n’: rest) –> splitLines rest _ -> []isLineTerminator c = c == ‘\r’ || c == ‘\n’
这段代码一目了然。对case的几种情况,顺序是否重要呢。答案是不重要,可以自行测试一下。
Haskell提供了一个分行函数line,以及一个将字符串List合并成一个字符串,其中原来List中的每个字符串末尾添加一个换行符。
3. 中缀函数
函数名置于参数中间,而普通的函数则是前缀函数(函数名置于参数前)。写中缀函数方法是在函数名两边加 ` 号(键盘上数字1左边的那个键)
-- file: InfixFun.hs
a `cons` b = a : [b]
data a `With` b= a `With` b deriving (Show)
使用时,如果函数名置于参数中间则需要在两边加 ` 号,如果置于参数前,则不需要。
注意上面的函数”cons”与Prelude包里的concat不同,concat的原型为[[a]] –> [a],是将输入的各个list合并成一个list。
4. 递归函数
学习Haskell过程中会经常接触,这里不再具体展开,只说一下尾调用优化(tail call optimization),因为函数式语言中没有循环,而是采用递归调用函数自身,这样每次调用时都分配一些内存空间,一般来说这会导致内存占用的线性增长,然而函数式语言的实现一般会检测尾递归然后转换使得在一个不变的内存空间中进行尾递归调用。
大部分命令式语言实现在进行循环时也是在一个不变的内存空间中执行循环体,但是很少有实现尾调用优化,这也是为什么在命令式语言中进行函数式编程导致内存泄漏或者性能很差。(事实上,部分命令式语言进行了TCO,实现尾递归性能不会明显下降,具体情况可以专门去研究一下)
5. 高阶函数
介绍几个高阶函数,所谓高阶函数就是其参数包含另一个函数,类似数学中f(g)。
a) map :: (a –> b) –> [a] –> [b],map函数接收两个输入参数,一个函数f以及一个list,对这个list中的每个元素应用这个函数f,然后得到b,最后返回b的list。
b) filter :: (a –> Bool) –> [a] –> [a],用指定函数筛选一个list中满足条件的元素
c) 对一个集合计算出一个值(reduce),这里给出一个书中的例子,Adler32算法,有关Adler32可自行搜索。
-- file: Adler32.hsimport Data.Char (ord)import Data.Bits (shiftL, (.&.), (.|.))base 65521adler32 xs = helper 1 0 xs where helper a b (x:xs) = let a’= (a + (ord x .&. oxff)) `mod` base b’= (a’+ b) `mod` base in helper a’b’ xs helper a b _ = (b `shiftL` 16) .|. a
我们可以将这些行为抽象到一个高阶函数:对一个list的每一项做一些事情,然后更新累加器,直到处理完所有list项后,返回这个累加器。有点类似将一维的list折叠起来成为一个点有木有?这种类型的函数在Haskell中称为fold。
d) left fold,从list的左边开始折叠
foldl :: (a –> b –> a) –> a –> [b] –> a
开始,函数对累加器种子a0和list第一个项b[0]进行计算得到a1,然后函数对a1和list下一项b[1]计算得到a2,如此知道取完list最后一项,最后返回累加器的值an。比如计算一个整型list各项的和,可以写成如下
-- file: Sum.hssum :: [Integer] –> Integersum xs = foldl (+) 0 xs
e) right fold,从list的右边开始折叠
foldr :: (a –> b –> b) –> b –> [a] –> b
f) fold实例
这个函数其实也很简单,就是从list的尾部取值a[n-1]然后与累加器种子b0计算得到b1,然后取list倒数第二个值a[n-2]与b1计算得到b2,依次直到取list的第一个值a[0]与bn-1计算得到bn,注意这里b后面的数字是下标。
别看foldr好像只是简单的从右折叠,其实用途还是很大滴,比如,最终计算得到一个列表时,则使用foldr,原理很简单,因为连接成为list总是在一个已有的list前面加一个元素,即
a1:[a]
并且,我们可以使用foldr来实现foldl,这是原书上的一个例子,初看确实如作者所说让人非常头疼不容易理解,这里给出这个例子并稍作解释。
-- file: MyFoldl.hsmyFoldl :: (a –> b –> a) –> a –> [b] –> amyFoldl f z xs = foldr step id xs z where step x g a = g (f a x)
一开始本人看这个函数的时候也是有点懵逼了,虽然原作者没有解释但是却给出提示”函数柯里化,通过:type id查看id的签名”。
如果看到局部函数step的定义,知道step接收三个参数,而myFoldl函数体中,foldr后面恰好跟着三个变量,如果把这三个变量当作step的三个输入参数,那就不可能理解这个函数了。好吧,我承认这里说了这么多废话,无非是不想提前剧透,给读者一个自行思考的空间,下面开始解释:
函数的优先级最高,foldr那一行没有小括号,故从左往右执行,foldr接收三个参数,一个函数step,某一个类型参数id,以及一个列表xs,通过:type id 可以知道id是一个函数,查阅haskell API文档也可以知道这是一个返回自身的函数,原型为a –> a,所以,在本上下文中,
id:: (a' –> a')xs:: [b]step:: b –> (a' –> a') –> (a' –> a')foldr:: (b –> (a' –> a') –> (a' –> a')) –> (a' –> a') –> [b] –> (a' –> a')
写了这么一大段,其实只要把 (a' –> a') 看成 a 就可以了,然后,foldr将xs中从末尾开始每一项bk(k为下标)和累加器值 idk(k为下标)作为step的输入参数,然后得到一个中间返回值为idk+1(k+1为下标)
-- foldr 定义如下foldr step zero (x:xs) = step x (foldr step zero xs)foldr _ zero [] = zero-- 本上下文中foldr step id xs == step xs[0] (foldr step id xs') == step xs[0] (step xs[1] (foldr step id xs'')) == step xs[0] (step xs[1] (… (step xs[n-1] id)…))-- 返回结果类型foldr step id xs :: (a' –> a')-- 柯里化foldr step id xs z = (foldr step id xs) z = step xs[0] (step xs[1] (…(step xs[n-1] id)…)) z (*)
到这里,就能对应上myFoldl函数体中局部函数step的定义了,如下
step :: b –> (a' –> a') –> a –> astep x g a = g (f a x)-- 对比(*)式得知-- 第一次计算时x = xs[0]g = step xs[1] (…(step xs[n-1] id)…))a = z-- 第二次计算时x = xs[1]g = step xs[2] (…(step xs[n-1] id)…))a = f a xs[0]-- 第三次计算时x = xs[2]g = step xs[3] (…(step xs[n-1] id)…))a = f (f a xs[0]) xs[1]-- ……-- 最后myFoldl的计算表达式为myFoldl = f (…(f a xs[0])…) xs[n-1]-- end
g) fold空间泄漏(space leaks)
通过前面分析,我们看到fold在计算过程中其实嵌套了很多表达式,一层一层地,事实上,最终只有在需要表达式的值时候,才会计算表达式结果。在计算式的值之前,表达式被存储为一个thunk(关于thunk可以上网搜索其含义),然而不幸的是,存储thunk比存储值占用太多空间了,而我们知道栈空间有限,所以在thunk大到一定程度会导致异常,比如
ghci> foldl (+) 0 [1..1000000]
这个thunk包含了100000个整形和99999个 (+)应用,这会占用很大的内存,事实上,在计算这个表达式值的时候确实引发了异常。解决的方案是使用Data.List模块中定义的 fold' 函数,它跟 fold 类似,但是不会建立thunk,使用如下:
ghci> :m +Data.Listghci> foldl' (+) 0 [1..1000000]500000500000
h) 匿名(lambda)函数
lambda函数以一个反斜线(\)开头,后面跟函数参数,然后跟 (->),然后跟函数体,比如上一部分的myFoldl函数可以写成这样,
myFoldl f z xs = foldr (\x g a –> g (f a x)) id xs z
然而,一个函数能写成lambda的前提是这个函数只有一个子句,如果有多个子句,比如head函数就不可以
unsafeHead = \(x:_) –> t
显然没有考虑到参数为 [ ] 的情况。事实上,在很多场合,一般都避免使用lambda表达式,因为那样导致代码可读性不高。
i) As-Patterns
对参数使用@符号,后面跟模式,比如
suffixes :: [a] –> [[a]]suffixes xs@(_:xs') = xs : suffiex xs'suffixes _ = []
这个例子中,如果@后的模式匹配成功,则xs被绑定到模式中,xs' 被绑定到xs的除去首项的列表,虽然也可以不使用@,如下
suffixes1 :: [a] –> [[a]]suffixes1 (x:xs) = (x:xs) : suffiex1 xssuffixes1 _ = []
但是,使用As-patterns,在递归调用时,参数是共享而非复制,我们知道复制会导致分配新的内存,虽然这个代价并不高。
j) 组合函数
可以将几个函数组合起来,比如
compose :: (b –> c) –> (a –> b) –> a –> ccompose f g x = f (g x)
其实这个compose函数的作用仅仅是将原先需要加小括号改变运算优先级的情况封装,从而使得函数参数 f g x 不加小括号的依次写出来。
幸运的是,haskell中不需要我们定义这样一个compose函数,Prelude包中已有一个这样的函数 (.),用法就是 h = f . g,这样给函数h一个输入 x 时,记住先对 x 应用 g 函数,然后应用 f 函数。
k) 空间泄漏和严格计算
尽量保证在构建一个list时,使用foldr,并用 foldl' 代替foldl ,那么实践中,空间泄漏就不太可能困扰我们。
由于 foldl' 是一个严格计算的,而不像 foldl 是延迟计算,foldl' 通过使用 seq 的一个函数来避开haskell的延迟计算特征,参见代码
-- 给出foldl'的签名foldl' :: (a –> b –> a) –> a –> [b] –> afoldl' _ zero [] = zerofoldl' step zero (x:xs) = let new = step zero x in new `seq` foldl' step new xs
其中,seq函数原型为 seq :: a –> t –> t,seq的引入就是为了获得严格特性(strictness),seq有两个输入参数,类型不定,并返回第二个参数值,具体可上网搜索seq详情。
当seq计算时,在seq返回值之前,它强制其参数先被计算出来。每一次利用 step zero x 计算出值之后再传给foldl' 。
使用seq的坑还是蛮多的,比如下面这个例子就是一个不好的用法
badfoldl' _ zero [] = zerobadfoldl' step zero (x:xs) = seq (step zero x) (badfoldl' step (step zero x) xs)
这里,代码意图是想严格计算 step zero x 的值,然而第一个 step zero x 表达式的值被严格计算出来,与第二个 step zero x 的值没有关联。当第一个表达式值计算出来的时候,seq返回第二个表达式,而第二个表达式 step (step zero x) xs) 中的 step zero x 被没有被严格计算出来。
最后,需要指出的是,seq也不是完全有利的,比如它导致运行时检查表达式是否被严格计算出来。总之,seq的坑,以后需要专门填一下。
Haskell 函数式编程的更多相关文章
- Haskell 函数式编程快速入门【草】
什么是函数式编程 用常规编程语言中的函数指针.委托和Lambda表达式等概念来帮助理解(其实函数式编程就是Lambda演算延伸而来的编程范式). 函数式编程中函数可以被非常容易的定义和传递. Hask ...
- Haskell学习-函数式编程初探
原文地址:Haskell学习-函数式编程初探 为什么要学习函数式编程?为什么要学习Haskell? .net到前端,C#和JavaScript对我来说如果谈不上精通,最起码也算是到了非常熟悉的 ...
- 让JavaScript回归函数式编程的本质
JavaScript是一门被误会最深的语言,这话一点不假,我们看下它的发展历史. 1995年,Netscape要推向市场,需要一门脚本语言来配套它.是使用一门已有的语言,还是发明一门新的语言,这也不是 ...
- C#函数式编程
提起函数式编程,大家一定想到的是语法高度灵活和动态的LISP,Haskell这样古老的函数式语言,往近了说ruby,javascript,F#也是函数式编程的流行语言.然而自从.net支持了lambd ...
- 开始对函数式编程 产生了尊崇感,因为Spring4.x ,Grooxy,Lisp,网易出来伞哥和他的博客
1 无意看到"丢弃重口味的xml配置--spring4用groovy配置bean",这篇文章,里面说到spring4开始可以使用Groovy进行配置,可以取代xml方式和注解方式 ...
- C#函数式编程之由函数构建函数
在面向对象的编程中,如果我们需要复用其他的类,我们可以通过继承来实现.而在函数式编程中我们也可以采取不同的方式来复用这些函数.今天的教程将会讲述两种方式,其中一个就是组合,将多个函数组合成为一个函数, ...
- paip.函数式编程方法概述以及总结
paip.函数式编程方法概述以及总结 1 函数式编程:函数式风格..很多命令式语言里支持函数式编程风格 1.1 起源 (图灵机,Lisp机器, 神经网络计算机) 1.2 函 ...
- 用C++进行函数式编程
http://www.programmer.com.cn/12717/ 文 / John Carmack 译 / 王江平 <Quake>作者Carmack认为追求函数式的程序设计有着实 ...
- 测试和恢复性的争论:面向对象vs.函数式编程
Michael Feathers最近的博文在博客社区引发了一场异常激烈的论战.Feathers发表言论说一些面向对象编程语言的内嵌特性有助于测试的进行,并且使用面向对象编程语言编写的代码更容易恢复. ...
随机推荐
- 解决xcode7.3的一个bug的方法
我今天遇到个bug,就是pod后,居然系统自带的提示语全部没有了,连NSString都要自己拼写,后来百度了,但是都没有解决,然后综合了一下,解决了. 方法: 首先点击桌面 找到前往-->然后选 ...
- SQL2008无法连接到.,及sa登录失败的总结
尊重别人的劳动成果,我是转载别人的: 本文转载自- 红黑联盟http://www.2cto.com/database/201203/123089.html 出现问题 : 标题: 连接到服务器----- ...
- [转]RadStudio DELPHI/C++ BUILDER Berlin 10.1 Update2安装破解教程
源链接:http://bbs.fishc.com/thread-76730-1-1.html 免责声明:本教程所有资源均为学习交流之用,软件和资料版权归原开发公司所有,请于下载后24小时内删除,禁止用 ...
- ideaIU-2016.2.5激活
IntelliJ IDEA的在线注册码生成页面 http://idea.iteblog.com 新的License server地址为:http://idea.iteblog.com/key.php
- HTML form的一些属性(第一版)
p,li { white-space: pre-wrap } HTML表单属性总结(第一版) 基本格式为:<input type="类型" name="名字[唯一, ...
- dplyr 数据操作 常用函数(5)
继续来了解dplyr中的其他有用函数 1.sample() 目的是可以从一个数据框中,随机抽取一些行,然后组成新的数据框. sample_n(tbl, size, replace = FALSE, w ...
- C#中的引用传递、值传递
先来说下C#中的数据类型.分值类型和引用类型两大类. 值类型:直接存储数据的值,保存在内存中 引用类型:存储对值的引用,实际上存储的就是一个内存的地址 C#预定义的简单类型,像int,float, ...
- CSS display:inline-block的元素特点:
将对象呈递为内联对象,但是对象的内容作为块对象呈递.旁边的内联对象会被呈递在同一行内,允许空格. 在CSS中,块级对象元素会单独占一行显示,多个block元素会各自新起一行,并且可以设置width,h ...
- 查询--游标 limit skip sort
打印出所有的里程: var cursor = db.tblDaily.find(); cursor.forEach(function(x){ print(x.DailyCount + x.DailyU ...
- 利用requestjs优化响应式移动端js加载
html: <script data-main="main" src="require.js"></script> main.js re ...