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