我在Structral programming and formal method 的前半部分简单的接触了Haskell的最基本的语法。 但是用haskell写出如此丑陋的代码显然不是目标!!
思考如下的愚蠢问题:
把list中的所有值加一
非常快我们可以写出如下的代码:
foo list = map (\x -> x+1) list
这段代码很烦, 但是不得不说比遍历整个list,每一次加1要短多了。 大部分接触过map这类函数的小伙伴们,在python,js中, 这种东西叫做高阶函数,也就是说他可以接受一个函数作为高阶函数的参数来进行运算。就算在c这样不支持闭包,lamba, 高阶函数的语言里,我们也可以通过函数指针的方式来实现。
在编程语言中当我们使用一个变量的时候,这个变量是一定有其类型的, 在Haskell这样类型约束强到几乎是偏执的语言中, 我们代码中的lambda 表达式或者是函数当然也是有类型的。回想一下函数的定义
addOne :: Int -> Int
没错, 在Haskell中,类型的显示声明操作符正是::
, 这里的Int ->Int
就是函数的类型。从之前说的纯函数的概念上来说,函数本质是一种对映射关系的抽象, 所以在haskell中->
正是一种映射操作符。hhhh是不是确实是很固执的纯函数式编程。
好的,这是只有一个参数的函数的情况,思考一下稍微复杂一些的加法函数
(+) :: Int -> Int -> Int
现在的函数有两个参数和一个返回值了,为啥这只有两个->
呢,该怎么区分函数的输入和返回值呢?
在haskell中,或者说许多函数式语言中,函数永远只有一个参数和一个返回值
这种处理就是大名鼎鼎的函数的柯里化函数Curried functions, 如果你是一个js开发者,很可能早就已经在某个高深莫测的文章中见过了。
知道了这一概念之后,让我们重新审视一下加法函数+,没错它只有一个输入Int
, 它返回了一个Int -> Int
。啊!对返回值也可以是函数。
在函数式编程中函数是一等公民
干TvT,这句话真不是白说的。
柯里化以后的函数通过返回函数的方式来实现多参数的调用.现在,有了函数地位和定义方式的概念我们终于可以很自然的写出map的函数定义了。
map :: (a -> b) -> [a] -> [b]
那柯里化有什么好处呢?
现在我们的函数其实变得更加灵活了!无需再定义addOne这种莫名其妙丑陋的东西了直接+1
就可以了,函数+
在这里吃进一个Int 1
之后返回了一个新函数!这被称为不完全调用函数(Partially Applied Functions)Nice!可以把代码写的更短了!
foo list = map (+1) list
-- wait ! foo和map也可以不完全调用!
foo = map (+1)
好了好了,真干净,爽爽爽!
不过话说程序员里有一个很有名的笑话:
有一个苏联特工费劲了千辛万苦,终于偷到了阿波罗登月计划代码的最后一页,代码是用lisp写的。但是当他看到内容的感受到了世界的恶意XD,因为最后一页是
))))))))))))))))))))))))))))))))))))))))))))))·········)))))
(╯‵□′)╯︵┻━┻
函数式编程一般都非常依赖与递归,以lisp为代表的这种S Expression-base的语言都会有比较多的括号。相信写过js回调的人也会有这种感觉,漫天的括号非常难受。haskell也很容易写出很多括号。比如当我们联用高阶函数map
和filter
(请hoogle之,用来过滤list的高阶函数)
sum (filter (> 10) (map (*2) [2..10]))
好多括号啊!haskell这种杂技语言显然不能接受这样的结果!可以用函数f $ x = f x
来实现
sum $ filter (> 10) $ map (*2) [2..10]
效果拔群,函数从左结合变成右结合了,自然就用不到括号了,强迫症爽了嘛?
问题复杂一点
把list中的所有值加一再乘2
作为函数式语言,这种级别的杂技显然不是终点。之前说到了这里的函数和数学里的函数是一样的,所以当两个函数组合的时候有 (f.g)(x) = f(g(x))
,哇,真是并没有什么卵用的功能啊,我们的函数又能写的更炫酷啦(毫无表情)!
map (\x -> (x+1)*2) [2..10]
消灭lambda表达式!!
map ((*2) . (+1)) [2..10]
ok,杂技表演完毕。等下我之前犯了一个错误, 谁说加法只能用来加Int的!所有数应该都可以被加法应用吧!
(+) :: (Num a) => a -> a -> a
像Num这样的约束在haskell中被称为typeclass, 它代表了一种类型可以被某一簇的函数所应用,而可以被应用的函数就定义在typeclass的定义中,这很有用,因为已经有人帮我们写好了很多的轮子,我们只需要让自己的类符合这些typeclass的规范,就可以得到很多可以用的函数啦!比如我希望之前写的child类可以被打印出来,可以被比较,我只需要
data Child = AA | BB | CC | DD deriving (Show, Eq)
easy, 不过这不是今天的重点,后面在介绍更多fancy的东西的时候你将感受到haskell神奇的类型系统。
坑,在搞了这么多杂技之后让我们来康康haskell的函数本身到底有啥不一样的地方吧。
其实确实,由于在函数式编程中,函数的纯度一旦被保证,有很多语言实现上的方式就变得完全不一样了。
- 引用透明Referential transparency 纯函数带来的状态不可变,让我们的编程更加透明了,而且函数的正确性就可以通过类似单元测试的的quickcheck方式来实现了。嗯,对于测试来说是真的友好。
- 天生无锁并发 并发编程和并行编程在现在的业务场景下面确实是越来越多了, 函数式因为函数没有副作用可以毫无顾虑的往多核上扔,但是这确实对于程序员本身有了更加高的要求,生产环境上毕竟哪来的这么多简简单单就能用纯函数来表示的场景呢,而且并发的时候说白了很多时候都是为了处理I/O,又要引入一些比较难理解的工具了。但是在spark, GPU等这些并行应用场景下确实好用。
- 惰性求值lazy evaluation 因为haskell之类的函数式编程是不可变状态的变量,很多时候都是需要重新返回一个新的值而不是去修改那个值,这时候如果是earger的策略显然是不合适的,特别是函数那么多,考虑到栈深度等很多原因,所以在haskell中函数是默认lazy的,只有真实的call了那个值,才会去计算这个值。说白了这里的等号操作符本质就是个绑定,根本没有计算上的特性,根本没有显示的赋值操作。从表达能力上来说其实也有一些考虑,比如可以很简单的表达一个无穷列表([1..])
- 尾递归优化tail recursion optimition 因为递归那么多为了不stack over flow这个显然是必须的。这个在后面应该会单独开文章讲,还有tail call的优化问题。
这只是一些, 这些变化的影响其实导致了编程思路的彻底不一样,原来的算法和数据结构在函数式的情况下都必须做一些不小的改动,还有非常重要的CPS,这感觉也是我这过去半年最大的收获吧,在结束了这两篇对彻底初心者教学的尝试之后也希望自己能真正把后面的内容写好。