Skip to content

Instantly share code, notes, and snippets.

@ms-ati
Created April 29, 2012 22:07
Show Gist options
  • Select an option

  • Save ms-ati/2553490 to your computer and use it in GitHub Desktop.

Select an option

Save ms-ati/2553490 to your computer and use it in GitHub Desktop.

Revisions

  1. ms-ati revised this gist Apr 30, 2012. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions 3nightclubs.rb
    Original file line number Diff line number Diff line change
    @@ -20,6 +20,10 @@
    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
  2. ms-ati revised this gist Apr 30, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 3nightclubs.rb
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@
    ##
    ## Based on Scala version here: https://gist.github.com/970717
    ##
    ## Demonstrates applicative validation in Ruby, inspired the blog post
    ## 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)
    ####
  3. ms-ati revised this gist Apr 30, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 3nightclubs.rb
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    ####
    ## Ruby port of "A Tale of 3 Nightclubs"
    ##
    ## Based on scala version here: https://gist.github.com/970717
    ## Based on Scala version here: https://gist.github.com/970717
    ##
    ## Demonstrates applicative validation in Ruby, inspired the blog post
    ## "An example of applicative validation in FSharpx"
  4. ms-ati revised this gist Apr 29, 2012. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions 3nightclubs.rb
    Original file line number Diff line number Diff line change
    @@ -3,9 +3,9 @@
    ##
    ## Based on scala version here: https://gist.github.com/970717
    ##
    ## Demonstrates applicative validation in Ruby, inspired the blog post:
    ## An example of applicative validation in FSharpx
    ## http://bugsquash.blogspot.com/2012/03/example-of-applicative-validation-in.html
    ## Demonstrates applicative validation in Ruby, inspired the blog post
    ## "An example of applicative validation in FSharpx"
    ## (http://bugsquash.blogspot.com/2012/03/example-of-applicative-validation-in.html)
    ####

    #
  5. ms-ati revised this gist Apr 29, 2012. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 3nightclubs.rb
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    ####
    ## Ruby port of "A Tale of 3 Nightclubs", using rumonade gem,
    ## Ruby port of "A Tale of 3 Nightclubs"
    ##
    ## Based on scala version here: https://gist.github.com/970717
    ##
  6. ms-ati revised this gist Apr 29, 2012. 1 changed file with 10 additions and 0 deletions.
    10 changes: 10 additions & 0 deletions 3nightclubs.rb
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,13 @@
    ####
    ## Ruby port of "A Tale of 3 Nightclubs", using rumonade gem,
    ##
    ## Based on scala version here: https://gist.github.com/970717
    ##
    ## Demonstrates applicative validation in Ruby, inspired 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
    #
  7. ms-ati revised this gist Apr 29, 2012. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions 3nightclubs.rb
    Original file line number Diff line number Diff line change
    @@ -11,6 +11,7 @@
    require 'rumonade'

    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?
  8. ms-ati created this gist Apr 29, 2012.
    179 changes: 179 additions & 0 deletions 3nightclubs.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,179 @@
    #
    # 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'

    Person = Struct.new(:gender, :age, :clothes, :sobriety) do
    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
    #