Let's examine a problem that we could math our way out of, but assume we don't know how. We'll lean on our programming to teach us what we don't know.
The Monty Hall problem comes from an old game show called Let's Make a Deal, hosted by Monty Hall. In the show, Monty would show three closed doors to a contestant and explain that one holds a car while the other two hold goats. The contestant then chose a door. After this first pick, Monty would reveal one unchosen door that was hiding a goat. The contestant then faced a key second choice: keep their door or switch to the other closed door. After this final choice, the two doors were opened and the contestant kept whatever they selected. The goal, of course, was to win a car.
What we really to want to know: are the chances of winning better if we switch or don't switch after the reveal?
Let's see if we can find out by modeling the problem in code:
defmodule MontyHallProblem do def dont_switch do Enum . at(randomize_doors(), pick_door()) # no need to model the reveal or second choice in this case since it # doesn't change the outcome end def switch do game = randomize_doors() pick1 = pick_door() reveal = 0 .. 2 |> Enum . filter( fn i -> i != pick1 and Enum . at(game, i) == :lose end ) |> Enum . random() pick2 = hd([ 0 , 1 , 2 ] -- [pick1, reveal]) Enum . at(game, pick2) end defp randomize_doors, do : Enum . shuffle( ~w[win lose lose]a ) defp pick_door, do : Enum . random( 0 .. 2 ) end
Hopefully this code is a pretty direct translation of the earlier problem description. Note that we now have two public functions, one that simulates a game where we switch and another for when we don't.
Here's a random run of each one:
MontyHallProblem . dont_switch() |> IO . inspect()
:win
MontyHallProblem . switch() |> IO . inspect()
:lose
Now what we want to do is just try them both—a lot—and show the results.
Let's write a little code for that too:
defmodule MonteCarloSimulation do def analyze(event, measurement, repetitions \\ 10_000 ) do Stream . repeatedly(event) |> Stream . take(repetitions) |> Enum . count( fn result -> result == measurement end ) |> then( & :io_lib . format( "~.2f%" , [&1 / repetitions * 100 ])) |> to_string() end end
This code takes some function that simulates an event , runs it for a requested number repetitions , and counts how many times the result matches a provided measurement . The rest is just about showing pretty output.
You may have noticed from the module name that this technique is another "Monte." Monte Carlo simulations are just repeated random samplings used to measure results. This works thanks to the law of large numbers.
It may sound like I'm sneaking some math into this solution, but I suspect most of us intuitively understand this process even if we don't know the names for it. For example, how many heads will I get if I flip a coin three times?
MonteCarloSimulation . analyze( fn -> Enum . random([ :heads , :tails ]) end , :heads , 3 )
"0.00%"
I guess I'm not very lucky, but watch what happens if I keep cranking up the number of flips:
MonteCarloSimulation . analyze( fn -> Enum . random([ :heads , :tails ]) end , :heads , 30 )
"43.33%"
MonteCarloSimulation . analyze( fn -> Enum . random([ :heads , :tails ]) end , :heads , 300 )
"54.67%"
MonteCarloSimulation . analyze( fn -> Enum . random([ :heads , :tails ]) end , :heads , 3_000 )
"50.47%"
MonteCarloSimulation . analyze( fn -> Enum . random([ :heads , :tails ]) end , :heads , 30_000 )
"50.40%"
What if I asked: how many heads will I get if I flip 30,000 coins? I feel like a lot of folks would answer, "around 15,000," or, "about half." That's exactly what happens. The more tests we do the more the results converge on the expected probability.
We can use that to discover the answer to the Monty Hall problem!
%{ "Don't switch win chance" => MonteCarloSimulation . analyze( & MontyHallProblem . dont_switch / 0 , :win ), "Switch win chance" => MonteCarloSimulation . analyze( & MontyHallProblem . switch / 0 , :win ) }
%{"Don't switch win chance" => "33.40%", "Switch win chance" => "66.78%"}
What we see there is mathematically correct, or at least very close to it. The don't switch result is probably the easiest to come to terms with. We selected one of three available doors and never changed, so we'll be right about a third of the time.
The switching result is a little trickier. If our initial guess is right a third of the time, that means it's wrong two thirds of the time. Put another way there's a two thirds chance that the car is behind one of the two doors we didn't initially pick. Then Monty is kind enough to rule one of those two out, shifting the full two thirds chance to the one remaining unselected door. If we switch to that one, we double our chances to win!
Programming can show you what to do, even if it doesn't explain why.