Python 模拟实现单子

Python 模拟实现单子,我们期望通过一种管道传递单子,即把单子作为参数传递给一个函数,并将类似的单子作为函数的值返回。必须将该函数设计成可以接收并返回类似的数据结构。

下面介绍用于模拟该过程的一个简单的管道,这种模拟可以是正规蒙特卡洛模拟的一部分。我们将从字面上理解蒙特卡洛模拟,并模拟一个掷骰子游戏,这个极其复杂的模拟过程涉及一些有状态的规则。

其中还涉及一些游戏用语。该游戏包含一个掷骰子的人(一名投手)和其他玩家。游戏分为以下两个阶段。

  1. 第一次掷骰子称为出骰子,其中包含3个条件。
  • 如果点数是7或11,则投手赢得游戏。任何投注线的人都会作为胜者赢得奖励,其他人则失去押注。游戏结束,投手可以开始新一轮游戏。
  • 如果点数是2、3或12,则投手输掉游戏。任何投注不过线的人赢得奖励,其他人则失去押注。游戏结束,投手必须将骰子转交给另一名投手。
  • 任何其他点数(即4、5、6、8、9或10)会建立一个。游戏状态从出骰子变为点骰子,游戏继续。
  1. 一旦建立了点,每一次的点骰子都会通过以下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也算输。这里要考虑哪些额外的投注机会可以削弱这一主场优势。

利用一些简单的函数式设计技术可以构建大量智能的蒙特卡洛模拟。特别是当存在复杂顺序或内部状态时,单子有助于构建这类计算。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程