Haskell Program to Play the Blackjack Card Game

For much of my work using Haskell I deal mostly with pure code with smaller bits of impure code for network and file IO, etc. Realizing that my use case for using Haskell (mostly pure code) may not be typical, I wanted the last example “cookbook recipe” in this book to be an example dealing with changing state, a program to play the Blackjack card game.

The game state is maintained in the type Table that holds information on a randomized deck of cards, the number of players in addition to the game user and the card dealer, the cards in the current hand, and the number of betting chips that all players own. Table data is immutable so all of the major game playing functions take a table and any other required inputs, and generate a new table as the function result.

This example starts by asking how many players, besides the card dealer and the game user, should play a simulated Blackjack game. The game user controls when they want another card while the dealer and any other simulated players play automatically (they always hit when their card score is less than 17).

I define the types for playing cards and an entire card deck in the file Card.hs:

Blackjack Card Game Architecture
Figure 23. Blackjack Card Game Architecture
 1 -- Card model: defines `Rank`, `Suit`, and `Card`, with helpers
 2 -- `orderedCardDeck` builds a deterministic deck; `cardValue` maps ranks to scores
 3 module Card (Card(..), Rank(..), Suit(..), orderedCardDeck, cardValue) where
 4 
 5 import Data.Maybe (fromMaybe)
 6 import Data.List (elemIndex)
 7 import Data.Map (Map, fromList, lookup, keys)
 8 
 9 data Card = Card { rank :: Rank
10                  , suit :: Suit }
11                  deriving (Eq, Show)
12                  
13 data Suit = Hearts | Diamonds | Clubs | Spades
14           deriving (Eq, Show, Enum, Ord, Bounded)
15 
16 data Rank = Two | Three | Four
17           | Five | Six | Seven | Eight
18           | Nine | Ten | Jack  | Queen | King | Ace
19           deriving (Eq, Show, Enum, Ord)
20 
21 -- | Map each 'Rank' to its Blackjack point value.
22 rankMap :: Map Rank Int
23 rankMap = fromList [(Two,2), (Three,3), (Four,4), (Five,5),
24                     (Six,6), (Seven,7), (Eight,8), (Nine,9),
25                     (Ten,10), (Jack,10), (Queen,10),
26                     (King,10), (Ace,11)]
27 
28 -- | Deterministic deck: list-comprehension over all ranks and all four suits.
29 -- Uses [minBound .. maxBound] to guarantee every Suit is included.
30 orderedCardDeck :: [Card]
31 orderedCardDeck = [Card r s | r <- keys rankMap,
32                               s <- [minBound .. maxBound]]
33 
34 -- | Look up the point value of a card's rank.
35 cardValue :: Card -> Int
36 cardValue aCard = fromMaybe 0 (Data.Map.lookup (rank aCard) rankMap)

This module defines essential components for representing and working with playing cards.

Data Types

  • Card: A record type with two fields:

    • rank :: Rank - Represents the card’s rank (e.g., Two, Queen, Ace).
    • suit :: Suit - Represents the card’s suit (e.g., Hearts, Spades).
  • Suit: An enumeration defining the four card suits: Hearts, Diamonds, Clubs, Spades. It derives Eq (equality), Show (string representation), Enum (enumeration capabilities), and Ord (ordering) for convenience.

  • Rank: An enumeration listing the thirteen card ranks, from Two to Ace. It also derives Eq, Show, Enum, and Ord.

Functions and Values

  • rankMap: A Data.Map that associates each Rank with its corresponding numerical value in games like Blackjack.

  • orderedCardDeck: A list comprehension that generates a standard 52-card deck, sorted by rank within each suit.

  • cardValue: A function that takes a Card and returns its numerical value based on the rankMap. It uses pattern matching to handle the Maybe type returned by Data.Map.lookup.

