PA 7 — Due: Fri Nov 17 11pm CT
Recall the academic integrity policy: turn in your own work and cite all sources. Portions of the following are adapted from a post by Brian Hempel; his mercurial Ph.D. advisor is to blame for the rest.
How long do wars last? That’s a difficult question involving complex factors. Sometimes there are dictators, sometimes there is scarcity, typically it is senseless.
The goal for this assignment is to answer that question for the simple case when war is completely random — that is, when two halves of a shuffled deck of cards go to war with each other.
The first two portions of this assignment attend to the representation of playing cards and the use of randomness to shuffle a deck. After that, you will develop a Monte Carlo simulation — a fancy way to say “using randomness” — to measure how long purely random wars last.
The starter code contains the following files, with several tasks to complete:
Cards.hs
Ord
ering on Card
s.fullDecks
, one with aces high and one aces
low.RawShuffle.hs
removeRandom :: [a] -> StdGen -> ((a, [a]), StdGen)
shuffle :: [a] -> StdGen -> ([a] , StdGen)
MonadicShuffle.hs
. Port the above to use the
Monad
ic interface of State
:
removeRandom :: [a] -> State StdGen (a, [a])
shuffle :: [a] -> State StdGen [a]
RandomWars.hs
State.hs
. Same as from class; do not modify.
Tester.hs
. Some tests for card ordering and
shuffling.
random-wars.cabal
cabal run tests
(card ordering and shuffling)cabal run random-wars
(simulation)Haskell provides expressive building blocks for representing custom data: type synonyms, datatypes, record type syntax, type classes, derived instances, and more that will we not cover in this course.
Representing the card values and suits in a standard 52-card deck is straightforward. (“Pip cards” and “numeral cards” are synonymous.)
data CardValue
= King
| Queen
| Jack
| PipCard Int -- From 1 to 10
deriving (Show)
data Suit
= Hearts
| Diamonds
| Spades
| Clubs
deriving (Show)
Then we might define a Card
to be a pair of these.
data Card =
Card CardValue Suit
We will order suits
alphabetically. Less clear is how to represent two different
standard orderings of card values: aces high and aces low. (The rules of war
dictate that aces are high. But to implement Card
s more
generally so they may be used in different games, we will support the
choice between aces high or aces low.)
The essential question is: how to arrange two implementations of the following?
(<=) :: Card -> Card -> Bool
One option is to keep track of whether aces are high or low “off to the side.” We could define two separate functions
lessThanOrEquals_AcesHigh :: Card -> Card -> Bool
lessThanOrEquals_AcesLow :: Card -> Card -> Bool
and then pick one depending on how acesHigh
is
configured for a game:
lessThanOrEquals :: Bool -> Card -> Card -> Bool
lessThanOrEquals acesHigh
| acesHigh = lessThanOrEquals_AcesHigh
| otherwise = lessThanOrEquals_AcesLow
This would be fine for our little program. But ideally, we would
actually plug our definitions of ordering into the Ord
interface.
Another option is to “hide” the different notions of ordering from
the type system, embedding them entirely within the run-time behavior of
Card
-manipulating programs.
data Aces = AcesHigh | AcesLow -- essentially a Bool
data Card =
Card AcesHigh CardValue Suit
instance Ord Card where
(<=) :: Card -> Card -> Bool
Card acesHigh1 value1 suit1 <= Card acesHigh2 value2 suit2
| acesHigh1 /= acesHigh2 =
error "(<=) @Card: Don't mix aces-high and aces-low cards!"
| acesHigh1 =
...
| otherwise =
...
A downside is that we may accidentally mix aces-high and aces-low cards in the same deck. It is easy to avoid violating this intended invariant, but let’s strive for a more direct route. (This approach also suffers the minor affront that these ordering flags need to be carried around in the run-time data.)
A third option, which is common when multiple instances for the same type-and-class are desired, is to define wrapper types — distinct datatypes for each of the desired orderings:
data CardAcesHigh = CardAcesHigh Card
data CardAcesLow = CardAcesLow Card
instance Ord CardAcesHigh where
(<=) :: CardAcesHigh -> CardAcesHigh -> Bool
card1 <= card2 = ...
instance Ord CardAcesLow where
(<=) :: CardAcesLow -> CardAcesLow -> Bool
card1 <= card2 = ...
This would work out just fine. But if there were a suitable
alternative that did not involve wrapping and unwrapping
Card
values, that would be preferable.
Our final approach combines the best of the above: we will define a
single Card
datatype (no wrappers) to be a member of
Ord
with two different implementations that are tracked
completely at compile-time (no extra run-time data). The trick is to
define Card
with a phantom type
variable.
data Card (a :: *) =
Card CardValue Suit
Card
now requires one type argument (Card
has kind * -> *
), but Card
values do not
rely on that type at all. So, we can instantiate that type argument
however we like, for example:
> :set -fprint-explicit-foralls
> :t Card King Hearts
Card King Hearts :: forall {a}. Card a
> :t Card @Int King Hearts
Card @Int King Hearts :: Card Int
> :t Card @Bool King Hearts
Card @Bool King Hearts :: Card Bool
> :t Card @(Int -> Bool) King Hearts
Card @(Int -> Bool) King Hearts :: Card (Int -> Bool)
The type argument has no effect on how Card
values are
represented and manipulated at run-time. It does, however,
allow the type system to distinguish between different types of
Card
s.
For our purposes, we will define two (singleton) types to represent different orderings…
data AcesHigh = AcesHigh
data AcesLow = AcesLow
… and then define two instances for Card
depending on its type argument:
instance Ord (Card AcesHigh) where
(<=) :: Card AcesHigh -> Card AcesHigh -> Bool
card1 <= card2 = ...
instance Ord (Card AcesLow) where
(<=) :: Card AcesLow -> Card AcesLow -> Bool
card1 <= card2 = ...
Interesting!
This general idea is not just a spooky Haskell trick. A quick search reveals enthusiastic blog posts about phantom types in F# (here’s the article from which I cribbed the photo above), Elm, Swift, Flow, TypeScript… Maybe there is hope for the world.
Cards.hs
)Implement the Ord (Card AcesHigh)
and
Ord (Card AcesLow)
instances — and any instances upon which
they depend. For full style points, rely on derived instances for
CardValue
and Suit
to simplify the two
Card
-Ord
ering instances.
Hint: In addition to adding additional
classes to the deriving
clauses, you may reorder the data
constructors in CardValue
and Suit
. (But make
sure the data constructor definitions are otherwise exactly the
same.)
Implement the following function to produce a full deck (52 cards) in
ascending order, treating aces high (True
) or low
(False
):
fullDeck :: Bool -> [Card a]
Each type of full deck is thus:
fullDeckAcesHigh :: [Card AcesHigh]
fullDeckAcesLow :: [Card AcesLow]
fullDeckAcesHigh = fullDeck True
fullDeckAcesLow = fullDeck False
For full style points, make good use of local variables (via
let
or where
), lists, functions, etc. You
should not, for example, write a list literal with 52, or 13,
elements.
Check that your implementation produces:
> :load Tester
> fullDeckAcesHigh
[2♣,2♦,2♥,2♠,3♣,3♦,3♥,3♠,4♣,4♦,4♥,4♠,5♣,5♦,5♥,5♠,6♣,6♦,6♥,6♠,7♣,7♦,7♥,7♠,8♣,8♦,8♥,8♠,9♣,9♦,9♥,9♠,10♣,10♦,10♥,10♠,J♣,J♦,J♥,J♠,Q♣,Q♦,Q♥,Q♠,K♣,K♦,K♥,K♠,A♣,A♦,A♥,A♠]
> fullDeckAcesLow
[A♣,A♦,A♥,A♠,2♣,2♦,2♥,2♠,3♣,3♦,3♥,3♠,4♣,4♦,4♥,4♠,5♣,5♦,5♥,5♠,6♣,6♦,6♥,6♠,7♣,7♦,7♥,7♠,8♣,8♦,8♥,8♠,9♣,9♦,9♥,9♠,10♣,10♦,10♥,10♠,J♣,J♦,J♥,J♠,Q♣,Q♦,Q♥,Q♠,K♣,K♦,K♥,K♠]
The next task is to support randomly shuffling a deck of cards — or any type of list, for that matter.
A common requirement for computer programs is that there should be some randomness in the behaviour of the program. For this you need a random number generator, which can generate an unlimited sequence of random numbers. Typically these numbers are not truly random, but pseudo-random.
A pseudo-random number generator generates a sequence of numbers based on an initial state s using a function f. The states are s_1 = f (s), s_2 = f (f (s)), s_3 = f (f (f (s))), etc. Each random number is then derived from each of these intermediate states (typically by taking a small proportion of the 0’s and 1’s from the state).
The f function has to be chosen so that subsequent states have no obvious correlation, so for most intents and purposes the numbers generated are random, but the sequence of numbers is in fact entirely determined by the initial state s.
In many programming languages, the random number generator state is hidden from the programmer, but because of the purity of Haskell, the random number generator state must explicitly be passed through your program — or “implicitly” with stateful functions, as covered here.
RawShuffle.hs
)To start, implement the following with “raw” stateful functions of
the form StdGen -> (output, StdGen)
— in such functions,
StdGen
is the type of “state objects” that are passed along.
Given a list, pick a random index into the list, then return the element at the given index as well as the list with that element removed. This is a basic function that can help you shuffle.
removeRandom :: [a] -> StdGen -> ((a, [a]), StdGen)
The uniformR
library function is a handy way to generate an index:
uniformR :: (RandomGen g, UniformRange a) => (a, a) -> g -> (a, g)
Then (you are required to) define and use the helper function:
removeAt :: Int -> [a] -> (a, [a])
Next implement (deck) shuffling:
shuffle :: [a] -> StdGen -> ([a], StdGen)
Write this function recursively: (a) handling the base case for an
empty list, and (b) handling the non-empty lists inductively by calling
removeRandom
and recursing.
A shuffling function should induce a uniform probability distribution over all permutations — all orderings of the cards should be equally likely. Test the uniformity of shuffles by looking at small decks and checking statistics.
Some deterministic tests (also accessible via
cabal run tests
):
> :load Tester
> let g0 = mkStdGen 22300
> let (out1, g1) = RawShuffle.removeRandom [1..5] g0
> let (out2, g2) = RawShuffle.removeRandom [1..5] g1
> let (out3, g3) = RawShuffle.removeRandom [1..5] g2
> (out1, out2, out3)
((4,[1,2,3,5]),(1,[2,3,4,5]),(3,[1,2,4,5]))
> let g0 = mkStdGen 22300
> let (list1, g1) = RawShuffle.shuffle [1..5] g0
> let (list2, g2) = RawShuffle.shuffle [1..5] g1
> let (list3, g3) = RawShuffle.shuffle [1..5] g2
> (list1, list2, list3)
([4,3,1,5,2],[3,1,5,2,4],[1,5,4,2,3])
And some non-deterministics tests via initStdGen
:: IO StdGen
:
> :load Tester
> fst . RawShuffle.removeRandom [1..5] <$> initStdGen
> fst . RawShuffle.removeRandom [1..5] <$> initStdGen
> fst . RawShuffle.removeRandom [1..5] <$> initStdGen
> fst . RawShuffle.shuffle [1..5] <$> initStdGen
> fst . RawShuffle.shuffle [1..5] <$> initStdGen
> fst . RawShuffle.shuffle [1..5] <$> initStdGen
MonadicShuffle.hs
)Copy-paste your solutions from RawShuffle.hs
…
… and then port the following definitions to now use the
Monad
ic interface of State
:
removeRandom :: [a] -> State StdGen (a, [a])
shuffle :: [a] -> State StdGen [a]
For full style points, these should not call
{run,eval,exec}State
nor explicitly manipulate
StdGen
values. Use
Functor
-Applicative
-Monad
operations to hide such plumbing.
The deterministic test results should be the same. For example:
> (evalState $ MonadicShuffle.shuffle [1..5]) $ mkStdGen 22300
[4,3,1,5,2]
The random test results should be, well, random:
> (evalState $ MonadicShuffle.shuffle [1..5]) <$> initStdGen
> (evalState $ MonadicShuffle.shuffle [1..5]) <$> initStdGen
> (evalState $ MonadicShuffle.shuffle [1..5]) <$> initStdGen
RandomWars.hs
)How long do purely random wars last? Your final task is to design and implement a simulation that produces an answer to this question.
Your simulation should generally follow the rules of war between two players, starting with a full deck (done!), shuffling it (done!), dealing two halves (a small exercise), and then beginning battle. Keep going until one of the players runs of out cards, counting the number of turns along the way. Run some reasonable number of trials, and report the average number of turns among the completed trials.
To fend off games that are not converging, as well as buggy
simulations, it is recommended to pick some threshold for maximum number
of steps at which point a trial is abandoned. You can always increase,
or remove, the threshold as you gain confidence that your simulation
will terminate in a reasonable amount of time. In the meantime, be ready
to killall random-wars
(or some such) from another
terminal.
This assignment does not specify every decision required to turn the rules of war into executable code. Such design decisions are for you to make, and document using comments.
Compared to the discussion about aces-high and aces-low orderings
from before, in war — and many other card games — the suit does
not actually contribute to the comparison, only the card value.
It’s up to you how to handle this. For example, you could: add a third
kind of ordering on Card
values; refactor the
CardValue
type to take its own phantom type variable,
passed down from the enclosing Card
; or you could simply
project CardValue
s out of the existing Card
definitions. If you choose to make any changes to Card
s
themselves, please define a new Card
type local to the
RandomWars.hs
file; do not make any changes to
Cards.hs
from before.
The main
definition, triggered by
% cabal run random-wars
should run your experiment from start to finish. Choose a number of trials and other configuration parameters such that the running time is, say, between 1 and 60 seconds. We may or may not actually run your simulations. Instead, you should copy-and-paste the average number that comes out from one of your own invocations into the starter file:
howLongDoWarsLast :: Int
The point of this exercise is to write some beautiful code involving randomness, not to discover new truths about war. (But if you did, that would be some seriously functional programming…)
So, consider that a simple search about simulating war will turn up many endeavors similar to ours. It’s totally fine to read these and draw from their approaches — as usual, cite your sources, explain what you’ve learned from them, and how they influenced your work.
Remember, the goal is to use the State StdGen
monad to
“automatically” thread the random number generators through your code;
the bind (>>=
) function will handle transmitting the
state behind the scenes.
Your simulation should (i) call initStdGen
only once,
and (ii) call evalState
only once when kicking off a call
to simulateGames :: Int -> State StdGen [Int]
. (And
these two expressions are already in the starter code.)
To figure out if a function needs to be in the
State StdGen
monad, ask yourself, “If I call this function
twice in sequence, do I want different results the second time?” If yes,
the function needs to be monadic so the generator is properly
transmitted from one call to the next. Also, the function itself should
be used in a monadic context so calls to it are properly glued
together with bind (>>=
). That is, rely on the monad
so you do as little explicit management of the state as possible.
If you find yourself using evalState
(or
runState
) elsewhere, or are unpacking the generator in
every random function, you are defeating the purpose of the
State StdGen
monad and will lose points. Handling the
transmission of state is (>==)
’s job.
Here is an outline of the grading rubric (10 points total):
2.0
Card ordering
1.5
Correctness0.5
Style (use of derived instances:
Eq
/Ord
×
CardValue
/Suit
)2.0
Full decks
1.5
Correctness0.5
Style2.0
Shuffling
1.0
Raw1.0
Monadic3.0
Simulation1.0
Clean code (based on the style
guidelines)Once you are finished, check that you add
ed,
commit
ted, and push
ed everything to your
repository before the deadline (or within an eligible late period):
https://github.com/UChicago-PL/cs223-fa23-pa-7-random-wars-USERNAME
Card AcesHigh
and
Card AcesLow
requires the FlexibleInstances
language extension, which is enabled by default in GHC2021
.