Search code examples
rubyhaskellclojureparadigms

Queues instead of method chaining and rules instead of conditionals in Ruby


Rich Hickey describes paradigms from Clojure and Haskell in his talk Simple Made Easy. As a ruby/rails programmer (that's all I truly know), I loved his ideas, but didn't understand 2 of them:

  • Using Queues, not Method Chaining
  • Rules instead of Conditionals

Using Queues instead

Obviously, in Rails we love method chaining, but I wanted to understand what a Queue would look like in Ruby the way he described it (54:54 in the Video):

If thing A calls thing B, you just complected it. You have a when and where thing. A has to know where B is in order to call B. When that happens is whenever that happens is when A does it. Stick a Queue in there.

Rules vs Conditionals

He talks about not using conditionals or switch statements but Rules instead (30:00 in Video).

This I simply don't get at all in terms of Ruby. How do I make decisions without using conditionals?

Thanks all, Justin


Solution

  • Hello, Queues

    The idea here is that, instead of passing a value directly from one object to another, we can decouple them by sticking a queue between them.

    Let's say we were modelling a farmer collecting the eggs from a chicken. The chicken produces eggs, the farmer collects them. A farmer's shift is finished when they've collected five eggs. Normally, we might write something like this:

    class Chicken
        def initialize(name)
                @name = name
        end
    
        def lay_egg
                sleep random(3)
                "an egg from #{@name}"
        end
    end
    
    class Farmer
        def initialize(name, chicken)
                @name           = name
                @chicken        = chicken
        end
    
        def work_shift
                5.times do
                        egg = @chicken.lay_egg
                        puts "#{@name} got #{egg}"
                end
        end
    end
    
    betsy       = Chicken.new "Betsy"
    fred        = Farmer.new "Fred", betsy
    fred.work_shift
    

    So, the farmer waits by the chicken and picks up eggs as they come. Great, problem solved, go to the fridge and get a beer. But what if, say, we bought a second chicken to double our egg production? Or, what if we wanted to test our farmer's dexterity by having them pick up eggs from a carton?

    Because we've coded the farmer to require a chicken, we've lost the flexibility we need to make these kind of decisions. If we can decouple them, we'll have a lot more freedom.

    So, let's stick a queue between them. The chicken will lay eggs at the top of a chute; the farmer will collect eggs from the bottom of the chute. Neither party relies directly on the other. In code, that might look like this:

    class Chicken
        def initialize(name, chute)
                @name   = name
                @chute  = chute
                Thread.new do
                        while true
                                lay_egg
                        end
                end
        end
    
        def lay_egg
                sleep rand(3)
                @chute << "an egg from #{@name}"
        end
    end
    
    class Farmer
        def initialize(name, chute)
                @thread = Thread.new do
                        5.times do
                                egg = chute.pop
                                puts "#{name} got #{egg}"
                        end
                end
        end
    
        def work_shift
                @thread.join
        end
    end
    
    chute       = Queue.new
    betsy       = Chicken.new "Betsy", chute
    fred        = Farmer.new "Fred", chute
    fred.work_shift
    

    Except that now, we can easily add a second chicken. This is the stuff dreams are made of:

    chute       = Queue.new
    betsy       = Chicken.new "Betsy", chute
    delores     = Chicken.new "Delores", chute
    fred        = Farmer.new "Fred", chute
    fred.work_shift
    

    You could imagine how we might also, say, load up a chute with a bunch of eggs to test the farmer. No need to mock a chicken, we just prep a queue and pass it in.

    Good-bye, Conditionals

    My answer to this is maybe a little more contentious, but a lot shorter. You could take a look at multimethods in Ruby, but the crux of the idea is forgoing closed, hardcoded logic paths in favour of open ones and, in fact, plain ol' polymorphism achieves exactly this.

    Whenever you call some object's method instead of switching on its type, you're taking advantage of Ruby's type-based rule system instead of hardcoding a logic path. Obviously, this:

    class Dog
    end
    
    class Cat
    end
    
    class Bird
    end
    
    puts case Bird.new
    when Dog then "bark"
    when Cat then "meow"
    else "Oh no, I didn't plan for this"
    end
    

    is less open than this:

    class Dog
        def speak
                "bark"
        end
    end
    
    class Cat
        def speak
                "meow"
        end
    end
    
    class Bird
        def speak
                "chirp"
        end
    end
    
    puts Bird.new.speak
    

    Here, polymorphism's given us a means of describing how the system behaves with different data that allows us to introduce new behaviour for new data on a whim. So, great job, you're (hopefully) avoiding conditionals every day!