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.

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.