Python 函子和应用型函子,函子指的是简单数据的函数式表示。数字3.14
的函子版本是一个返回该值的零参数函数。示例如下:
>>> pi = lambda: 3.14
>>> pi()
3.14
这样就创建了一个零参数匿名函数对象并返回了一个简单的值。
当对函子应用柯里化函数时,会创建一个新的柯里化函子。可以将这一概念概括为:通过使用函数来表示参数、值和函数本身,可以将函数应用于参数来获取值。
一旦程序中的所有内容都是函数,那么所有运算都只是函数式复合模型的一个变体。柯里化函数的参数和结果都可以是函子。有时可以对functor
对象使用getValue()
方法,以获得可用于非柯里化代码且适用于Python的简单类型。
由于此时编程是基于函数式复合的,因此在实际使用getValue()
方法请求一个值之前,无须进行任何计算。程序不会执行大量中间计算,而会定义复杂的中间对象以根据请求生成所需的值。原则上,更智能的编译器或运行时系统能优化这种复合。
当把函数应用于functor
对象时,我们会使用一个类似于map()
的方法,相当于*
运算符。可以通过function * functor
或map(function, functor)
方法理解函子在表达式中的作用。
为了恰当地处理具有多个参数的函数,可以使用 &
运算符构建复合函子。functor & functor
方法常用于从一对函子中构建出一个functor
对象。
可以使用Maybe
函子的子类封装Python的简单类型。函子Maybe
的有趣之处在于可以用它恰当地处理缺失数据。第11章采用的方法是将内置函数装饰为可感知None
值的函数。PyMonad库采用的方法是装饰数据,以将其排除在外。
函子Maybe
有以下两个子类:
Nothing
Just(某个简单值)
使用Mothing
来代替Python中的简单值None
,以此表示缺失数据。使用Just(某个简单值)
来封装其他所有Python对象。这些函子是常数值的类函数表达形式。
可以对这些Maybe
对象使用柯里化函数来恰当地处理缺失数据,示例如下:
>>> x1 = systolic_bp * Just(25) & Just(50) & Just(1) & Just(0)
>>> x1.getValue()
116.09
>>> x2 = systolic_bp * Just(25) & Just(50) & Just(1) & Nothing
>>> x2.getValue() is None
True
运算符*
复合了带参数复合的systolic_bp()
函数。运算符&
构建了一个复合函子,可以将该函子作为参数传递给多参数的柯里化函数。
这表明得到的是一个值,而不是一个TypeError
异常。在处理可能存在缺失数据或无效数据的大型复杂数据集时,这样处理会很方便,比把所有函数都装饰为可感知None
值要好得多。
这种做法对柯里化函数非常有效。由于函子的方法非常少,因此不能在非柯里化Python代码中处理Maybe
函子。
对于非柯里化Python代码,必须使用
getValue()
方法来获取简单的Python值。
使用惰性List()
函子
刚接触函子List()
的人可能会感到困惑,不同于Python内置的list
类型,它是非常“懒惰的”。当对内置的list(range(10))
方法求值时,list()
函数会对range()
对象求值来创建一个包含10个项的列表。然而,PyMonad的List()
函子“懒惰”到甚至不会进行这种计算。
下面比较两者:
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> List(range(10))
[range(0, 10)]
函子List()
没有对range()
对象进行求值,它只是在未求值的情况下先保留了该对象。对于收集但不求解函数的情况,函数pymonad.List()
十分有用。
其中range()
的使用可能会令人困惑。Python 3中的range()
对象同样也是惰性的,因此示例中便包含两层延后。pymonad.List()
会根据需求创建数据项。List
中的每一项都是一个可被求值并生成一个值序列的range()
对象。
可以根据之后的需求对List
函子进行求值:
>>> x = List(range(10))
>>> x
[range(0, 10)]
>>> list(x[0])
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
这样就创建了一个包含range()
对象的惰性List
对象。随后提取并求解了列表中0
位置的一个range()
对象。
List
对象不会对生成器函数或range()
对象进行求值,它将任何可迭代参数都视为单个可迭代对象。然而,我们可以使用*
运算符来扩展生成器或range()
对象的值。
请注意,
*
运算符有几种含义:它是内置的数学乘法运算符、PyMonad定义的函数式复合运算符,以及在调用函数时将单个序列对象绑定为函数所有位置参数的内置修饰符。下面将使用它的第三种含义将一个序列赋给多个位置参数。
range()
函数的柯里化版本如下,其下界是1
而不是0
。对于某些数学运算,这样做会比较方便,因为可以避免内置range()
函数中位置参数的复杂性。
@curry
def range1n(n):
if n == 0: return range(1, 2) # Only the value 1
return range(1, n+1)
这样就简单地封装了内置的range()
函数,并通过PyMonad包将其柯里化了。
由于List
对象是一个函子,因此可以将函数映射到List
对象上。
将函数应用于List
对象中的每一项,示例如下:
>>> fact= prod * range1n
>>> seq1 = List(*range(20))
>>> f1 = fact * seq1
>>> f1[:10]
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
这样就定义了一个复合函数fact()
,它由前面的prod()
函数和range1n()
函数构建而来,它是一个阶乘函数;并且创建了一个List()
函子seq1
,它是一个包含20个值的序列;还将fact()
函数映射至了seq1
函子,从而创建了一个阶乘值的序列f1
;还查看了前10个值。
多个函数的复合与函数和函子的复合之间存在一定的相似性。
prod*range1n
和fact*seq1
都使用函数式复合,显然前者复合的都是函数,而后者复合的是函数和函子。
用于扩展该示例的另一个小函数如下:
@curry
def n21(n):
return 2*n+1
该n21()
小函数执行了一个小型计算。然而由于它是柯里化的,因此可以将其应用于一个函子,如List()
函数。上述示例的第二部分如下所示:
>>> semi_fact= prod * alt_range
>>> f2 = semi_fact * n21 * seq1
>>> f2[:10]
[1, 3, 15, 105, 945, 10395, 135135, 2027025, 34459425, 654729075]
这样就通过此前的prod()
函数和alt_range()
函数定义了一个复合函数。函数f2
是一个半阶乘(双阶乘)函数。通过将小函数n21()
映射到seq1
序列来构建函数f2
的值,便创建了一个新的序列。随后将semi_fact
函数应用于这个新序列,创建出了与原序列值对应的一组序列值。
接下来可以将/
运算符映射至map()
和operator.truediv
并行算子了。
>>> 2*sum(map(operator.truediv, f1, f2))
3.1415919276751456
函数map()
会将给定的运算符应用于两个算子,并生成一个可新增分数的序列。
方法
f1 & f2
会基于两个List
对象创建出值的所有组合。这是List
对象的重要特性之一——易于枚举所有组合,这使得通过简单的算法便能计算和过滤出所有可行的备选子集。但这并不是我们想要的,这就是使用map()
函数而不是operator.truediv * f1 & f2
方法的原因。
我们利用一些函数式复合技术和一个函子的类定义,定义了一个相当复杂的计算。该计算的完整定义如下:
理想情况下,我们不希望使用固定大小的List
对象,而希望有一个惰性求值的、潜在无穷整型数值序列。之后可以使用sum()
函数和takewhile()
函数的柯里化版本计算序列值的总和,直到这些值小到不会对结果产生实在影响。这就需要一个比List()
对象惰性更强的版本,以配合itertools.counter()
函数使用。PyMonad 1.3中没有这样的潜在无穷列表,因此只能使用固定大小的List()
对象。