2015-12-02 18 views
8

Come si esegue un comportamento illegale non eseguibile?Come posso garantire che il comportamento illegale non sia eseguibile?

Sommario:

Da quando ho iniziato il mio viaggio per imparare F #, sto imparando su Type-Driven Design e proprietà a base di test. Di conseguenza, mi sono innamorato dell'idea di rendere gli stati illegali non rappresentabili.

Ma quello che mi piacerebbe davvero fare è di rendere unexecutable comportamenti illegali.

sto imparando F # scrivendo un gioco di blackjack. Di conseguenza, voglio assicurarmi che quando un mazziere distribuisce carte, il mazziere può solo dare una "mano iniziale" o un "colpo". Tutte le altre distribuzioni di carte sono illegali.

In C#, vorrei implementare il modello di strategia e, quindi, creare un DealHandCommand e DealHitCommand. Quindi indicherei un numero intero costante per il numero di carte da distribuire (per strategia).

DealHandCommand = 2 carte

DealHitCommand = 1 carta

Sulla base di queste strategie, vorrei quindi implementare uno stato macchina per rappresentare una sessione di un gioco di blackjack. Quindi, dopo aver distribuito la mano iniziale (cioè DealHandCommand), eseguo una transizione di stato in cui le offerte future possono solo eseguire "DealHitCommand".

In particolare, ha senso implementare uno stato macchina all'interno di un linguaggio ibrido-funzionale al fine di ottenere un comportamento illegale come non eseguibile?

risposta

9

E 'facile da implementare una macchina a stati in F #. Di solito segue un processo in tre fasi, con la terza fase è facoltativa:

  1. Definire un Unione discriminata un caso per ogni stato
  2. Definire una funzione di transizione per ciascun caso
  3. opzionale: implementare tutte le resto del codice

Fase 1

In questo caso sembra a me che ci sono due stati:

  • Un primo mano con due carte
  • Un Hit con una carta in più

Questo suggerisce che questo Deal unione discriminati:

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

Inoltre, definire ciò che un Game è :

type Game = Game of Deal list 

Si noti l'uso di un'unione discriminata a caso singolo; there's a reason for that.

Fase 2

Ora definire una funzione che passa da ogni stato ad un Game.

Si scopre che non è possibile la transizione da qualsiasi stato del gioco al caso Hand, perché un Hand è quello che inizia una nuova partita.D'altra parte (gioco di parole) è necessario fornire i carte che vanno in mano:

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

L'altro caso è quando un gioco è in corso, si dovrebbe consentire solo Hit, ma non Hand, in modo da definire questo passaggio:

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

Come si può vedere, la funzione hit richiede di passare in un Game esistente.

Fase 3

Che cosa impedisce un client di creare un valore non valido Game, per esempio [Hand; Hit; Hand; Hit; Hit]?

È possibile incapsulare la macchina dello Stato di cui sopra con un 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 

Qui, i tipi Deal e Game sono dichiarati, ma i loro 'costruttori' non sono . Ciò significa che non puoi creare direttamente valori di questi tipi. Questo, ad esempio, non compila:

let g = BlackJack.Game [] 

L'errore dato è:

errore FS0039: Il valore, costruttore dello spazio dei nomi o il tipo di 'gioco' non è definito

L'unico modo per creare un valore Game è quello di chiamare una funzione che crea per voi:

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

Questo permette anche di continuare il gioco:

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

Avrete notato che il file di firma di cui sopra definisce anche due funzioni per ottenere le carte di Game e Deal valori. Qui ci sono le implementazioni:

let card = function 
    | Hand (c1, c2) -> [c1; c2] 
    | Hit c -> [c] 

let cards (Game deals) = List.collect card deals 

Un cliente li può usare in questo modo:

> let cs = g' |> BlackJack.cards;; 
> 

val cs : Card list = [{Suit = Spades; 
         Face = Two;}; 
         {Suit = Spades; 
         Face = Ace;}; 
         {Suit = Diamonds; 
         Face = King;}] 

Si noti che questo approccio è in gran parte strutturale; ci sono poche parti mobili.

Appendice

Questi sono i file utilizzati in precedenza:

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 
+0

Grazie Marco. Nel file fsi, se volessimo esplicitamente consentire al mondo esterno di utilizzare "Gioco", come lo esprimeremo? Sarebbe solo "tipo Game()" all'interno del file fsi? –

+0

@ScottNimrod Se si volesse fare ciò, il modo più semplice sarebbe quello di eliminare il file '.fsi', perché in tal caso sarebbe valida la normale dichiarazione del modulo. Altrimenti, sono sicuro che [la documentazione] (https://msdn.microsoft.com/en-us/library/dd233196.aspx) può dirti come farlo. –

1

Una delle possibilità potrebbe utilizzare discriminati unione:

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

(ammesso che abbiate tipo Card)

2

un colpito è una carta in più a destra?

Se è così allora basta usare due tipi :

  • type HandDealt = Dealt of Card * Card
  • e type Playing = Playing of Cards
  • (forse di più - dipende da ciò che si desidera).

Allora, invece di comandi si hanno funzioni semplici:

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

questo modo si può solo seguire un certo comportamento ed è staticamente controllato.

di couse probabilmente si desidera estendere quei tipi a più giocatori, ma penso che si ottiene quello che sto per


PS: forse ti piace ancora di saltare la fase di HandDealt/start (se non si 't bisogno fase centrale per le cose come le scommesse/scissione/etc - ma vi prego di ricordare che non ho alcun indizio circa il blackjack):.

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

tocca a te

Problemi correlati