初探函数式编程 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
morefreeze
V2EX    编程

初探函数式编程

  •  
  •   morefreeze 2018-04-09 10:45:31 +08:00 1425 次点击
    这是一个创建于 2747 天前的主题,其中的信息可能已经有所发展或是发生改变。

    看了阮一峰的文章入门和一些国外的讲解,感觉对这几个概念理解可能有些偏差,欢迎指出问题,以下是 blog 正文。

    http://morefreeze.github.io/2018/03/peek-functional-programming.html

    最近遇到了一些函数式编程的概念,心想我用 Pythonmap reduce 不就是在函数式编程嘛, 但看了半天仍然一头雾水,什么是 UnitBoxflatMapmap 差在哪里?于是先学了阮老师的函数式编程入门教程, 唉哟喂,和我原来想的还不一样,Python 这些操作虽然算是函数式编程,但要系统地理解为什么能这样, 还得从头说起。

    这里我就从阮老师的这篇文章开始,好在他是用 JS 讲解的,于是我就写个 Python 实现的版本。

    基本概念

    说到函数式编程,肯定都知道“函数是一等公民”这条公理,但这其中少了许多细节。

    1. 这个函数只能接受一个参数,并返回一个值
    2. 不满足 1 条件的函数可以通过柯里化(curry)来变形成符合 1 条件的
    3. 函数之间要满足结合律

    翻译成程序员理解的话就是说,函数要没有副作用,比如修改全局变量,传多个参数, 这些都是禁止的。

    柯里化(curry)

    柯里化简单来说就是把接受多个参数的函数通过“俄罗斯套娃”的形式,展成多个函数调用的形式, 每个函数只处理一个函数,就像这样:

    def add(x, y): return x + y def curry_add(x): return lambda y: x + y curry_add(2)(3) # 5 

    显然把好端端的函数都重写一遍挺费劲的,可以用装饰器来简化这一过程,代码受 pymonad 的启发:

    def curry(f): num_args = f.__code__.co_argcount def wrap(args, num_args): if num_args == 0: return f(*args) # 每次返回一个只接受一个参数的函数 return lambda x: wrap(args + [x], num_args - 1) return wrap([], num_args) @curry def add(x, y): return x + y add(2)(3) # 5 

    上面 curry 装饰器直接返回 wrap 的调用,而这个 wrap 的函数总是返回一个接受一个参数的函数的调用, 比如装饰 add 函数,第一层返回一个 add1(x) 的函数调用,第二层就返回一个 add2(y) 的调用,而到第三层,因为 num_args == 0,直接调用 add(x, y) 将前面的所有参数一起传给 add,最终执行 return x + y,得到计算结果。

    函子(Functor)

    下面轮到函子出场啦,这是基本的运算单位和功能单位。规定凡是实现了 map 方法的都是函子。

    听名字有点像函数,但从应用上来看,它表示的是一些我们熟悉的数据类型,比如 int, string 等, 这些类型可以应用 map 操作,比如给一个数翻倍,将一串字符串变成大写。

    让我们实现一个简单的函子:

    class Functor(object): def __init__(self, val): self.val = val def map(self, f): return self. of(f(self.val)) def __repr__(self): return '%s(%s)' % (self.__class__.__name__, self.val) @classmethod def of(cls, *argv, **kwargv): return cls(*argv, **kwargv) Functor.of(2).map(lambda x: x * 2) # Functor(4) Functor.of("foobar").map(lambda x: x.upper) # Functor(FOOBAR) Functor.of(2).map(lambda x: x * 2).map(lambda x: x * 3) # Functor(12) 

    函子只有一个成员 val, 就是用来存储各种数据类型的,然后有一个 map 方法,这个方法接受一个函数运算作为参数,将这个函数应用在 self.val 上, 但要注意返回的仍然是一个函子,这样后面才能继续应用 map 进行操作, 也就是说支持链式操作。

    另外 __repr__ 函数只是为了方便打印调试用的。

    注意到还有一个类方法 of,这是因为如果直接用 Functor() 来初始化不像函数式编程, 所以一般约定使用 of 来生成新的对象。

    可以看到三个例子展示了map操作,注意到第 3 个例子是链式写法,当然这是建立在这些函数都没有副作用的前提下,稍后将会看到这种写法的局限性。

    Maybe 函子

    编程中经常遇到一种情况是一个成员初始值赋为 nullNone,之后才有可能赋为它的类型的值, 在之后的函数处理中,如果用到这个值,经常会看到 if (foo == null)if foo is None 的条件语句来做边界处理, 这就比较烦,所以这时要用到 Maybe 函子,它的定义如下:

    class Maybe(Functor): def map(self, f): return self.of(f(self.val) if self.val else None) Maybe.of('foobar').map(lambda s: s.upper()) # Maybe(FOOBAR) Maybe.of(None).map(lambda s: s.upper()) # Maybe(None) 

    只是在实现的 map 中判断下值是否为空,再根据情况处理即可,其实就是把函数中要进行的判断放到 Maybe 函子中判断了。这个其实很像 rust 语言中的 Option

    ap 函子

    Functor 只能传数据类型,再应用接受一个参数的函数,那对于已经柯里化的多参数函数怎么调用呢, 这时就用到了 ap 函子,ap 是 applicative (应用)的缩写。只要实现 ap 方法就行。

    class Ap(Functor): def ap(self, F): return Ap.of(self.val(F.val)) Ap.of(lambda x: x + 2).ap(Maybe.of(2)) # Ap(4) @curry def add(x, y): return x + y Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3)) # Ap(5) 

    注意到 ap 函子和 Functor (或 Maybe ) 的不同是它用一个函数(而不是一个数据)初始化,然后将函数应用(apply)在后面的数据上。

    Monad 函子

    Functor.map 是接受一个普通的函数,这个普通函数接受一个普通值并返回一个普通值,那如果一个函数中可能出现异常导致需要返回一个空值,这时一般我们会让这个函数返回一个封装值,比如这样:

    def tryParse(s): try: return Maybe.of(int(s)) except ValueError: return Maybe.of(None) Maybe.of('42').map(tryParse) # Maybe(Maybe(42)) Maybe.of('foo').map(tryParse) # Maybe(Maybe(None)) Maybe.of('42').map(tryParse).map(tryParse) # TypeError 

    注意到经过 map 后的值多套了一层 Maybe,这样就没法再用链式写法了, 那怎么办呢,发现 map 函数接受的是一个普通值,那么只要让进出一致,就又可以愉快地用链式写法了, 也就是说新 map 接受一个封装值并返回一个封装值,里面要做的工作是去掉封装传给真正的处理函数, 我们管这种既能包普通值,又能包函数的类型叫 Monad,新 map 方法 叫 flat_map,这个方法就是把封装的值展开(flat)再应用上 map, 就像这样:

    class Monad(Fuctor): def join(self): return self.val def flat_map(self, f): return self.map(f).join() def half(x): try: return Monad.of(x / 2 if x % 2 == 0 else None) except TypeError: return Monad.of(None) Monad.of(4).flat_map(half) # Monad(2) Monad.of(3).flat_map(half) # Monad(None) Monad.of(4).flat_map(half).flat_map(half) # Monad(1) Monad.of(3).flat_map(half).flat_map(half) # Monad(None) 

    可以看到给出的后两个例子支持了链式操作,并且如果函数处理得当,对 None 值也做了处理。

    但如果已经有了一些纯函数,是不是还要都改成返回 Monad 的类型呢, 肯定不啦,可以用装饰器轻松达到这个目的,就像这样:

    def monadize(f): def _(p): return Monad.of(f(p)) return _ @monadize @curry def add3(x): return x + 3 Monad.of(2).flat_map(add3).flat_map(add3) # Monad(8) 

    小结

    这篇文章简单讲了函数式编程的 4 个基础概念:Functor, May, ApMonad,并用 Python 简单实现了下。

    1. Functor 用来包装数据类型,这里的值可以是数字,字符串,也可以是复杂对象,调用 map 方法将函数应用在包装的数据上
    2. Maybe 可以将表示空值的 None 作为值,并且不会对 None 进行操作
    3. Ap 可以将多参数的函数包装,调用 ap() 不断将参数填充到函数里并求值
    4. Monad 使用 flat_map 接受一个返回封装值的函数,并将函数的返回值取出,将多层封装展开

    下期预告

    下期将会介绍函数式编程最典型的三种 Monad,有了它们,就可以在函数式编程的海洋里浪了, 所以如果你还没掌握 Monad,可以回头再看一看上面的文章,或者直接在下面留言给我 (也许需要梯子才能加载出留言)。

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1080 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 23:20 PVG 07:20 LAX 16:20 JFK 19:20
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86