最近在学spark,接触到了scala这门编程语言,学到了许多函数式编程的知识,同时也被scala中各种各样的操作符恶心到了:(::、:+、+:、:::)。确实也很有趣。
由于第一门接触到的语言是python,因此在这里写一些关于python的函数式编程相关知识。理解函数式编程相关概念对你阅读代码的能力会很有帮助,有助于帮助你写出更好的代码。同时,理解函数式编程的相关概念理论,也会让你学习其他编程语言时事半功倍。经典的函数式编程语言像是haskell,scala等,大家有兴趣可以去学习一下这些编程语言。
在这篇文章里,笔者只给出了部分函数式编程的内容,如果你很有兴趣,可以去看补充的内容。在那里给出了几个学习函数式编程的很好的项目。
函数是“一等公民”
什么是一等公民呢?当我们说函数是“一等公民”的时候,我们实际上说的是它们和其他对象都一样…所以就是普通公民(坐经济舱的人?)。函数真没什么特殊的,你可以像对待任何其他数据类型一样对待它们——把它们存在数组里,当作参数传递,赋值给变量…等等。
将函数作为参数,或将函数的返回作为参数
demo1:将列表中的整数过滤出来
# 函数作为参数 def filter(func,xs): ls = [] for x in xs: if func(x): ls.append(x) return ls # 函数作为函数的返回值 def is_a(T): def is_T(x): if type(x) is T: # is 操作符是Python语言的一个内建的操作符。它的作用在于比较两个变量是否指向了同一个对象 return True return is_T # 函数作为函数的返回值 print(filter(is_a(int),[0,'1',2,None])) # [0,2]
闭包是访问在其作用域外的变量的一种方式。正式地说,闭包是一种用于实现词法作用域命名绑定的技术。它是存储一个函数和它的环境的一种方法。
闭包是一个作用域,它会捕获函数的局部变量,因此即使执行过程已经移出了定义它的那个代码块,也可以访问它们。也就是说,它们允许在声明变量的代码块已经执行完成之后,还是可以引用这个作用域。
简单来说:如果一个函数,访问到了它的外部(局部)变量的值,那么这个函数和他所处的环境叫做闭包
demo2:
def add(): a = 4 def add_to_five(b): return a+b return add_to_five add_ = add() print(add_(5)) # or print(add()(5)) # 这也叫柯里化,柯里化一定有闭包 def add(a): def add_to_five(b): return a+b return add_to_five print(add(4)(5)) add = lambda x:lambda y:x+y print(add(4)(5))
函数
add()
返回了一个函数(在内部调用了add()
),我们将它保存在了一个叫做add_to_five
的变量中,并且柯里化地用一个参数5来调用它。理想情况下,当函数
add
执行完成后,它的作用域,包括本地变量add(即+),x,y,都应该无法访问了。但是,add_to_five()
的调用返回了8。这说明,add
函数的状态被保存了,即使在代码块已经完成执行之后。否则,就不会知道add
曾经被add(5)
这样调用过,且x的值被设为了5。词法作用域(lexical scoping)是它能找到x和add这两个已经完成执行的父级私有变量的原因。这个值就称为闭包。
将一个多元函数转变为一元函数的过程。 每当函数被调用时,它仅仅接收一个参数并且返回带有一个参数的函数,直到传递完所有的参数。
函数柯里化:把一个参数列表的多个参数,变成多个参数列表(只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。)
demo:
def sum(a,b): return a+b # 柯里化 def curried_sum(a): def curried_sum_(b): return a+b return curried_sum_ print(sum(4,5)) print(curried_sum(4)(5)) curried_sum_ = curried_sum(4) print(curried_sum_(5))
将一个包含多个参数的函数转换成另一个函数,这个函数如果被给到的参数少于正确的数量,就会返回一个接受剩余参数的函数。
from toolz import curry
@curry
def add(a:int,b:int):
return a+b
print(add(1,2))
print(add(1)(2))
print(add(1)) #(y) = 1+y
"部分地"应用一个函数,即预设原始函数的部分参数来创建一个新的函数。
把两个函数放在一起形成第三个函数的行为,一个函数的输入为另一个函数的输出。
demo:
import math def compose(f,g): def compose_(a): return f(g(a)) return compose_ print(compose(str,math.floor)(121.1221))
函数g处理了参数a,将结果给函数f,最终返回结果
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
demo
s = "zhangsan" def f(name): print(f"hi,{name}") f('lisi') # 下面的不是 def f1(): print(f"hi,f{s}") f1() greeting = None def greet(name): global greeting greeting = 'Hi, {}'.format(name) greet('Brianne') greeting # "Hi, Brianne"
如果一个函数或者表达式除了返回一个值之外,还与外部可变状态进行了交互(读取或写入),则它是有副作用的。
def f1():
print(f"hi,f{s}")
f1()
greeting = None
def greet(name):
global greeting
greeting = 'Hi, {}'.format(name)
greet('Brianne')
greeting # "Hi, Brianne"
让我们来仔细研究一下“副作用”以便加深理解。那么,我们在纯函数定义中提到的万分邪恶的副作用到底是什么?“作用”我们可以理解为一切除结果计算之外发生的事情。
“作用”本身并没什么坏处,而且在本书后面的章节你随处可见它的身影。“副作用”的关键部分在于“副”。就像一潭死水中的“水”本身并不是幼虫的培养器,“死”才是生成虫群的原因。同理,副作用中的“副”是滋生 bug 的温床。
副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
副作用可能包含,但不限于:
如果函数执行多次结果还是相同的话,那就是幂等
sorted(sorted([2,1,12,31,42,]))
定义函数时,不显式地指出函数所带参数。这种风格通常需要柯里化或者高阶函数。也叫 Tacit programming。
def map(func):
def map_(xs):
for x in xs:
func(x)
return map_
def add(a):
def add_(b):
return a+b
return add_
print(map(add(1))) # 并没有实际的输出
根据输入返回 true 或 false。通常用在 filter 的回调函数中。
f = lambda a:a>2
print(list(filter(f,[1,2,3,5])))
def throw(ex):
raise ex
def contract(value):
if type(value) is int:
return True
else:
return throw(Exception("Contract violated: expected int -> boolean")) # 自定义异常的抛出
add1 = lambda num: contract(num) and num + 1
print(add1(2)) # 3
print(add1('some string')) # Contract violated: expected int -> boolean)
在范畴论中,范畴是指对象集合及它们之间的态射 (morphism)。在编程中,数据类型作为对象,函数作为态射。
一个有效的范畴遵从以下三个原则:
a
是范畴里的一个对象时,必有一个函数使 a -> a
。a
,b
,c
是范畴里的对象,f
是态射 a -> b
,g
是 b -> c
态射。g(f(x))
一定与 (g • f)(x)
是等价的。f • (g • h)
与 (f • g) • h
是等价的。由于这些准则是在非常抽象的层面控制着组合方式,因此范畴论对于发现组合的新方法来说是伟大的。
数学上,态射(morphism)是两个数学结构之间保持结构的一种过程抽象。
函子是一个实现了 map
函数的对象。map
函数会遍历对象中的每个值并生成一个新的对象,遵守两个准则
形式:
object.map(x => x) ≍ object
形式:
object.map(compose(f, g)) ≍ object.map(g).map(f)
def f(x):
x +=1
return x
def g(x):
x = x*2
return x
result1 = list(map(f,[1,2,3])) # 在这里object放在了map里面
result2 = list(map(f,map(g,[1,2,3])))
print(result1)
print(result2) # [3,5,7]
一个对象,拥有一个of
函数,可以将一个任何值放入它自身。
class Array(list):
of = lambda *args: Array([a for a in args])
print(Array.of(1)) # [1]
如果一个表达式能够被它的值替代而不改变程序的行为,则它是引用透明的。
例如我们有 greet 函数:
greet = lambda x: 'hello, world.'
任何对 greet()
的调用都可以被替换为 Hello World!
, 因此 greet 是引用透明的。
当一个应用程序由表达式组成并且没有副作用时,我们可以从这些组成部分中得知系统的真相。
也许你已经看到了,在前面我们用到了许多匿名函数
f = lambda x:x+1
# 等价于
def f_(x):
return x+1
print(f(2))
print(f_(2))
f2 = lambda x,y: [x(i) for i in y]
print(f2(lambda x:x+1,[1,2,3]))
# 等价于
def f2_(func:Function,y:List) -> List:
tmp_list = []
for i in y:
tmp_list.append(func(i))
return tmp_list
print(f2_(lambda x:x+1,[1,2,3]))
数学的一个分支
推荐一个小视频:https://www.bilibili.com/video/BV1d34y1v7xr?spm_id_from=333.999.0.0&vd_source=660c5a54096b80be102b4f1e8974debe
惰性求值是一种按需调用的求值机制,它将表达式的求值延迟到需要它的值为止,在函数式语言中,允许类似无限列表这样的结构存在,而这在非常重视命令顺序的命令式语言中通常是不可用的。
import random
def rand():
while True:
yield random.randint(1,101) #生成器
randIter = rand()
print(next(randIter))
这里大概介绍了函数式编程的一半知识。之后的某些内容会涉及到群论等数学知识;如果想要获得更多的知识:
https://github.com/llh911001/mostly-adequate-guide-chinese
这是一份使用javascript介绍函数式编程的书籍,内容详细
https://github.com/jmesyou/functional-programming-jargon.py
这是一份介绍python函数式编程的项目,笔者所写大部分来自这个项目,但由于这个项目很多使用 lambda 函数来介绍函数式编程,对于新手而言不太友好。因此在这里改成一些更容易接受的完整函数语句
https://github.com/hemanth/functional-programming-jargon
这是一份包含多种语言函数式编程的项目