monad 允许我们用一种宽松的语言对表达式求值施加命令。我们可以使用单子来坚持像a+b+c这样的表达式是按从左到右的顺序计算的。这可能会干扰编译器优化表达式计算的能力。例如,当我们希望文件的内容以特定顺序读取或写入时,这是必要的:monad 确保以特定顺序对read()和write()函数进行求值。
宽松且具有优化编译器的语言受益于 monad 对表达式求值施加的顺序。Python 在很大程度上是严格的,不进行优化。对单子没有实际要求。
然而,PyMonad 包包含的不仅仅是 monad。有许多函数式编程功能具有独特的实现。在某些情况下,PyMonad 模块可以生成比仅使用标准库模块编写的程序更简洁、更具表现力的程序。
在本章中,我们将了解以下内容:
- 下载和安装 PyMonad
- 咖喱的概念以及它如何应用于函数式构图
- 用于创建复合函数的 PyMonad 星形运算符
- 函子和将数据项转换为更广义函数的技术
bind()操作,使用>>创建有序单子- 我们还将解释如何使用 PyMonad 技术构建蒙特卡罗模拟
PyMonad 包可在Python 包索引(PyPi中找到)。为了将 PyMonad 添加到您的环境中,您需要使用pip。
访问https://pypi.python.org/pypi/PyMonad 了解更多信息。
对于 Mac OS 和 Linux 开发人员,pip install pymonad命令可能需要sudo命令作为前缀。如果您已经安装了个人版的 Python,则无需使用sudo。如果您已经在系统范围内安装了 Python,那么您将需要sudo。当运行诸如sudo pip install pymonad之类的命令时,系统将提示您输入密码,以确保您具有执行安装所需的管理权限。对于 Windows 开发人员,sudo命令与此无关,但您确实需要具有管理权限。
安装 PyMonad 软件包后,可以使用以下命令进行确认:
>>> import pymonad
>>> help(pymonad) 这将显示docstring模块,并确认设备安装正确。
整个项目名 PyMonad 使用混合大小写。我们导入的已安装 Python 包名 PyMonad 都是小写的。目前,PyMonad 包中没有类型提示。这些特性的非常通用的性质要求广泛使用TypeVar提示来描述各种功能的特征。此外,PyMonad 中有一些名称与内置typing模块中的名称冲突;这使得类型提示的语法可能更加复杂,因为消除重载名称的歧义需要包名。我们将省略本章中示例的类型提示。
一些函数式语言通过将多参数函数语法转换为单参数函数的集合来工作。这个过程被称为Curry:它是以逻辑学家哈斯克尔·库里(Haskell Curry)的名字命名的,哈斯克尔·库里从早期的概念发展了这一理论。
**Curry 是一种将多参数函数转换为高阶单参数函数的技术。在一个简单的情况下,考虑函数 Ty0T0;给出两个参数x和y;这将返回一些结果值z。我们可以将
分为两个功能:
和
。给定第一个参数值x,对函数
求值将返回一个新的单参数函数
。第二个函数可以被赋予第二个参数值y,并返回所需结果z。
我们可以用 Python 对一个 curried 函数求值如下:f_c(2)(3)。我们将 curried 函数应用于2的第一个参数值,创建一个新函数。然后,我们将该新函数应用于第二个参数值3。
这适用于任何复杂的功能。如果我们从一个函数
开始,我们将其转换为一个函数
。这是递归完成的。首先,对函数
的求值返回一个新函数,其中包含b和c参数
。然后我们可以利用返回的双参数函数创建
。
我们可以用g_c(1)(2)(3)来评估复杂的 curried 函数。这种形式语法非常庞大,因此我们使用一些语法糖将g_c(1)(2)(3)简化为更容易接受的东西,如g(1, 2, 3)。
让我们看看 Python 中的一个具体示例。例如,我们有一个如下函数:
from pymonad import curry
@curry
def systolic_bp(bmi, age, gender_male, treatment):
return (
68.15+0.58*bmi+0.65*age+0.94*gender_male+6.44*treatment
)这是一个简单的、基于多元回归的收缩压模型。根据体重指数(BMI)、年龄、性别(1 表示男性)和既往治疗史(1 表示既往治疗)预测血压。有关模型及其衍生方式的更多信息,请访问:http://sphweb.bumc.bu.edu/otlt/MPH-Modules/BS/BS704_Multivariable/BS704_Multivariable7.html 。
我们可以将systolic_bp()函数与所有四个参数一起使用,如下所示:
>>> systolic_bp(25, 50, 1, 0)
116.09
>>> systolic_bp(25, 50, 0, 1)
121.59 体重指数为 25、年龄为 50 岁且未接受过治疗的男性,其血压预计接近 116。第二个例子显示了一位有治疗史的类似女性,其血压可能为 121。
因为我们使用了@curry装饰器,所以我们可以创建类似于部分应用函数的中间结果。请查看以下命令片段:
>>> treated = systolic_bp(25, 50, 0)
>>> treated(0)
115.15
>>> treated(1)
121.59在前面的例子中,我们评估了创建 curried 函数的systolic_bp(25, 50, 0)方法,并将其分配给treated变量。特定患者的 BMI、年龄和性别值通常不会改变。现在,我们可以将新的treated函数应用于剩余参数,以根据患者病史获得不同的血压期望值。
这在某些方面与functools.partial()函数类似。重要的区别在于,curry 创建的函数可以以多种方式工作。functools.partial()函数创建了一个更专门的函数,该函数只能与给定的一组绑定值一起使用。
下面是创建一些附加 curried 函数的示例:
>>> g_t= systolic_bp(25, 50)
>>> g_t(1, 0)
116.09
>>> g_t(0, 1)
121.59 这是基于我们最初模型的基于性别的治疗功能。我们必须提供性别和治疗价值,以从模型中获得最终价值。
虽然使用普通函数进行 curry 很容易可视化,但当我们将 curry 应用于高阶函数时,实际值就会显示出来。在理想情况下,functools.reduce()功能是可转换的,因此我们可以执行以下操作:
sum = reduce(operator.add)
prod = reduce(operator.mul)然而,这不起作用。内置的reduce()函数不能通过使用 PyMonad 库来实现,因此上面的示例实际上不起作用。但是,如果我们定义自己的reduce()函数,我们就可以像前面所示的那样对其进行处理。
以下是一个家用reduce()功能的示例,可如前所示使用:
from collections.abc import Sequence
from pymonad import curry
@curry
def myreduce(function, iterable_or_sequence):
if isinstance(iterable_or_sequence, Sequence):
iterator= iter(iterable_or_sequence)
else:
iterator= iterable_or_sequence
s = next(iterator)
for v in iterator:
s = function(s,v)
return smyreduce()函数的行为类似于内置的reduce()函数。myreduce()函数用于 iterable 或 sequence 对象。给定一个序列,我们将创建一个迭代器;给定一个 iterable 对象,我们将简单地使用它。我们使用迭代器中的第一项初始化结果。我们将函数应用于进行中的总和(或乘积)和每个后续项。
It's also possible to wrap the built-in reduce() function to create a curryable version. That's only two lines of code; an exercise left for the reader.
由于myreduce()函数是一个 curried 函数,我们现在可以使用它基于我们的高阶函数myreduce()创建函数:
>>> from operator import add
>>> sum = myreduce(add)
>>> sum([1,2,3])
6
>>> max = myreduce(lambda x,y: x if x > y else y)
>>> max([2,5,3])
5 我们使用应用于add运算符的 curried reduce 定义了自己版本的sum()函数。我们还使用一个拾取两个值中较大值的lambda对象定义了自己版本的默认max()函数。
我们无法通过这种方式轻松创建更一般形式的max()函数,因为 currying 侧重于位置参数。尝试使用key=关键字参数会增加太多的复杂性,使该技术无法实现我们的总体目标,即简洁且富有表现力的功能程序。
为了创建更通用的max()函数版本,我们需要在max()、
min()和sorted()等函数所依赖的key=关键字参数范式之外加入
。我们必须接受高阶函数作为第一个参数,就像filter()、map()和reduce()函数一样。我们还可以创建自己的更一致的高阶 curried 函数库。这些功能将完全依赖于位置参数。首先提供高阶函数,这样我们自己的 curriedmax(function, iterable)方法将遵循map()、filter()和functools.reduce()函数设置的模式。
我们可以手动创建 curried 函数,而无需使用 PyMonad 库中的 decorator;执行此操作的一种方法显示在下面的函数定义中:
def f(x, *args):
def f1(y, *args):
def f2(z):
return (x+y)*z
if args:
return f2(*args)
return f2
if args:
return f1(*args)
return f1这会将一个函数
转换为一个函数f(x),该函数返回一个函数。在概念上,
。然后我们使用中间函数来创建f1(y)和f2(z)函数
。
当我们计算f(x)函数时,我们将得到一个新函数f1。如果提供了其他参数,则这些参数将传递给f1函数进行求值,从而产生最终值或另一个函数。
显然,这种实现 curry 的手动扩展可能容易出错。这不是处理函数的实用方法。然而,它可以用来说明咖喱的含义以及它是如何在 Python 中实现的。
curried 函数的一个重要价值是能够通过函数组合将它们组合起来。我们在第 5 章、高阶函数和第 11 章、装饰设计技巧中查看了功能组合。
当我们创建了一个 curried 函数时,我们可以更容易地执行函数组合来创建一个新的、更复杂的 curried 函数。在本例中,PyMonad 包定义了用于组合两个函数的*操作符。为了解释这是如何工作的,我们将定义两个可以组合的 curried 函数。首先,我们将定义一个计算产品的函数,然后我们将定义一个计算特定值范围的函数。
这是我们的第一个函数,用于计算产品:
import operator
prod = myreduce(operator.mul) 这是基于我们之前定义的 curriedmyreduce()函数。它使用operator.mul()函数计算一个 iterable 的倍缩减量:我们可以将一个乘积称为序列的一倍缩减量。
下面是第二个 curried 函数,它将生成一系列值:
@curry
def alt_range(n):
if n == 0:
return range(1, 2) # Only the value [1]
elif n % 2 == 0:
return range(2, n+1, 2)
else:
return range(1, n+1, 2)alt_range()函数的结果将是偶数值或奇数值。如果n为奇数,则它将只有高达(包括)n的奇数值。如果n为偶数,则只有不超过n的偶数值。序列对于实现半阶乘或双阶乘函数
非常重要。
下面是我们如何将prod()和alt_range()函数组合成一个新的 curried 函数:
>>> semi_fact = prod * alt_range
>>> semi_fact(9)
945这里的 PyMonad*操作符将两个函数组合成一个复合函数,名为semi_fact。alt_range()函数应用于参数。然后,将prod()函数应用于alt_range函数的结果。
使用 PyMonad*操作符相当于创建一个新的lambda对象:
semi_fact = lambda x: prod(alt_range(x)) 与创建新的lambda对象相比,curried 函数的组合所涉及的语法要少一些。
理想情况下,我们希望能够使用如下函数组合和 curry 函数:
sumwhile = sum * takewhile(lambda x: x > 1E-7)这可以定义一个版本的sum()函数,该函数可以处理无限序列,在满足阈值时停止生成值。这实际上不起作用,因为 PyMonad 库似乎不像处理内部List对象那样处理无限多个可重用对象。
函子的概念是一段简单数据的函数表示。数值3.14的函子版本是返回此值的零参数函数。考虑下面的例子:
>>> pi = lambda: 3.14
>>> pi()
3.14我们创建了一个零参数lambda对象,它返回一个简单的值。
当我们将一个 curried 函数应用于一个函子时,我们正在创建一个新的 curried 函子。这概括了将函数应用于参数以通过使用函数表示参数、值和函数本身来获取值的思想。
一旦我们的程序中的一切都是函数,那么所有的处理都只是函数组合主题的一个变体。curried 函数的参数和结果可以是函子。在某个时刻,我们将对functor对象应用getValue()方法,以获得一个 Python 友好的简单类型,我们可以在未传输的代码中使用该类型。
由于编程是基于函数组合的,因此在我们使用getValue()方法实际需要一个值之前,不需要进行任何计算。我们的程序没有执行大量中间计算,而是定义了中间复杂对象,这些对象可以在请求时生成值。原则上,这种组合可以通过一个聪明的编译器或运行时系统进行优化。
当我们将函数应用于functor对象时,我们将使用类似于map()的方法,该方法被实现为*操作符。我们可以将function * functor或map(function, functor)方法视为理解函子在表达式中所起作用的一种方式。
为了礼貌地处理具有多个参数的函数,我们将使用&操作符构建复合函子。我们经常会看到使用functor & functor方法从一对函子构建functor对象。
我们可以用Maybe函子的子类包装 Python 简单类型。Maybe函子很有趣,因为它为我们提供了一种优雅地处理缺失数据的方法。我们在第 11 章装饰设计技巧中使用的方法是装饰内置功能,使其None意识到。PyMonad 库采取的方法是修饰数据,使其优雅地拒绝被操作。
Maybe函子有两个子类:
NothingJust(some simple value)
我们使用Nothing作为简单 Python 值None的替代。这就是我们表示缺失数据的方式。我们使用Just(some simple value)包装所有其他 Python 对象。这些函子是常量值的函数式表示。
我们可以对这些Maybe对象使用 curried 函数来优雅地容忍丢失的数据。下面是一个简短的例子:
>>> 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()函数。&操作符构建一个复合函子
,该函子可以作为参数传递给多个参数的 curried 函数。
这表明我们得到的是一个答案,而不是一个TypeError异常。在处理可能丢失或无效数据的大型复杂数据集时,这非常方便。这比装饰我们所有的功能让它们None意识到要好得多。
这对于 curried 函数非常有效。我们不能在未编译的 Python 代码中对Maybe函子进行操作,因为函子的方法很少。
We must use the getValue() method to extract the simple Python value for uncurried Python code.
List()函子一开始可能会令人困惑。它非常懒惰,不像 Python 的内置list类型。当我们评估内置的list(range(10))方法时,list()函数将评估range()对象,以创建一个包含 10 项的列表。然而,PyMonadList()函子甚至懒得做这个评估。
比较如下:
>>> 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 3range()对象也是懒惰的。在这种情况下,有两层懒惰。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] 我们创建了一个懒惰的List对象,其中包含一个range()对象。然后,我们提取并评估该列表中位置0处的range()对象。
List对象不会评估生成器函数或range()对象;它将任何 iterable 参数视为单个迭代器对象。但是,我们可以使用*操作符来展开生成器或range()对象的值。
Note that there are several meanings for the * operator: it is the built-in mathematical times operator, the function composition operator defined by PyMonad, and the built-in modifier used when calling a function to bind a single sequence object as all of the positional parameters of a function. We're going to use the third meaning of the * operator to assign a sequence to multiple positional parameters.
下面是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 个。
There is a similarity between the composition of functions and the composition of a function and a functor. Both prod*range1n and fact*seq1 use functional composition: one combines things that are obviously functions, and the other combines a function and a functor.
下面是我们将用于扩展此示例的另一个小函数:
@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是半阶乘或双阶乘。函数f2的值是通过将我们的小n21()函数映射到seq1序列来建立的。这将创建一个新序列。然后,我们将semi_fact函数应用于这个新序列,以创建与值序列平行的值序列。
我们现在可以将/操作符映射到map()和operator.truediv并行函子:
>>> 2*sum(map(operator.truediv, f1, f2))
3.1415919276751456 map()函数将给定的运算符应用于两个函子,产生一系列我们可以添加的分数。
The f1 & f2 method will create all combinations of values from the two List objects. This is an important feature of List objects: they readily enumerate all combinations allowing a simple algorithm to compute all alternatives and filter the alternatives for the proper subset. This is something we don't want; that's why we used the map() function instead of the operator.truediv * f1 & f2 method.
我们使用一些函数合成技术和函子类定义定义了一个相当复杂的计算。以下是此计算的完整定义:
理想情况下,我们不喜欢使用固定大小的List对象。我们更希望有一个惰性的、可能无限的整数值序列。然后,我们可以使用sum()和takewhile()函数的通用版本来查找序列中的值之和,直到值太小而无法产生结果。这将需要一个更懒惰的List()对象版本,它可以与itertools.counter()函数一起工作。Pymonad1.3 中没有这个潜在的无限列表;我们仅限于一个固定大小的List()对象。
PyMonad 库的名称来源于monad的函数编程概念,这是一个具有严格顺序的函数。许多函数式编程背后的基本假设是函数式评估是自由的:它可以根据需要进行优化或重新排列。monad 提供了一个异常,它强制执行从左到右的严格顺序。
正如我们所看到的,Python 是严格的。它不需要单子。然而,我们仍然可以在有助于澄清复杂算法的地方应用该概念。
强制严格求值的技术是 monad 和将返回 monad 的函数之间的绑定。平面表达式将成为嵌套绑定,优化编译器无法对其重新排序。bind()函数映射到>>操作符,允许我们编写如下表达式:
Just(some file) >> read header >> read next >> read next前面的表达式将转换为以下表达式:
bind(
bind(
bind(Just(some file), read header),
read next),
read next)bind()函数确保在对表达式求值时对其施加严格的从左到右求值。另外,请注意,前面的表达式是函数组合的示例。当我们使用>>操作符创建 monad 时,我们正在创建一个复杂的对象,当我们最终使用getValue()方法时,将对其进行评估。
Just()子类是创建一个简单的 monad 兼容对象所必需的,该对象封装了一个简单的 Python 对象。
monad 概念对于在一种高度优化和宽松的语言中表达严格的评估顺序至关重要。Python 不需要 monad,因为它使用从左到右的严格求值。这使得 monad 很难演示,因为它在 Python 上下文中并没有真正做一些完全新颖的事情。事实上,monad 冗余地声明了 Python 遵循的典型严格规则。
在其他语言中,如 Haskell,monad 对于需要严格排序的文件输入和输出至关重要。Python 的命令式模式很像 Haskelldo块,它有一个隐式 Haskell>>=操作符来强制按顺序计算语句。(PyMonad 使用bind()函数和>>操作符进行 Haskell 的>>=操作。)
monad 将通过一种管道:monad 将作为参数传递给函数,类似的 monad 将作为函数的值返回。这些函数必须设计为接受和返回类似的结构。
我们将看一个简单的管道,它可以用于流程模拟。这种模拟可能是蒙特卡罗模拟的一个正式部分。我们将从字面上理解 Monte Carlo simulation(蒙特卡罗模拟),并模拟赌场的骰子游戏,掷骰子。这涉及到一些用于相当复杂模拟的有状态规则。
这里涉及到一些奇怪的赌博术语,对此我们深表歉意。掷骰子包括某人掷骰子(投手)和其他投注者。游戏分为两个阶段:
- 第一卷骰子被称为出卷。有三个条件:
- 如果骰子总数为 7 或 11,射手获胜。任何在通行证线上下注的人都将作为赢家获得报酬,其他所有下注都将失败。游戏结束了,射击手可以再次比赛了。
- 如果骰子总数为 2、3 或 12,射手将失败。任何在不通过线上下注的人都会赢,而所有其他下注都会输。游戏结束了,投手必须把骰子传给另一个投手。
- 任何其他总计(即 4、5、6、8、9 或 10)建立一个点。游戏状态从出来掷骰变为点掷骰。游戏还在继续。
- 一旦建立一个点,骰子的每个点掷骰将使用三个条件进行评估:
- 如果骰子总数为 7,射手将输。几乎所有的赌注都是输家,除了在不通过线和一个模糊的命题赌注上的赌注。比赛结束了。由于投手输了,骰子被传递给另一个投手。
- 如果骰子总数达到原来的点数,射手获胜。任何在通行证线上下注的人都将作为赢家获得报酬,其他所有下注都将失败。游戏结束了,射击手可以再次比赛了。
- 任何其他总数都将继续游戏,没有任何解决方案。一些命题赌注可能在这些中间卷上赢或输。
这些规则可以被视为需要改变状态。或者,我们可以将其视为一系列操作,而不是状态更改。有一个函数必须首先使用。然后使用另一个递归函数。这样,这种函数对方法非常适合 monad 设计模式。
实际上,赌场允许在游戏期间进行大量相当复杂的命题下注。我们可以从游戏的基本规则中分别评估这些。许多赌注(命题、现场赌注和购买号码)都是在游戏的掷分阶段进行的。我们将忽略这一额外的复杂性,将重点放在中心游戏上。
我们需要一个随机数源:
import random
def rng():
return (random.randint(1,6), random.randint(1,6))前面的函数将为我们生成一对骰子。
以下是我们对整个比赛的期望:
def craps():
outcome = (
Just(("", 0, [])) >> come_out_roll(dice)
>> point_roll(dice)
)
print(outcome.getValue())我们创建一个初始 monad,Just(("",0, [])),以定义我们将要使用的基本类型。游戏将生成一个三元组,其中包含结果文本、分值和一系列掷骰。在每个游戏开始时,默认的三元组建立三元组类型。
我们将这个单子传递给另外两个函数。这将创建一个带有游戏结果的单子outcome。我们使用>>操作符按照必须执行的特定顺序连接函数。在使用优化编译器的语言中,这将防止重新排列表达式。
最后我们使用getValue()方法得到单子的值。由于 monad 对象是惰性的,因此该请求触发对各种 monad 的求值以创建所需的输出。
come_out_roll()函数的第一个参数是rng()函数。monad 将成为该函数的第二个参数。come_out_roll()功能可以掷骰子并应用出来规则来确定我们是赢了、输了还是得了分。
point_roll()函数还有rng()函数作为第一个参数。单子将成为第二个参数。然后,point_roll()功能可以掷骰子,查看赌注是否解决。如果下注未解决,此函数将递归运行以继续查找解决方案。
come_out_roll()函数如下所示:
@curry
def come_out_roll(dice, status):
d = dice()
if sum(d) in (7, 11):
return Just(("win", sum(d), [d]))
elif sum(d) in (2, 3, 12):
return Just(("lose", sum(d), [d]))
else:
return Just(("point", sum(d), [d]))我们掷骰子一次,以确定第一次掷骰是否会赢、输或确定点数。我们返回一个适当的 monad 值,其中包括结果、分值和掷骰子。即时赢和即时输的分值并没有真正意义。我们可以在这里合理地返回一个0值,因为没有真正确定任何点。
point_roll()函数如下所示:
@curry
def point_roll(dice, status):
prev, point, so_far = status
if prev != "point":
return Just(status)
d = dice()
if sum(d) == 7:
return Just(("craps", point, so_far+[d]))
elif sum(d) == point:
return Just(("win", point, so_far+[d]))
else:
return (
Just(("point", point, so_far+[d]))
>> point_roll(dice)
)我们将status单子分解为元组的三个独立值。我们可以使用小的lambda对象来提取第一、第二和第三个值。我们还可以使用operator.itemgetter()函数提取元组的项。相反,我们使用了多重赋值。
如果未建立积分,则先前状态为赢或输。这个游戏在一次投掷中就解决了,这个函数只返回status单子。
如果建立了点,则状态为点。掷骰子并将规则应用于此新骰子。如果掷骰数为 7,则游戏失败,并返回最终单子。如果掷骰是点数,则游戏获胜,并返回相应的单子。否则,一个稍微修改过的 monad 将传递给point_roll()函数。经修订的status单子将此卷纳入卷的历史记录中。
典型的输出如下所示:
>>> craps()
('craps', 5, [(2, 3), (1, 3), (1, 5), (1, 6)])最后一个单子有一个显示结果的字符串。它具有已建立的点和掷骰子的顺序。每个结果都有一个特定的支付,我们可以用它来确定投注者的股份的总体波动。
我们可以使用模拟来检查不同的下注策略。我们可能正在寻找一种方法来击败游戏中的任何房子边缘。
There's some small asymmetry in the basic rules of the game. Having 11 as an immediate winner is balanced by having 3 as an immediate loser. The fact that 2 and 12 are also losers is the basis of the house's edge of 5.5 percent (1/18 = 5.5) in this game. The idea is to determine which of the additional betting opportunities will dilute this edge.
许多聪明的蒙特卡罗模拟可以通过一些简单的函数式编程设计技术来实现。特别是当存在复杂的顺序或内部状态时,monad 可以帮助构造这些类型的计算。
脓单胞菌的另一个特征是名称混乱的单倍体。这直接来自数学,它指的是一组具有运算符和标识元素的数据元素,该组相对于该运算符是闭合的。当我们想到自然数、add算子和恒等元素0时,这是一个适当的幺半群。对于正整数,有一个运算符*和一个恒等值1,我们也有一个幺半群;使用|作为运算符和空字符串作为标识元素的字符串也符合条件。
PyMonad 包含许多预定义的幺半群类。我们可以扩展它来添加我们自己的monoid类。其目的是将编译器限制为某些类型的优化。我们还可以使用 monoid 类创建数据结构,这些数据结构积累了一个复杂的值,可能包括以前操作的历史记录。
其中大部分内容提供了对函数式编程的深入了解。解释一下文档,这是一种在稍微宽容一点的环境中学习函数式编程的简单方法。我们不必学习整个语言和工具集来编译和运行函数式程序,而只需尝试交互式 Python。
实际上,我们不需要太多这些特性,因为 Python 已经是有状态的,并且提供了严格的表达式求值。在 Python 中引入有状态对象或严格有序求值并没有实际的理由。通过将函数概念与 Python 的命令式实现相结合,我们可以用 Python 编写有用的程序。出于这个原因,我们将不深入研究肾盂。
在本章中,我们研究了如何使用 PyMonad 库直接在 Python 中表达一些函数式编程概念。该模块包含许多重要的函数编程技术。
我们研究了 Curry 的概念,Curry 是一个允许应用参数组合来创建新函数的函数。对函数进行 curry 处理还允许我们使用函数组合从简单的片段创建更复杂的函数。我们研究了将简单数据对象包装成函数的函子,这些函数也可用于函数合成。
单子是一种在使用优化编译器和惰性计算规则时强加严格计算顺序的方法。在 Python 中,我们没有一个很好的 Monad 用例,因为 Python 在幕后是一种命令式编程语言。在某些情况下,命令式 Python 可能比单子结构更具表现力和简洁性。
在下一章中,我们将研究如何应用函数式编程技术来构建 web 服务应用程序。HTTP 的思想可以概括为response = httpd(request)。理想情况下,HTTP 是无状态的,这使它与功能设计完美匹配。使用 cookie 类似于提供一个响应值,该值应作为以后请求的参数。**
