Shadowing ruined my evening
[note] This post is about nothing, really.
Last night I was playing around with implementing solutions for problems from rosetta code in order to polish my Haskell skills before starting my first day in a developer role where I will be expected to use it daily. Since I have no prolonged experience with using it outside of some messing around with it here and there to learn it, it seemed like a good idea to practice a bit more.
Setup
I was implementing a solution for the Go Fish "problem", which is to create a simple game of Go Fish that the user can play against a machine. Seems pretty straight-forward: create an initial state, modify that state every turn until the deck is empty and then report who the winner is. So I got to work, defining a main function like this:
main :: IO ()
main = do
setup >>= start
print "Thanks for playing :)"
I also made some types to make my code more readable:
data Rank = Two | Three | Four | Five | Six
| Seven | Eight | Nine | Ten | Jack
| Queen | King | Ace
deriving ( Ord, Enum, Bounded, Eq )
data Suit = Diamonds
| Spades
| Clubs
| Hearts
deriving ( Ord, Enum, Bounded, Eq )
type Player = (String, Hand, Score)
type Card = (Rank, Suit)
type Deck = [Card]
type Hand = [Card]
type Score = Int
data State = Running Player Player Deck
| Finished String
In setup
, the deck would be created and shuffled (which requires actions within IO
because randomness):
setup :: IO Deck
setup = shuffle [ (r, s) | r <- [Two .. Ace],
s <- [Diamonds .. Hearts] ]
The created Deck
would be passed to the start
function, which would deal 9 cards for each player from the deck
included in the State
and pass it into the iter
function where the game loop with turns and stuff would run.
A pretty basic and elegant setup, in my opinion.
The code for this start
function looked like this:
start :: Deck -> IO ()
start deck = let (hand_a, deck) = deal 9 deck; in
let (hand_b, deck) = deal 9 deck; in
let human = ("Human", hand_a, 0);
robot = ("Robot", hand_b, 0);
state = Running human robot deck; in
iter state >>= (print . winner)
First, it takes 9 cards from the deck to use as "Human"'s opening hand. Then, it takes another 9 cards from the remainder
of the deck, which was also returned from the deal
function. The variable deck
is "updated" whenever a modified version
is returned from this function.
I defined some functions like winner
and iter
in a quick and dirty way (I made absolutely sure iter
in particular
had a base case, this isn't my first time using Haskell), and ran the program:
$ runghc GoFish.hs
GoFish.hs: <<loop>>
Bruh.
I checked everywhere that resembled code that might be executed more than once, littered my program with traceShowId
,
added print
to force evaluation in random places, but to no avail. All my recursive functions had base cases and I
definitely capped all my possibly infinite lists, and even the ones I knew were finite, just to be sure.
It kept happening! Grrr.
Punchline
After about an hour I tried renaming some variables because why not:
start :: Deck -> IO ()
start deck = let (hand_a, aaaa) = deal 9 deck; in
let (hand_b, bbbb) = deal 9 aaaa; in
let human = ("Human", hand_a, 0);
robot = ("Robot", hand_b, 0);
state = Running human robot bbbb; in
iter state >>= (print . winner)
... which did the trick.
$ runghc GoFish.hs
GoFish.hs: todo
CallStack (from HasCallStack):
error, called at GoFish.hs:47:19 in main:Main
So from the perspective of a Rust programmer, start
looks pretty innocent. I use shadowing a lot in my Rust programs
in this exact pattern because that's kinda how Rust works: I consume deck
, but because my function returns a modified
version of the deck, with the same type and the same role, I assign the updated version to the original identifier.
Haskell, however, does not like it when I do this. This is because definitions in Haskell are recursive, which makes a lot of sense for Haskell, where you don't worry about whether the size of a value is known at compile time or whether something is stored on the stack or not (in most cases). This is something I had knew already, but did I not consider this consequence while writing that particular function.
Conclusion
Basically, I just made a silly and completely insignificant mistake and wrote a blog post about it that is way too long. Why? Because what else am I gonna put here?
Naming things is hard. Which is why I prefer to re-use names if they refer to the same thing. In Rust, this means clearer and more concise code (in my opinion, at least). In Haskell, this means either an infinite series, or a headache.