Python 模拟实现单子,我们期望通过一种管道传递单子,即把单子作为参数传递给一个函数,并将类似的单子作为函数的值返回。必须将该函数设计成可以接收并返回类似的数据结构。
下面介绍用于模拟该过程的一个简单的管道,这种模拟可以是正规蒙特卡洛模拟的一部分。我们将从字面上理解蒙特卡洛模拟,并模拟一个掷骰子游戏,这个极其复杂的模拟过程涉及一些有状态的规则。
其中还涉及一些游戏用语。该游戏包含一个掷骰子的人(一名投手)和其他玩家。游戏分为以下两个阶段。
- 第一次掷骰子称为出骰子,其中包含3个条件。
- 如果点数是7或11,则投手赢得游戏。任何投注过线的人都会作为胜者赢得奖励,其他人则失去押注。游戏结束,投手可以开始新一轮游戏。
- 如果点数是2、3或12,则投手输掉游戏。任何投注不过线的人赢得奖励,其他人则失去押注。游戏结束,投手必须将骰子转交给另一名投手。
- 任何其他点数(即4、5、6、8、9或10)会建立一个点。游戏状态从出骰子变为点骰子,游戏继续。
- 一旦建立了点,每一次的点骰子都会通过以下3个条件来验证。
- 如果点数是7,则投手输掉游戏。除了投注不过线的人和竟猜注码,其他人全都输掉游戏,游戏结束。由于投手输掉了游戏,因此骰子转交给其他投手。
- 如果点数和原来一样,则投手输掉游戏。任何投注过线的人成为胜者赢得奖励,其他人输掉游戏。游戏结束,投手可以开始新一轮游戏。
- 任何其他点数的情况,游戏继续进行。一些竟猜注码可能在中间投掷过程中获利或失利。
可以将这些规则视为对状态改变的需求,或者将其视为一系列操作而不是状态改变。必须先使用一个函数再使用递归函数。通过这种方式,配对函数的设计十分适合单子设计模式。
在实际操作中,游戏期间可以投注许多复杂的竟猜注码。可以将它们从游戏的基本规则中分离出来另行求值。其中许多投注(竟猜注码、现场押注等)都是在游戏的点骰子阶段进行的。我们将忽略这些额外的复杂性,而只关注核心游戏。
需要一组源随机数,如下所示:
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())
这样就创建了一个初始单子Just(("",0, []))
,用于定义要使用的基本类型。游戏将生成一个包含结果文本、骰子点值和投掷序列的三元组。每次游戏开始,默认的三元组会构建三元组类型。
把这个单子传递给其他两个函数,这样会根据游戏结果创建出一个结果单子outcome
。按照函数必须遵循的执行顺序使用>>
运算符,将它们以特定顺序相连接。在具有优化编译器的语言中,这会避免表达式顺序重排。
最后使用getValue()
方法获取单子的值。由于单子对象是惰性求值的,因此调用该方法会触发对各个单子的求值,以此创建所需的输出。
函数come_out_roll()
以柯里化函数rng()
为第一个参数,以单子为第二个参数。函数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]))
掷一次骰子来确定第一次的投掷是赢是输还是建立点数,返回的是一个合适的单子,其中包括结果、点值和骰子的投掷。点值对于中间的赢或输没有太大意义。由于没有建立任何点值,因此这里合理地返回一个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
单子分解为元组的3个独立值。既可以使用小型匿名函数对象获取第一个、第二个和第三个值,也可以使用operator.itemgetter()
函数获取元组中的项,但这里使用的是多重赋值语句。
如果尚未建立点,则它的前一个状态是赢或者输。游戏在一次投掷后便结束了,而且这个函数会仅返回status
单子。
如果已经建立了一个点,那么其状态便是一个点。当掷出骰子后,规则便适用于新一轮投掷。如果投掷结果是7,则输掉该游戏并返回最终的单子;如果投掷结果是一个点,则赢得该局游戏并返回相应的单子,否则会将略微修改过的单子传递给point_roll()
函数。修改后的status
单子会将此次投掷记录在历史投掷中。
典型的输出结果如下所示:
>>> craps()
('craps', 5, [(2, 3), (1, 3), (1, 5), (1, 6)])
最终的单子有一个用于显示结果的字符串,其中包含建立的点值和骰子的投掷序列。每个结果都对应一个特定奖励(payout),可以根据它来确定投注者所持押注的总体波动。
可以通过模拟来检验不同的投注策略。我们希望寻找一种方法来破除游戏中任何隐含的主场优势。
游戏的基本规则中存在有一些细小的不对称性。掷得11直接赢和掷得3直接输是相互平衡的。主场有5.5%(1/18≈0.055)的优势是因为掷得2和12也算输。这里要考虑哪些额外的投注机会可以削弱这一主场优势。
利用一些简单的函数式设计技术可以构建大量智能的蒙特卡洛模拟。特别是当存在复杂顺序或内部状态时,单子有助于构建这类计算。