如何使非法行为无法执行?
要点:
自从开始学习F#之后,我正在学习类型驱动设计和基于属性的测试。结果,我爱上了让非法国家无法代表的想法。
但我真正想做的是使非法行为无法执行。
我正在通过写一个BlackJack游戏来学习F#。因此,我想确保当经销商分发卡片时,经销商只能处理“初始手”或“击中”。所有其他卡片分发都是非法的。
在C#中,我将实现策略模式,从而创建一个DealHandCommand和一个DealHitCommand。然后我会硬编码一个常数整数值来表示要解决的卡数(按策略)。
DealHandCommand = 2张卡
DealHitCommand = 1张卡
基于这些策略,我会实现一个状态机来代表BlackJack游戏的一个会话。因此,在我处理了初始手牌(即DealHandCommand)之后,我执行了一个状态转换,其中未来的交易只能执行“DealHitCommand”。
具体而言,在混合功能语言中实现状态机是否有意义以实现非法行为?
答案 0 :(得分:9)
在F#中实现状态机很容易。它通常遵循三个步骤,第三步是可选的:
在这种情况下,我觉得有两种状态:
这表明这个Deal
歧视联盟:
type Deal = Hand of Card * Card | Hit of Card
另外,定义Game
是什么:
type Game = Game of Deal list
注意使用单一案件歧视联盟; there's a reason for that
现在定义一个从每个状态转换为Game
的函数。
事实证明,您无法将从任何游戏状态转换为Hand
案例,因为Hand
是启动的 a新游戏。另一方面(双关语)你需要提供进入手中的卡:
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
此处声明类型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
答案 1 :(得分:2)
点击还有一张卡吗?
如果是这样,那么只需使用两个类型:
type HandDealt = Dealt of Card * Card
type Playing = Playing of Cards
然后代替命令,你有简单的功能:
dealHand :: Card * Card -> HandDealt
start :: HandDealt -> Playing
dealAnother :: Playing -> Card -> Playing
PS:也许你甚至想跳过HandDealt
/ start
阶段(如果你不需要中间阶段来进行投注/分裂/等等) - 但请注意我对二十一点没有任何线索):
dealHand :: Card * Card -> Playing
dealAnother :: Playing -> Card -> Playing
它取决于你
答案 2 :(得分:1)
其中一种可能性可能是使用歧视联盟:
type DealCommand =
| Hand of Card * Card
| Hit of Card
(假设您有类型Card
)