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 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
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.
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.
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 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With