【问题标题】:How can I ensure that illegal behavior is unexecutable?如何确保非法行为无法执行?
【发布时间】:2015-12-02 22:15:09
【问题描述】:

如何使非法行为无法执行?

总结:

自从开始学习 F# 以来,我正在学习类型驱动设计和基于属性的测试。结果,我爱上了让非法国家无法代表的想法。

但我真正想做的是让非法行为无法执行。

我正在通过编写 BlackJack 游戏来学习 F#。因此,我想确保当庄家发牌时,庄家只能发“初始手牌”或“打牌”。所有其他的卡片分发都是非法的。

在 C# 中,我将实现策略模式,从而创建一个 DealHandCommand 和一个 DealHitCommand。然后我会硬编码一个常量整数值来表示要发牌的数量(每个策略)。

DealHandCommand = 2 张牌

DealHitCommand = 1 张牌

基于这些策略,然后我将实现一个状态机来表示一个二十一点游戏的会话。因此,在我处理完初始手牌(即 DealHandCommand)后,我执行状态转换,在该状态转换中,未来的交易只能执行“DealHitCommand”。

具体来说,在混合功能语言中实现状态机以实现无法执行的非法行为是否有意义?

【问题讨论】:

    标签: f# functional-programming state-machine


    【解决方案1】:

    在 F# 中实现状态机很容易。它通常遵循三步过程,第三步是可选的:

    1. 为每个州定义一个带有案例的歧视联盟
    2. 为每种情况定义一个转换函数
    3. 可选:实现所有其余代码

    第一步

    在这种情况下,我觉得有两种状态:

    • 最初的有两张牌
    • 一击加一张牌

    这表明Deal 有区别的联合:

    type Deal = Hand of Card * Card | Hit of Card
    

    另外,定义 Game 是什么:

    type Game = Game of Deal list
    

    注意使用单例区分联合; there's a reason for that.

    第 2 步

    现在定义一个从每个状态转换到Game 的函数。

    事实证明,您无法任何游戏状态转换到Hand 情况,因为Hand开始新游戏的原因。另一方面(双关语)你需要提供进入手牌的

    let init c1 c2 = Game [Hand (c1, c2)]
    

    另一种情况是游戏正在进行时,你应该只允许Hit,而不是Hand,所以定义这个过渡:

    let hit (Game deals) card = Game (Hit card :: deals)
    

    如您所见,hit 函数要求您传入现有的Game

    第三步

    是什么阻止客户端创建无效的Game 值,例如[Hand; Hit; Hand; Hit; Hit]?

    你可以用signature file封装上面的状态机:

    BlackJack.fsi:

    type Deal
    type Game
    val init : Card -> Card -> Game
    val hit : Game -> Card -> Game
    val card : Deal -> Card list
    val cards : Game -> Card list
    

    在这里,类型 DealGame 被声明,但它们的“构造函数”没有。这意味着您不能直接创建这些类型的值。例如,这不会编译:

    let g = BlackJack.Game []
    

    给出的错误是:

    错误 FS0039:未定义值、构造函数、命名空间或类型“游戏”

    创建Game 值的唯一方法是调用为您创建它的函数:

    let g =
        BlackJack.init
            { Face = Ace; Suit = Spades }
            { Face = King; Suit = Diamonds }
    

    这也使您能够继续游戏:

    let g' = BlackJack.hit g { Face = Two; Suit = Spades }
    

    您可能已经注意到,上面的签名文件还定义了两个函数来将卡片从GameDeal 值中取出。以下是实现:

    let card = function
        | Hand (c1, c2) -> [c1; c2]
        | Hit c -> [c]
    
    let cards (Game deals) = List.collect card deals
    

    客户可以这样使用它们:

    > let cs = g' |> BlackJack.cards;;
    >
    
    val cs : Card list = [{Suit = Spades;
                           Face = Two;};
                          {Suit = Spades;
                           Face = Ace;};
                          {Suit = Diamonds;
                           Face = King;}]
    

    请注意,这种方法主要是结构性的;活动部件很少。

    附录

    这些是上面使用的文件:

    Cards.fs:

    namespace Ploeh.StackOverflow.Q34042428.Cards
    
    type Suit = Diamonds | Hearts | Clubs | Spades
    type Face =
        | Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten
        | Jack | Queen | King | Ace
    
    type Card = { Suit: Suit; Face: Face }
    

    BlackJack.fsi:

    module Ploeh.StackOverflow.Q34042428.Cards.BlackJack
    
    type Deal
    type Game
    val init : Card -> Card -> Game
    val hit : Game -> Card -> Game
    val card : Deal -> Card list
    val cards : Game -> Card list
    

    BlackJack.fs:

    module Ploeh.StackOverflow.Q34042428.Cards.BlackJack
    
    open Ploeh.StackOverflow.Q34042428.Cards
    
    type Deal = Hand of Card * Card | Hit of Card
    
    type Game = Game of Deal list
    
    let init c1 c2 = Game [Hand (c1, c2)]
    
    let hit (Game deals) card = Game (Hit card :: deals)
    
    let card = function
        | Hand (c1, c2) -> [c1; c2]
        | Hit c -> [c]
    
    let cards (Game deals) = List.collect card deals
    

    Client.fs:

    module Ploeh.StackOverflow.Q34042428.Cards.Client
    
    open Ploeh.StackOverflow.Q34042428.Cards
    
    let g =
        BlackJack.init
            { Face = Ace; Suit = Spades }
            { Face = King; Suit = Diamonds }
    let g' = BlackJack.hit g { Face = Two; Suit = Spades }
    
    let cs = g' |> BlackJack.cards
    

    【讨论】:

    • 谢谢马克。在 fsi 文件中,如果我们确实想要明确地允许外界使用“游戏”,我们将如何表达呢?它只是在 fsi 文件中“键入 Game()”吗?
    • @ScottNimrod 如果你想这样做,最简单的方法是删除.fsi 文件,因为这样正常的模块声明就会生效。否则,我相信the documentation 可以告诉你如何做到这一点。
    【解决方案2】:

    a hit 又是一张卡,对吗?

    如果是这样,那么只需使用两个 类型

    • type HandDealt = Dealt of Card * Card
    • type Playing = Playing of Cards
    • (可能更多 - 取决于您想要什么)。

    那么你有简单的函数来代替 Commands

    • dealHand :: Card * Card -> HandDealt
    • start :: HandDealt -> Playing
    • dealAnother :: Playing -> Card -> Playing

    这样你只能遵循某种行为并且它是静态检查的。

    当然,您可能希望将这些类型扩展到多个玩家,但我认为您明白了我的意思


    PS:也许您甚至想跳过HandDealt / start 阶段(如果您不需要中间阶段来进行下注/拆分/等事情。但请注意,我对二十一点一无所知):

    • dealHand :: Card * Card -> Playing
    • dealAnother :: Playing -> Card -> Playing

    由你决定

    【讨论】:

      【解决方案3】:

      其中一种可能是使用有区别的联合:

      type DealCommand =
          | Hand of Card * Card
          | Hit of Card
      

      (假设您输入了Card

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-08-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-02-27
        • 1970-01-01
        相关资源
        最近更新 更多