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