Skip to content

Instantly share code, notes, and snippets.

@ms-ati
Created April 29, 2012 22:07
Show Gist options
  • Save ms-ati/2553490 to your computer and use it in GitHub Desktop.
Save ms-ati/2553490 to your computer and use it in GitHub Desktop.
A Tale of 3 Nightclubs (ruby port)
####
## Ruby port of "A Tale of 3 Nightclubs"
##
## Based on Scala version here: https://gist.github.com/970717
##
## Demonstrates applicative validation in Ruby, inspired by the blog post:
## "An example of applicative validation in FSharpx"
## (http://bugsquash.blogspot.com/2012/03/example-of-applicative-validation-in.html)
####
#
# Part Zero : 10:15 Saturday Night
#
# (In which we will see how to let the type system help you handle failure)...
#
#
# First let's define a domain. (All the following requires ruby 1.9 or compatible, and rumonade >= 0.3.0)
#
require 'pp'
require 'rumonade'
Sobriety = [:sober, :tipsy, :drunk, :paralytic, :unconscious]
Gender = [:male, :female]
Person = Struct.new(:gender, :age, :clothes, :sobriety) do
# accept a block to modify the copy before returning it
def copy
a_copy = dup
yield a_copy if block_given?
a_copy
end
end
#
# Let's define a trait which will contain the checks that *all* nightclubs make!
#
module Nightclub
# First CHECK age
def check_age(p)
if p.age < 18
Left("Too Young!")
elsif p.age > 40
Left("Too Old!")
else
Right(p)
end
end
# Second CHECK clothes
def check_clothes(p)
if p.gender == :male && !p.clothes.include?("Tie")
Left("Smarten Up!")
elsif p.gender == :female && p.clothes.include?("Trainers")
Left("Wear high heels")
else
Right(p)
end
end
# Third CHECK sobriety
def check_sobriety(p)
if [:drunk, :paralytic, :unconscious].include?(p.sobriety)
Left("Sober Up!")
else
Right(p)
end
end
end
#
# Part One : Clubbed to Death
#
# Now let's compose some validation checks
#
class ClubbedToDeath
include Nightclub
def cost_to_enter(p)
# PERFORM THE CHECKS USING Monadic flat_map operation (need to add for-expression sugar!)
check_age(p).right.flat_map do |a|
check_clothes(a).right.flat_map do |b|
check_sobriety(b).right.flat_map do |c|
Right(if c.gender == :female then 0 else 5 end)
end
end
end
end
end
# Now let's see these in action
module Test1
Ken = Person.new(:male, 28, ["Tie", "Shirt"], :tipsy)
Dave = Person.new(:male, 41, ["Tie", "Jeans"], :sober)
Ruby = Person.new(:female, 25, ["High Heels"], :tipsy)
# Let's go clubbing!
COSTS = [
ClubbedToDeath.new.cost_to_enter(Dave), # Left("Too Old!")
ClubbedToDeath.new.cost_to_enter(Ken), # Right(5)
ClubbedToDeath.new.cost_to_enter(Ruby), # Right(0)
ClubbedToDeath.new.cost_to_enter(Ruby.copy { |r| r.age = 17 }), # Left("Too Young!")
ClubbedToDeath.new.cost_to_enter(Ken.copy { |r| r.sobriety = :unconscious }) # Left("Sober Up!")
]
end
puts "Part One: Clubbed to Death"
pp Test1::COSTS
#
# Part Two : Club Tropicana
#
# Part One showed monadic composition, which from the perspective of Validation is *fail-fast*.
# That is, any failed check short circuits subsequent checks. This nicely models nightclubs in the
# real world, as anyone who has dashed home for a pair of smart shoes and returned, only to be
# told that your tie does not pass muster, will attest.
#
# But what about an ideal nightclub? One that tells you *everything* that is wrong with you.
#
# Applicative functors to the rescue!
#
class ClubTropicana
include Nightclub
def cost_to_enter(p)
# PERFORM THE CHECKS USING applicative functors, accumulating failure via a monoid (an Array in this case)
(check_age(p).lift_to_a + check_clothes(p).lift_to_a + check_sobriety(p).lift_to_a).right.map do |a,b,c|
if c.gender == :female then 0 else 7.5 end
end
end
end
#
# And the use? Dave tried the second nightclub after a few more drinks in the pub
#
module Test2
include Test1
COSTS = [
ClubTropicana.new.cost_to_enter(Dave.copy { |d| d.sobriety = :paralytic }), # Left(["Too Old!", "Sober Up!"])
ClubTropicana.new.cost_to_enter(Ruby) # Right(0)
]
end
puts "\nPart Two: Club Tropicana"
pp Test2::COSTS
#
# So, what have we done? Well, with a *tiny change* (and no changes to the individual checks themselves),
# we have completely changed the behaviour to accumulate all errors, rather than halting at the first sign
# of trouble. Imagine trying to do this in Java, using exceptions, with ten checks.
#
#
# Part Three : Gay Bar
#
# And for those wondering how to do this with a *very long list* of checks. Use inject:
# list_of_eithers.inject(:+)
#
class GayBar
include Nightclub
def check_gender(p)
if p.gender != :male
Left("Men Only")
else
Right(p)
end
end
def cost_to_enter(p)
checks = [method(:check_age), method(:check_clothes), method(:check_sobriety), method(:check_gender)]
checks.map { |chk| chk.call(p).lift_to_a }.inject(:+).right.map { |c| c.last.age + 1.5 }
end
end
module Test3
include Test2
COSTS = [
GayBar.new.cost_to_enter(Person.new(:male, 59, ["Jeans"], :paralytic)), # Left(["Too Old!", "Smarten Up!", "Sober Up!"])
GayBar.new.cost_to_enter(Ruby.copy { |r| r.clothes = ["Trainers"] }), # Left(["Wear high heels", "Men Only"])
GayBar.new.cost_to_enter(Ken) # Right(29.5)
]
end
puts "\nPart Three: Gay Bar"
pp Test3::COSTS
#
# As always; the point is that our validation functions are "static";
# we do not need to change the way they have been coded because we want to combine them in different ways
#
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment