如何确保非法行为不可执行?

How can I ensure that illegal behavior is unexecutable?

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

总结:

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

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

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

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

DealHandCommand = 2 张牌

DealHitCommand = 1 张牌

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

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

其中一种可能性是使用可区分的联合:

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

(假设您输入的是 Card

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 阶段(如果你不需要像 betting/splitting/etc 这样的中间阶段。-但请注意我对二十一点一无所知):

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

由你决定

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

  1. 用每个状态的案例定义可区分联合
  2. 为每个案例定义一个转换函数
  3. 可选:实现所有其余代码

步骤 1

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

  • 初始手牌有两张牌
  • A 命中 一张额外的牌

这表明这个 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

步骤 3

是什么阻止客户端创建无效的 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,但未声明它们的 'constructors'。这意味着您不能直接创建这些类型的值。例如,这不会编译:

let g = BlackJack.Game []

给出的错误是:

error FS0039: The value, constructor, namespace or type 'Game' is not defined

创建 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.Whosebug.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.Whosebug.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.Whosebug.Q34042428.Cards.BlackJack

open Ploeh.Whosebug.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.Whosebug.Q34042428.Cards.Client

open Ploeh.Whosebug.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