Explanation

  1. Card Data Type: The core of the module. It defines a playing card as a combination of a Rank and a Suit.

  2. Suit and Rank Enumerations: These provide a clear and type-safe representation of suits and ranks. Deriving Enum and Ord allows easy iteration and comparison.

  3. rankMap: This map is crucial for assigning numerical values to cards, particularly in games where card values matter (e.g., Blackjack).

  4. orderedCardDeck: This function generates a standard 52-card deck. It uses list comprehension to iterate over all Rank values (obtained from the keys of rankMap) and all Suit values (from Hearts to Clubs), creating a Card for each combination.

  5. cardValue: This function retrieves the numerical value of a given card. It uses Data.Map.lookup to find the value associated with the card’s rank in rankMap. The case expression handles the possibility of lookup returning Nothing (which should ideally never happen in this context).

Key Points

  • The code provides a well-structured representation of playing cards in Haskell.
  • The use of enumerations enhances type safety and readability.
  • Data.Map is employed for efficient lookup of card values.
  • The orderedCardDeck function conveniently generates a standard deck of cards.

As usual, the best way to understand this code is to go to the GHCi repl:

 1 *Main Card RandomizedList Table> :l Card
 2 [1 of 1] Compiling Card             ( Card.hs, interpreted )
 3 Ok, modules loaded: Card.
 4 *Card> :t orderedCardDeck
 5 orderedCardDeck :: [Card]
 6 *Card> orderedCardDeck
 7 [Card {rank = Two, suit = Hearts},Card {rank = Two, suit = Diamonds},Card {rank = Two, suit = Clubs},Card {rank = Three, suit = Hearts},Card {rank = Three,
 8     ...
 9 *Card> head orderedCardDeck
10 Card {rank = Two, suit = Hearts}
11 *Card> cardValue $ head orderedCardDeck
12 2

So, we have a sorted deck of cards and a utility function for returning the numerical value of a card (we always count ace cards as 11 points, deviating from standard Blackjack rules).

The next thing we need to get is randomly shuffled lists. The Haskell Wiki has a good writeup on randomizing list elements and we are borrowing their function randomizedList (you can see the source code in the file RandomizedList.hs). Here is a sample use:

1 *Card> :l RandomizedList.hs 
2 [1 of 1] Compiling RandomizedList   ( RandomizedList.hs, interpreted )
3 Ok, modules loaded: RandomizedList.
4 *RandomizedList> import Card
5 *RandomizedList Card> randomizedList orderedCardDeck
6 [Card {rank = Queen, suit = Hearts},Card {rank = Six, suit = Diamonds},Card {rank = Five, suit = Clubs},Card {rank = Five, suit = Diamonds},Card {rank = Seven, suit = Clubs},Card {rank = Three, suit = Hearts},Card {rank = Four, suit = Diamonds},Card {rank = Ace, suit = Hearts},
7   ...

Much of the complexity in this example is implemented in Table.hs which defines the type Table and several functions to deal and score hands of dealt cards:

  • createNewTable :: Players -> Table. Players is the integer number of other players at the table.
  • setPlayerBet :: Int -> Table -> Table. Given a new value to bet and a table, generate a new modified table.
  • showTable :: Table -> [Char]. Given a table, generate a string describing the table (in a format useful for development)
  • initialDeal :: [Card] -> Table -> Int -> Table. Given a randomized deck of cards, a table, and the number of other players, generate a new table.
  • changeChipStack :: Int -> Int -> Table -> Table. Given a player index (index order: user, dealer, and other players), a new number of betting chips for the player, and a table, then generate a new modified table.
  • setCardDeck :: [Card] -> Table -> Table. Given a randomized card deck and a table, generate a new table containing the new randomized card list; all other table data is unchanged.
  • dealCards :: Table -> [Int] -> Table. Given a table and a list of player indices for players wanting another card, generate a new modified table.
  • resetTable :: [Card] -> Table -> Int -> Table. Given a new randomized card deck, a table, and a new number of other players, generate a new table.
  • scoreHands :: Table -> Table. Given a table, score all dealt hands and generate a new table with these scores. There is no table type score data, rather, we “score” by changing the number of chips all of the players (inclding the dealer) has.
  • dealCardToUser :: Table -> Int -> Table. For the game user, always deal a card. For the dealer and other players, deal another card if their hand score is less than 17.
  • handOver :: Table -> Bool. Determine if the current hand is over.
  • setPlayerPasses :: Table -> Table. Call this function when the payer passes. Other players and dealer are then played out automatically.

The implementation in the file Table.hs is fairly simple, with the exception of the use of Haskell lenses to access nested data in the table type. I will discuss the use of lenses after the program listing, but: as you are reading the code look out for variables starting with the underscore character _ that alerts the Lens system that it should create data accessors for these variables.

This code defines a module named Table which provides data structures and functions to simulate a simplified table in a card game, potentially Blackjack.

Core Components

  • Table data type:

    • Represents the state of the table, storing information like:
      • Number of players
      • Chip stacks for each player
      • Cards dealt to each player (including the dealer)
      • Current player’s bet
      • Whether the user has passed their turn
      • The remaining card deck
  • Functions:

    • createNewTable: Creates a new table with the specified number of players and initial chip stacks.
    • resetTable: Resets the table for a new round, clearing dealt cards and optionally changing the card deck.
    • setCardDeck: Sets a new card deck for the table.
    • dealCards: Deals cards to specified players.
    • initialDeal: Performs the initial deal at the beginning of a round.
    • showTable: Generates a string representation of the table’s current state.
    • scoreHands: Calculates and updates chip stacks based on player and dealer scores.
    • setPlayerBet: Sets the current player’s bet.
    • setPlayerPasses: Simulates the player passing their turn, dealing additional cards to other players and the dealer.
    • changeChipStack: Modifies a specific player’s chip stack.
    • score: Calculates the score of a player’s hand.
    • dealCardToUser: Deals a card to a specified player, with special handling for the user and dealer.
    • handOver: Checks if the user has passed their turn.

Lenses

The code uses lenses (makeLenses ''Table) to provide convenient access and modification of the Table data type’s fields.

Game Logic (Simplified)

  • The code seems to implement a basic version of a card game where players and the dealer are dealt cards.
  • scoreHands calculates scores and updates chip stacks based on win/loss conditions.
  • dealCardToUser handles dealing cards, ensuring the dealer keeps drawing until their score is at least 17.
  • setPlayerPasses simulates the user passing, triggering the dealer and other players to finish their turns.
  1 {-# LANGUAGE TemplateHaskell #-}  -- for makeLens
  2 
  3 -- | Game state and rules for Blackjack; pure transformations on 'Table'.
  4 -- Uses lenses to update nested fields concisely.
  5 --
  6 -- Player indexing convention:
  7 --   0     = human player
  8 --   1     = dealer
  9 --   2 … n = AI players
 10 module Table (Table (..), createNewTable, setPlayerBet, showTable, initialDeal,
 11               changeChipStack, setCardDeck, dealCards, resetTable, scoreHands,
 12               dealCardToUser, handOver, setPlayerPasses) where  -- note: export dealCardToUser only for ghci development
 13 
 14 import Control.Lens
 15 
 16 import Card
 17 import Data.Bool
 18 import Data.Maybe (fromMaybe)
 19 
 20 data Table = Table { _numPlayers        :: Int
 21                    , _chipStacks       :: [Int] -- ^ number of chips, indexed by player index
 22                    , _dealtCards       :: [[Card]] -- ^ dealt cards for user, dealer, and other players
 23                    , _currentPlayerBet :: Int
 24                    , _userPasses       :: Bool
 25                    , _cardDeck         :: [Card]
 26                    }
 27            deriving (Show)
 28            
 29 type Players = Int
 30              
 31 createNewTable :: Players -> Table
 32 createNewTable n =
 33   Table n
 34         [500 | _ <- [1 .. n]] -- give each player (incuding dealer) 10 chips
 35         [[] | _ <- [0..n]] -- dealt cards for user and other players (we don't track dealer's chips)
 36         20 -- currentPlayerBet
 37         False
 38         [] -- placeholder for random shuffled card deck
 39  
 40 resetTable :: [Card] -> Table -> Int -> Table
 41 resetTable cardDeck aTable numberOfPlayers =
 42   Table numberOfPlayers
 43         (_chipStacks aTable)
 44         [[] | _ <- [0..numberOfPlayers]]
 45         (_currentPlayerBet aTable)
 46         False
 47         cardDeck
 48      
 49      -- Use lens extensions:
 50             
 51 makeLenses ''Table
 52  
 53 showDealtCards :: [[Card]] -> String
 54 showDealtCards dc =
 55   (show [map cardValue hand | hand <- dc])
 56 
 57 setCardDeck :: [Card] -> Table -> Table
 58 setCardDeck newDeck =
 59   over cardDeck (\_ -> newDeck)  
 60 
 61 dealCards :: Table -> [Int] -> Table
 62 dealCards aTable playerIndices =
 63   last $ scanl dealCardToUser aTable playerIndices
 64  
 65 -- | Initial deal: reset table with a new shuffled deck, then deal two rounds.
 66 initialDeal :: [Card] -> Table -> Int -> Table
 67 initialDeal cardDeck aTable numberOfPlayers =
 68   dealCards
 69     (dealCards (resetTable cardDeck aTable numberOfPlayers) [0 .. numberOfPlayers])
 70     [0 .. numberOfPlayers]
 71     
 72 showTable :: Table -> [Char]
 73 showTable aTable =
 74   "\nCurrent table data:\n" ++
 75   "  Chipstacks: " ++
 76   "\n    Player: " ++ (show (head (_chipStacks aTable))) ++
 77   "\n    Other players: " ++ (show (tail (_chipStacks aTable))) ++
 78   "\n  User cards: " ++ (show (head (_dealtCards aTable))) ++
 79   "\n  Dealer cards: " ++ (show ((_dealtCards aTable) !! 1)) ++
 80   "\n  Other player's cards: " ++ (show (tail (tail(_dealtCards aTable)))) ++
 81   -- "\n  Dealt cards: " ++ (show (_dealtCards aTable)) ++
 82   "\n  Dealt card values: " ++ (showDealtCards (_dealtCards aTable)) ++
 83   "\n  Current player bet: " ++
 84   (show (_currentPlayerBet aTable)) ++
 85   "\n  Player pass: " ++
 86   (show (_userPasses aTable)) ++ "\n"
 87   
 88 clipScore :: Table -> Int -> Int
 89 clipScore aTable playerIndex =
 90   let s = score aTable playerIndex in
 91     if s < 22 then s else 0
 92       
 93 -- | Resolve bets for the hand: compare each player's score against the dealer.
 94 -- Busts are treated as 0; updates chip stacks accordingly.
 95 --
 96 -- Indexing: chipStacks2 !! 0 = human, !! 1 = dealer, !! 2+ = AI players.
 97 -- 'otherScores' starts at player index 2, so we zip with 'tail (tail chipStacks2)'
 98 -- to skip the human's and dealer's chip entries.
 99 scoreHands :: Table -> Table
100 scoreHands aTable =
101   let chipStacks2 = _chipStacks aTable
102       playerScore = clipScore aTable 0
103       dealerScore = clipScore aTable 1
104       otherScores = map (clipScore aTable) [2..]
105       newPlayerChipStack = if playerScore > dealerScore then
106                              (head chipStacks2) + (_currentPlayerBet aTable)
107                            else
108                              if playerScore < dealerScore then
109                                 (head chipStacks2) - (_currentPlayerBet aTable)
110                              else (head chipStacks2)
111       newOtherChipsStacks =
112         map (\(x,y) -> if x > dealerScore then
113                          y + 20
114                        else
115                          if x < dealerScore then
116                            y - 20
117                          else y) 
118             (zip otherScores (tail (tail chipStacks2)))
119       dealerChipStack = chipStacks2 !! 1
120       newChipStacks  = newPlayerChipStack : dealerChipStack : newOtherChipsStacks
121   in
122     over chipStacks (\_ -> newChipStacks) aTable
123 
124 setPlayerBet :: Int -> Table -> Table
125 setPlayerBet newBet =
126   over currentPlayerBet (\_ -> newBet)  
127 
128 setPlayerPasses :: Table -> Table
129 setPlayerPasses aTable =
130   let numPlayers = _numPlayers aTable
131       playerIndices = [1..numPlayers]
132       t1 = over userPasses (\_ -> True) aTable
133       t2 = dealCards t1 playerIndices
134       t3 = dealCards t2 playerIndices
135       t4 = dealCards t3 playerIndices
136   in
137     t4
138     
139     
140 changeChipStack :: Int -> Int -> Table -> Table
141 changeChipStack playerIndex newValue =
142   over chipStacks (\a -> a & element playerIndex .~ newValue)
143 
144 score :: Table -> Int -> Int
145 score aTable playerIndex =
146   let scores = map cardValue ((_dealtCards aTable) !! playerIndex)
147       totalScore = sum scores in
148     totalScore
149   
150 dealCardToUser' :: Table -> Int -> Table
151 dealCardToUser' aTable playerIndex =
152   let nextCard = head $ _cardDeck aTable
153       playerCards = nextCard : ((_dealtCards aTable) !! playerIndex)
154       newTable = over cardDeck (\cd -> tail cd) aTable in
155     over dealtCards (\a -> a & element playerIndex .~ playerCards) newTable
156 
157 -- | Dealer/AI rule: user always draws; other players draw until score >= 17.
158 dealCardToUser :: Table -> Int -> Table
159 dealCardToUser aTable playerIndex
160   | playerIndex == 0  = dealCardToUser' aTable playerIndex -- user
161   | otherwise         = if (score aTable playerIndex) < 17 then
162                              dealCardToUser' aTable playerIndex
163                         else aTable
164   
165 handOver :: Table -> Bool
166 handOver aTable =
167   _userPasses aTable

In line 46 we use the function makeLenses to generate access functions for the type Table. We will look in some detail at lines 52-54 where we use the lense over function to modify a nested value in a table, returning a new table:

1 setCardDeck :: [Card] -> Table -> Table
2 setCardDeck newDeck =
3   over cardDeck (\_ -> newDeck)

The expression in line 3 evaluates to a partial function that takes another argument, a table, and returns a new table with the card deck modified. Function over expects a function as its second argument. In this example, the inline function ignores the argument it is called with, which would be the old card deck value, and returns the new card deck value which is placed in the table value.

Using lenses can greatly simplify the code to manipulate complex types.

Another place where I am using lenses is in the definition of function scoreHands (lines 88-109). On line 109 we are using the over function to replace the old player betting chip counts with the new value we have just calculated:

1   over chipStacks (\_ -> newChipStacks) aTable

Similarly, we use over in line 113 to change the current player bet. In function handOver on line 157, notice how I am using the generated function _userPasses to extract the value of the user passes boolean flag from a table.

The function main, defined in the file Main.hs, uses the code we have just seen to represent a table and modify a table, is fairly simple. A main game loop repetitively accepts game user input, and calls the appropriate functions to modify the current table, producing a new table. Remember that the table data is immutable: we always generate a new table from the old table when we need to modify it.

 1 -- Main.hs – entry point; collects player count then hands off to the TUI
 2 module Main where
 3 
 4 import Card           -- pure code (card types + values)
 5 import Table          -- pure code (game state + rules)
 6 import RandomizedList -- impure code (random shuffle)
 7 import TUI            -- Brick-based terminal UI
 8 import Text.Read (readMaybe)
 9 
10 randomDeck :: IO [Card]
11 randomDeck = randomizedList orderedCardDeck
12 
13 -- | Prompt for the number of other players, validating input.
14 getPlayerCount :: IO Int
15 getPlayerCount = do
16   putStrLn "Besides yourself, how many other players do you want at the table? (1-4)"
17   s <- getLine
18   case readMaybe s :: Maybe Int of
19     Just n | n >= 1 && n <= 4 -> return (n + 1)  -- 0=user, 1=dealer, 2+= other players
20     _ -> do
21       putStrLn "Invalid input. Please enter a number between 1 and 4."
22       getPlayerCount
23 
24 main :: IO ()
25 main = do
26   putStrLn "♠ ♥  Welcome to Blackjack!  ♦ ♣"
27   n <- getPlayerCount
28   cardDeck <- randomDeck
29   let aTable = initialDeal cardDeck (createNewTable n) n
30   runTUI aTable n

This module combines the previously defined Card and Table modules with an impure RandomizedList module and a TUI module (Brick-based terminal UI) to implement a Blackjack card game.

Core Functions

  • randomDeck: Generates a randomized version of the orderedCardDeck using the randomizedList function from the RandomizedList module.

  • getPlayerCount: Prompts the user for the number of additional players (1-4), validating input with readMaybe and recursing on invalid input. Returns the total player count (adding 1 for the dealer).

  • main:

    • Prints a welcome banner.
    • Calls getPlayerCount to get the validated number of players.
    • Creates a new table with the specified number of players and an initial deal.
    • Launches the Brick-based terminal UI via runTUI.

Key Points

  • The code demonstrates an interactive card game implementation using a terminal UI library (Brick).
  • It combines pure modules (Card, Table) with impure modules (RandomizedList for randomization, TUI for the terminal interface).
  • Input validation is handled using readMaybe for safe parsing.
  • The RandomizedList module provides a function randomizedList for shuffling the card deck, introducing impurity into the game logic.

I encourage you to try playing the game yourself, but if you don’t here is a sample game:

 1 *Main Card RandomizedList Table> main
 2 Start a game of Blackjack. Besides yourself, how many other
 3 players do you want at the table?
 4 1
 5 
 6 Current table data:
 7   Chipstacks: 
 8     Player: 500
 9     Other players: [500]
10   User cards: [Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
11   Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
12   Other player's cards: [[Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
13   Dealt card values: [[3,2],[10,7],[10,6]]
14   Current player bet: 20
15   Player pass: False
16 
17 Enter command: h)it or set bet to 10, 20, 30; any other key to stay:
18 h
19 
20 Current table data:
21   Chipstacks: 
22     Player: 500
23     Other players: [500]
24   User cards: [Card {rank = Six, suit = Hearts},Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
25   Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
26   Other player's cards: [[Card {rank = Eight, suit = Hearts},Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
27   Dealt card values: [[6,3,2],[10,7],[8,10,6]]
28   Current player bet: 20
29   Player pass: False
30 
31 Enter command: h)it or set bet to 10, 20, 30; any other key to stay:
32 h
33 
34 Current table data:
35   Chipstacks: 
36     Player: 500
37     Other players: [500]
38   User cards: [Card {rank = King, suit = Clubs},Card {rank = Six, suit = Hearts},Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
39   Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
40   Other player's cards: [[Card {rank = Eight, suit = Hearts},Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
41   Dealt card values: [[10,6,3,2],[10,7],[8,10,6]]
42   Current player bet: 20
43   Player pass: False
44 
45 Enter command: h)it or set bet to 10, 20, 30; any other key to stay:
46 
47 Current table data:
48   Chipstacks: 
49     Player: 500
50     Other players: [500]
51   User cards: [Card {rank = King, suit = Clubs},Card {rank = Six, suit = Hearts},Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
52   Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
53   Other player's cards: [[Card {rank = Eight, suit = Hearts},Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
54   Dealt card values: [[10,6,3,2],[10,7],[8,10,6]]
55   Current player bet: 20
56   Player pass: True
57 
58 Hand over. State of table at the end of the game:
59 
60 Current table data:
61   Chipstacks: 
62     Player: 520
63     Other players: [520]
64   User cards: [Card {rank = King, suit = Clubs},Card {rank = Six, suit = Hearts},Card {rank = Three, suit = Clubs},Card {rank = Two, suit = Hearts}]
65   Dealer cards: [Card {rank = Queen, suit = Diamonds},Card {rank = Seven, suit = Clubs}]
66   Other player's cards: [[Card {rank = Eight, suit = Hearts},Card {rank = King, suit = Hearts},Card {rank = Six, suit = Diamonds}]]
67   Dealt card values: [[10,6,3,2],[10,7],[8,10,6]]
68   Current player bet: 20
69   Player pass: True

Here the game user has four cards with values of [10,6,3,2] for a winning score of 21. The dealer has [10,7] for a score of 17 and the other player has [8,10,6], a value greater than 21 so the player went “bust.”

I hope that you enjoyed this last example that demonstrates a reasonable approach for managing state when using immutable data.