如何使非法行为无法执行?
摘要:
自从开始学习F#之后,我学习了类型驱动设计和基于属性的测试.结果,我爱上了让非法国家无法代表的想法.
但我真正想做的是使非法行为无法执行.
我正在通过写一个BlackJack游戏来学习F#.因此,我想确保当经销商分发卡片时,经销商只能处理"初始手"或"击中".所有其他卡片分发都是非法的.
在C#中,我将实现策略模式,从而创建DealHandCommand和DealHitCommand.然后我会硬编码一个常数整数值来表示要处理的卡数(每个策略).
DealHandCommand = 2张牌
DealHitCommand = 1张卡
基于这些策略,我将实现一个状态机来代表BlackJack游戏的一个会话.因此,在我处理初始手(即DealHandCommand)之后,我执行状态转换,其中未来的交易只能执行"DealHitCommand".
具体而言,在混合功能语言中实现状态机是否有意义,以实现不可执行的非法行为?
在F#中实现状态机很容易.它通常遵循三个步骤,第三步是可选的:
使用每个州的案例定义一个被区分的联盟
为每种情况定义转换函数
可选:实现所有其余代码
在这种情况下,我觉得有两种状态:
最初的手牌有两张牌
一命中一个额外的卡
这表明这种Deal
歧视的联盟:
type Deal = Hand of Card * Card | Hit of Card
另外,定义一个Game
是什么:
type Game = Game of Deal list
注意使用单一案例的歧视联盟; 这是有原因的.
第2步现在定义一个从每个状态转换为a的函数Game
.
事实证明,你不能从任何游戏状态转换到Hand
案例,因为a 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]
?
您可以使用签名文件封装上述状态机:
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
这里,类型Deal
和Game
声明,但他们的'构造函数'不是.这意味着您无法直接创建这些类型的值.例如,这不编译:
let g = BlackJack.Game []
给出的错误是:
错误FS0039:未定义值,构造函数,命名空间或类型"Game"
创建Game
值的唯一方法是调用为您创建值的函数:
let g = BlackJack.init { Face = Ace; Suit = Spades } { Face = King; Suit = Diamonds }
这也使您可以继续游戏:
let g' = BlackJack.hit g { Face = Two; Suit = Spades }
您可能已经注意到,上面的签名文件还定义了两个函数来获取卡Game
和Deal
值.以下是实施:
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