【问题标题】:Functionally pure dice rolls in C#C# 中的功能纯骰子掷骰子
【发布时间】:2017-04-09 05:47:17
【问题描述】:

我正在用 C# 编写一个基于骰子的游戏。我希望我所有的游戏逻辑都是纯粹的,所以我设计了一个像这样的掷骰子生成器:

public static IEnumerable<int> CreateDiceStream(int seed)
{
    var random = new Random(seed);

    while (true)
    {
        yield return 1 + random.Next(5);
    }
}

现在我可以在我的游戏逻辑中使用它:

var playerRolls = players.Zip(diceRolls, (player, roll) => Tuple.Create(player, roll));

问题是下次我从diceRolls 拿走我想跳过我已经拿走的卷:

var secondPlayerRolls = players.Zip(
    diceRolls.Skip(playerRolls.Count()), 
    (player, roll) => Tuple.Create(player, roll));

这已经很丑陋且容易出错。随着代码变得更加复杂,它不能很好地扩展。

这也意味着在函数之间使用掷骰子序列时我必须小心:

var x = DoSomeGameLogic(diceRolls);

var nextRoll = diceRolls.Skip(x.NumberOfDiceRollsUsed).First();

我应该在这里使用一个好的设计模式吗?

请注意,由于同步和播放要求,我的函数保持纯正很重要。


这个问题不是关于正确初始化System.Random。请阅读我写的内容,如果不清楚,请发表评论。

【问题讨论】:

  • 您能否提供更多背景信息来说明为什么您甚至有一个 secondPlayerRolls 变量?我的意思是说第二回合不就是对同一个函数的另一个调用,而不是它自己的代码和自己的变量吗?
  • @sdgfsdh 是的,我是说在编写这样的游戏时永远不会有secondRolls 变量这样的东西,所以你描述的问题就不会发生。跨度>
  • @Plutonix 这个问题与您标记为重复的内容完全无关。
  • 我的意思是“纯粹”的功能纯度。没有副作用且结果完全由参数决定的函数是纯函数。

标签: c# random functional-programming


【解决方案1】:

实现纯随机生成器的“经典”方式是使用state monad(更多解释here)的特殊形式,它包装了生成器当前状态的携带。所以,而不是实现(请注意,我的 C# 相当生疏,所以请将其视为伪代码):

Int Next() {
    nextState, nextValue = NextRandom(globalState);
    globalState = nextState;
    return nextValue;
} 

你可以这样定义:

class Random<T> {
    private Func<Int, Tuple<Int, T>> transition;

    private Tuple<Int, Int> NextRandom(Int state) { ... whatever, see below ... }

    public static Random<A> Unit<A>(A a) { 
        return new Random<A>(s => Tuple(s, a)); 
    }

    public static Random<Int> GetRandom() {
        return new Random<Int>(s => nextRandom(s));
    }

    public Random<U> SelectMany(Func<T, Random<U>> f) {
        return new Random(s => {
            nextS, a = this.transition(s);
            return f(a).transition(nextS);
        }
    }

    public T Run(Int seed) {
        return this.transition(seed);
    }
}

如果我做的一切正确的话,它应该可以与 LINQ 一起使用:

// player1 = bla, player2 = blub, ...
Random<Tuple<Player, Int>> playerOneRoll = from roll in GetRandom()
                                           select Tuple(player1, roll);
Random<Tuple<Player, Int>> playerTwoRoll = from roll in GetRandom()
                                           select Tuple(player2, roll);
Random<List<Tuple<Player, Int>>> randomRolls = from t1 in playerOneRoll
                                               from t2 in playerTwoRoll
                                               select List(t1, t2);

var actualRolls = randomRolls.Run(234324);

等等,可能使用一些组合器。这里的技巧是表示由当前输入状态参数化的整个“随机动作”;但这也是问题所在,因为您需要很好地实现NextRandom。

如果您可以重用 .NET Random 实现的内部结构,那就太好了,但看起来,您无法访问其内部状态。但是,我确信互联网上有足够好的 PRNG 状态函数(this one 看起来不错;您可能需要更改状态类型)。

monad 的另一个缺点是,一旦你开始使用它们(即,在 Random 中构造东西),你需要将整个控制流“传递”到顶层,你应该调用它Run 一劳永逸。这是一个需要习惯的东西,并且在 C# 中比针对此类事情优化的函数式语言更乏味。

【讨论】:

    【解决方案2】:

    这是一个非常好的谜题。

    由于操作 diceRolls 的状态是不可能的(否则,我们会遇到你提到的同步和重放问题),我们需要一个返回 (a) 要使用的值和 (b) 的操作) 一个新的 diceRolls 可枚举,它在消耗的物品之后开始。

    我的建议是使用 (a) 的返回值和 (b) 的输出参数:

    static IEnumerable<int> Consume(this IEnumerable<int> rolls, int count, out IEnumerable<int> remainder)
    {
        remainder = rolls.Skip(count);
        return rolls.Take(count);
    }
    

    用法:

    var firstRolls = diceRolls.Consume(players.Count(), out diceRolls);
    var secondRolls = diceRolls.Consume(players.Count(), out diceRolls);
    

    DoSomeGameLogic 将在内部使用Consume 并返回剩余的卷。因此,它需要按如下方式调用:

    var x = DoSomeGameLogic(diceRolls, out diceRolls);
    // or
    var x = DoSomeGameLogic(ref diceRolls);
    // or
    x = DoSomeGameLogic(diceRolls);
    diceRolls = x.RemainingDiceRolls;
    

    【讨论】:

    • 不是基本相同的 Skip OP 已经使用了吗?生成直到 N 的所有数字来获得第 N 次滚动感觉不是很好。
    • 我觉得应该是return rolls.Take(remainder);吧?
    • @Evk:OPs 解决方案需要在创建第二轮的代码行中使用第一轮的数量,这很丑陋且容易出错。我的解决方案解决了这个问题(通过仅在创建第一卷的代码行中要求第一卷的数量)。我没有做任何事情来提高性能或修复您正确识别的“画家施莱米尔”问题。
    • @EvanTrimboli:不,Take 需要一个整数。
    • 您是否认为有一种方法可以不从一开始就一遍又一遍地枚举序列并且仍然保留代码“纯”?我想如果您将重用相同的枚举器,它将不再是纯的?
    猜你喜欢
    • 2012-02-29
    • 2018-09-08
    • 1970-01-01
    • 1970-01-01
    • 2018-05-03
    • 1970-01-01
    • 2015-09-22
    • 2019-03-09
    • 1970-01-01
    相关资源
    最近更新 更多