Shadowing ruined my evening
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.
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
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] ]
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
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>>
I checked everywhere that resembled code that might be executed more than once, littered my program with
It kept happening! Grrr.
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.
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.