Functional Programming in Ruby

July 21, 2019

[ruby] [functional-programming] [object-oriented-design]

A functional programming riff on a common coding test.

During my most recent round of interviewing, I was given the following problem:

Iteratively design, test and build a simple game of BlackjackBlackjack is the American variant of a globally popular banking game known as Twenty-One. .

I was given the following stub of a test spec to start:

# deck_spec.rb
RSpec.describe Deck do
  subject(:deck) { Deck.new }

  it "creates a new deck of cards" do
    expect(deck).to be_a(Deck)
    expect(deck.count).to eq(52)
  end

  it "can be shuffled"

  it "can draw a card"
end

Now, before we go on about the value of such coding tests, or whether this one is a particularly good or bad one, let’s just use this as a personal exercise: How can we solve this problem with code such that it doesn’t run afoul of one of the thornier problems in systems design: Keeping track of all the things in a system such that every change in the system is consistent. Avdi Grim made an excellent Ruby Tapas screencast about this problem and used Contextual Identity as part of the solution.

Let’s try a different approach, one that uses functional programming techniques insteadIf you’re using ReactJS, this will look quite similar to the state property. . Functional programming, at its simplest, enforces everything in the system to be immutable and that to make changes, one applies a function that returns a new and complete state of the system.

Lather, rinse, repeat.

So, let’s try to make a Blackjack system that operates in FP-ish way. Unlike functional programming languages that guarantee immutability, I’m going to use the ice_nine gem to freeze all of my Ruby objects.The Ruby Object class has a #freeze method, which prevents an element from further modifications but it doesn’t work for elements that are inside another element (nested elements).

# deck.rb
require "ice_nine"
require "ice_nine/core_ext/object"

class Deck
  attr_reader :cards

  def initialize(cards = self.class.default_cards)
    @cards = cards
    deep_freeze
  end

  def count
    cards.count
  end
end

This immediately diverts us into the question, “What is a Card object?” While it is certainly not a Value Object, it can be sorted and compared. When you want to get fancy, they have Suits as well. I’m going to spare you the development of these classes, because it distracts from the core of this article, but you can get copies of them from my blackjack-ruby repository. So, just shade your eyes a little, here’s how we get started, our initializer can create a default deck of cards.

# deck.rb
CARDS = [["A", "Ace", 1]] + (2..10).map { |x| [x.to_s, x.to_s, x] } + [["J", "Jack", 11]] + [["Q", "Queen", 12]] + [["K", "King", 13]]
CARDS.deep_freeze
# https://en.wikipedia.org/wiki/High_card_by_suit
SPADES = Suit.new("♤", "Spades", 4); CLUBS = Suit.new("♧", "Clubs", 1); HEARTS = Suit.new("♡", "Hearts", 3); DIAMONDS = Suit.new("♢︎", "Diamonds", 2)

class Deck
...
  def self.default_cards
    # https://www.rubytapas.com/2016/12/08/episode-459-array-product/
    [SPADES, CLUBS, HEARTS, DIAMONDS].product(CARDS).map { |(suit, card)| Card.new(suit, *card) }
  end
...
end

With the preliminaries out of the way, and our first test is now green, let’s go on to the next one:

# deck_spec.rb
RSpec.describe Deck do
...
  it "can be shuffled" do
    new_deck = deck.shuffle

    expect(new_deck).not_to be(deck)
    expect(new_deck.cards).not_to eq(deck.cards)
  end
...
end

The Ruby Array class implements a #shuffle method and in a normal class, we’d do something like this:

# deck.rb
def shuffle
  @cards = cards.shuffle
end

But that will fail because the Deck is frozen (I’m totally digging the informative error message).

FrozenError: can't modify frozen Deck

Remember, however, that our initializer accepts a collection of Cards…

# deck.rb
def shuffle
  self.class.new(cards.shuffle)
end

So the #shuffle method shuffles the current Deck, but instead of modifying it, it returns a brand new Deck using the shuffled cards in the initializer.

Next, what about #draw? We need to keep the Card that was drawn from the Deck, and we need a deck without the card in it. This ain’t Dogs Playing Poker, sweet cheeks.

A Friend In Need -- 1903, C.M. Coolidge. https://commons.wikimedia.org/wiki/File:A_Friend_in_Need_1903_C.M.Coolidge.jpg
# deck_spec.rb
it "can draw a card from the top of the deck" do
  card, new_deck = deck.draw

  expect(card).to eq(deck.top)
  expect(new_deck.count).to eq(deck.count - 1)
