Skip to content

Instantly share code, notes, and snippets.

@battermann
Created February 19, 2016 11:14
Show Gist options
  • Save battermann/cc72769a686fcd753d79 to your computer and use it in GitHub Desktop.
Save battermann/cc72769a686fcd753d79 to your computer and use it in GitHub Desktop.
Data-centric vs Function-centric Domain Design
open System
// -----------------------------------------------------------
// Domain
// -----------------------------------------------------------
module GameDomain =
type Dice = One | Two | Three | Four | Five | Six
type Guess = Guess of Dice
type ValidMoves = Guess list
type GameState = {
Trials: Guess list
Secret:Dice }
type Score = Score of int
type MoveResult =
| Unsolved of ValidMoves
| Solved of Dice * Score
type GameApi = {
NewGame: unit -> GameState * MoveResult
MakeGuess: GameState -> Guess -> GameState * MoveResult }
// -----------------------------------------------------------
// Implementation
// -----------------------------------------------------------
module GameImplementation =
open GameDomain
open System.Linq
let private rnd = Random()
let private allPossibleGuesses = [One; Two; Three; Four; Five; Six] |> List.map Guess
let private newGame ()=
match rnd.Next(1,7) with
| 1 -> One
| 2 -> Two
| 3 -> Three
| 4 -> Four
| 5 -> Five
| 6 -> Six
|> fun dice -> { Trials = []; Secret = dice }, Unsolved allPossibleGuesses
let private isSolved guess secret = guess = secret
let private makeGuess gameState (Guess guess) =
let trials = Guess guess :: gameState.Trials
let score = 6 - List.length trials
let findNextMoves trials =
allPossibleGuesses
|> List.filter (fun guess -> trials |> (not << List.exists ((=) guess)))
let moveResult =
if isSolved guess gameState.Secret then
(gameState.Secret, Score score) |> Solved
else
findNextMoves trials |> Unsolved
{ gameState with Trials = trials }, moveResult
let api = {
NewGame = newGame
MakeGuess = makeGuess }
// -----------------------------------------------------------
// Game Domain Model with Capability Based Security
// -----------------------------------------------------------
module GameDomainWithCapabilityBasedSecurity =
open GameDomain
type MoveCapability = unit -> CbsMoveResult
and NextMoveInfo = {
GuessToMake : Guess
Capability : MoveCapability }
and CbsMoveResult =
| Unsolved of NextMoveInfo list
| Solved of Dice * Score
type CbsGameApi = { NewGame : MoveCapability }
// -----------------------------------------------------------
// Game Implementation with Capability Based Security
// (uses the API implementation without security)
// -----------------------------------------------------------
module GameImplementationWithCapabilityBasedSecurity =
open GameDomain
open GameDomainWithCapabilityBasedSecurity
let rec makeMove api moveStatePair =
let (newState, moveResult) =
match moveStatePair with
| Some (guess, gameState) -> api.MakeGuess gameState guess
| None -> api.NewGame()
let makeMoveInfo g = { GuessToMake = g; Capability = fun () -> makeMove api (Some (g, newState)) }
match moveResult with
| MoveResult.Unsolved validMoves -> validMoves |> List.map makeMoveInfo |> CbsMoveResult.Unsolved
| MoveResult.Solved (secret, score) -> CbsMoveResult.Solved (secret, score)
let resolveApi api = { NewGame = fun () -> makeMove api None }
// -----------------------------------------------------------
// UI
// -----------------------------------------------------------
(*
This UI implementation was taken from Scoot Wlaschin from here https://gist.github.com/swlaschin/7a5233a91912e66ac1e4
It was minimally adjusted to get it working in this context
*)
module ConsoleUi =
open GameDomainWithCapabilityBasedSecurity
/// Track the UI state
type UserAction<'a> =
| ContinuePlay of 'a
| ExitGame
/// Print each available move on the console
let displayNextMoves nextMoves =
nextMoves
|> List.iteri (fun i moveInfo ->
printfn "%i) %A" i moveInfo.GuessToMake)
/// Get the move corresponding to the
/// index selected by the user
let getCapability selectedIndex nextMoves =
if selectedIndex < List.length nextMoves then
let move = List.nth nextMoves selectedIndex
Some move.Capability
else
None
/// Given that the user has not quit, attempt to parse
/// the input text into a index and then find the move
/// corresponding to that index
let processMoveIndex inputStr availableMoves processInputAgain =
match Int32.TryParse inputStr with
// TryParse will output a tuple (parsed?,int)
| true,inputIndex ->
// parsed ok, now try to find the corresponding move
match getCapability inputIndex availableMoves with
| Some capability ->
// corresponding move found, so make a move
let moveResult = capability()
ContinuePlay moveResult // return it
| None ->
// no corresponding move found
printfn "...No move found for inputIndex %i. Try again" inputIndex
// try again
processInputAgain()
| false, _ ->
// int was not parsed
printfn "...Please enter an int corresponding to a displayed move."
// try again
processInputAgain()
/// Ask the user for input. Process the string entered as
/// a move index or a "quit" command
let rec processInput availableCapabilities =
// helper that calls this function again with exactly
// the same parameters
let processInputAgain() =
processInput availableCapabilities
printfn "Enter an int corresponding to a displayed move or q to quit:"
let inputStr = Console.ReadLine()
if inputStr = "q" then
ExitGame
else
processMoveIndex inputStr availableCapabilities processInputAgain
/// After each game is finished,
/// ask whether to play again.
let rec askToPlayAgain api =
printfn "Would you like to play again (y/n)?"
match Console.ReadLine() with
| "y" ->
ContinuePlay (api.NewGame())
| "n" ->
ExitGame
| _ -> askToPlayAgain api
/// The main game loop, repeated
/// for each user input
let rec gameLoop api userAction =
printfn "\n------------------------------\n" // a separator between moves
match userAction with
| ExitGame ->
printfn "Exiting game."
| ContinuePlay moveResult ->
// handle each case of the result
match moveResult with
| Unsolved nextMoves ->
printfn "USOLVED: Make a guess"
displayNextMoves nextMoves
let newResult = processInput nextMoves
gameLoop api newResult
| Solved (dice, score) ->
printfn "SOLVED: %A" score
printfn "SECRET: %A" dice
printfn ""
let nextUserAction = askToPlayAgain api
gameLoop api nextUserAction
/// start the game with the given API
let startGame api =
let userAction = ContinuePlay (api.NewGame())
gameLoop api userAction
// -----------------------------------------------------------
// Logging
// -----------------------------------------------------------
module Logger =
open GameDomain
let injectLogging api =
let makeGuess gameState guess =
printfn "[LOGINFO] %A" guess
api.MakeGuess gameState guess
{ api with MakeGuess = makeGuess }
// -----------------------------------------------------------
// Console Application
// -----------------------------------------------------------
module ConsoleApplication =
let startGame() =
let loggingApi = Logger.injectLogging GameImplementation.api
let api = GameImplementationWithCapabilityBasedSecurity.resolveApi loggingApi
ConsoleUi.startGame api
// ConsoleApplication.startGame()
@nimbosa
Copy link

nimbosa commented Mar 12, 2016

coming from this blog post:
Domain Design | data- or function-centric?

this is exactly what i had in mind for my next apps with extra-thin wrappers, for tight security, cross-platform portability, and native responsiveness

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment