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.
# 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.