Вопрос

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

Это было полезно?

Решение

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!

Другие советы

Neither of these two points are terrifically well-embodied by Haskell. I think Haskell still leads to somewhat uncomplected code, but approaches the whole problem with both a different philosophy and different tools.

Queues

Roughly, Hickey wants to point out that if you are writing a method on an object which calls another object

class Foo
  def bar(baz)
    baz.quux
  end
end

then we've just hard-coded the notion that whatever gets passed into Foo#bar must have a quux method. It's a complection in his point of view because it means that Foo's implementation is inherently tied to the implementation of how an object passed to Foo#bar is implemented.

This is less a problem in Ruby where method invocation is much more like a dynamically dispatched message being send between objects. It simply means that an object passed to Foo#bar must somehow respond responsibly when given the quux message, not much more.

But it does imply sequentiality in message handling. If you instead sent the message down a queue to eventually be delivered to the resulting object then you could easily place an intermediator at that seam---perhaps you want to run bar and quux concurrently.

More than Haskell, this idea is taken to a logical extreme in Erlang and I highly recommend learning how Erlang solves these kinds of issues.

 spawn(fun() -> Baz ! quux)

Rules

Hickey repeatedly stresses that particular, hard-coded methods of doing branching complect things. To point, he doesn't enjoy case statement or pattern matching. Instead, he suggests Rules, by which I assume he means "production rules" systems. These produce choice and branching by allowing a programmer to set up a set of rules for when certain actions "fire" and then waiting until incoming events satisfy sufficient rules to cause actions to fire. The most well-known implementation of these ideas is Prolog.

Haskell has pattern matching built deeply into its soul, so it's hard to argue that Haskell immediately decomplects in this way... but there is a really good example of a rules system alive in Haskell—type-class resolution.

Probably the best known notion of this is mtl-style typeclasses where you end up writing functions with signatures like

foo :: (MonadReader r m, MonadState s m, MonadIO m, MonadCatch m)
    => a -> m b

where foo is completely polymorphic in the type m so long as it follows certain constraints—it must have a constant context r, a mutable context s, the ability to execute IO, and the ability to throw and catch exceptions.

The actual resolution of which types instantiate all of those constraints is solved by a rules system often (fondly or otherwise) called "type class prolog". Indeed, it's a powerful enough system to encode entire programs inside of the type system.

It's actually really nice and gives Haskell a sort of natural dependency-injection style as described by the mtl example above.

I think, though, that after using such a system for a long time most Haskellers come to understand that while rules systems are clever sometimes... they also can easily spin out of control. Haskell has a lot of careful restrictions to the power of type class prolog which ensure that it's easy for a programmer to predict how it will resolve.

And that's a primary problem with rules systems at large: you lose explicit control over what actions end up firing... so it becomes harder to massage your rules to achieve the kind of result you expect. I'm not actually certain I agree with Rich here that rules systems thus lead to decomplection. You might not be explicitly branching off information tied to other objects, but you're setting up a lot of fuzzy, long-range dependencies between things.

Queues

Using queues means split program into several processes. For example one process that receive emails only and push them into "processing" queue. The other process pull from "processing" queue and transform message somehow, put into "outgoing" queue. This allows to easily replace some parts without touching other. You can even consider doing processing in other language if performance is bad. If you write e=Email.fetch; Processor.process(e) you are couple all processes together.

Another advantage of queues is there may be many producers and consumers. You can easily "scale" processing part just by adding more "processing" processes (using threads, other machines etc). On the other hand you can launch more "email fetcher" processes too. This is complicated if you complect all in one call.

There is a simple queue in ruby http://ruby-doc.org/stdlib-1.9.3/libdoc/thread/rdoc/Queue.html and many others (rabbitmq, db-based etc)

Rules

Rules makes code uncomplicated. Instead of if-then-else you are encouraged to create a rules . Take a look at clojure core.match lib:

(use '[clojure.core.match :only (match)])

(doseq [n (range 1 101)]
  (println
    (match [(mod n 3) (mod n 5)]
      [0 0] "FizzBuzz"
      [0 _] "Fizz"
      [_ 0] "Buzz"
      :else n)))

You can write if(mod3.zero? && mod5.zero?) else if .... but it will be not so obvious and (more important) hard to add more rules.

For ruby take a look at https://github.com/k-tsj/pattern-match although I didn't use such libraries in ruby.

UPDATE:

In his talk Rich mentioned that prolog-like system can be used to replace Conditions with Rules. core.match is not so powerful as prolog but it can give you an idea of how conditions can be simplified.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top