end

The return signature is an Array that we nicely destructure out into the two variables, card and new_deck. Because all we need to do is initialize a new deck with the desired cards, we can do this:

# deck.rb
def draw
  [cards.first, self.class.new(cards[1..-1])]
end

Believe it or not, we’ve completed the implementation of Deck! It has a very small surface area, but it does all the things that we want a deck of cards to do (for this card game, anyway). Let’s move on to making a card Game class.

Object Oriented Design is all about managing state through messages. Our Game (of Blackjack) class can deal out hands to get things started, and it can draw a card for a single player. Let’s start with the latter, first, because dealing out hands is very much like repeatedly drawing cards for each player in turn.

RSpec.describe Game do
  it "can deal out hands to each of the players"
  it "can draw a card for a player" do
    card = game.draw(game.players.first)

    expect(game.deck.count).to eq(51)
    expect(game.players.first.hand.count).to eq(1)
    expect(card).to be_a(Card)
  end
end

This test will fail because we haven’t defined what our “local” game is. Let’s keep this game simple, shall we? Our game of Blackjack will use 1 deck of cards and have 2 players: “Player A” and the “Dealer”.You’ll never see a game like this in Las Vegas. Ever.

# game_spec.rb
RSpec.describe Game do
  subject(:game) { 
    Game.new(
      Deck.new.shuffle, 
      [ Player.new("Player A"), Player.new("Dealer") ]
    ) 
  }

Whoops, our tests are still failing because I wrote the code that I wished I had, including the Player class that I haven’t written yet. Players are yet another immutable class and they have only 2 attributes: The player’s name and the hand of cards.

# player.rb
class Player
  attr_reader :name, :hand

  def initialize(name, hand = [])
    @name = name
    @hand = hand
    deep_freeze
  end
end

Okay, back to our test… The simple start to the problem is just tell the Deck to draw a Card from the deck

# game.rb
def draw(player)
  card, new_deck = deck.draw

  @players = players.map { 
    |p| p == player ? Player.new(p.name, p.hand + [card]) : p 
  }
  @deck = new_deck
end

Whoops, there’s that FrozenError: can't modify frozen Game exception that keeps us from cheating. So, how do we solve this? Does #draw create a brand new Game each time? That seems semantically odd. We’ve missed something in our modeling. What are we really doing when we draw a card? We’re changing the state of the Game, so let’s model each “turn” as well, a Turn.

# turn.rb
class Turn
  attr_reader :deck, :players, :description

  def initialize(deck, players, description = "")
    @deck = deck
    @players = players
    @description = description
    deep_freeze
  end
end

A Game becomes an ordered collection of Turns that are deterministically transformed from one turn to the next by one function, #draw. This is also our only point of mutability.

# game.rb
class Game
  attr_reader :history

  def initialize(deck, players)
    @history = [Turn.new(deck, players, "Initial game set up")]
  end

  # Some convenience accessors 
  # to look at the "current" state of the Game
  def latest_turn
    history.first
  end

  def deck
    latest_turn.deck
  end

  def players
    latest_turn.players
  end
...
end

and now rewrite #draw to push new Turns onto our game history.

# game.rb
# Draws a card from the deck
# Adds a new turn on the game history (most recent first)
# Returns the card that was drawn
def draw(player)
  card, new_deck = deck.draw
  next_turn = Turn.new(new_deck, update_player_hand(card, player), "#{card} => #{player}")
  history.unshift(next_turn)

  card
end

private

# Returns an updated player hand, very ReactJSish
def update_player_hand(card, player)
  players.map { |p| p == player ? Player.new(p.name, p.hand + [card]) : p }
end

Dealing out the initial hands is super easy, since #draw does all of the heavy lifting.

# game.rb
# Deal N cards to each player
def deal(cards)
  cards.times do
    players.each do |player|
      draw(player)
    end
  end
end

We’ve got a pretty good set up now. Let’s take the code for a spin.

game = Game.new(Deck.new.shuffle, [Player.new("Player A"), Player.new("Dealer")])
=> #<Game:...>
game.deal(2)
=> 2
puts game.history.map(&:description).reverse
Initial game set up
2♢︎ => Player A
2 => Dealer
Q♢︎ => Player A
9 => Dealer

Pretty neat, huh?

We can go in all sorts of directions, now. We still haven’t built any rules for Blackjack, yet, but now that we have a Game class to manage the state of the card table, we’ve got a strong foundation.

Functional Programming in Ruby - July 21, 2019 - Ken Mayer