Of Closures and Objects
- ruby closures oop fp
Of closures and objects
A couple of months ago one of my coworkers posted the following little koan (source) in our Slack chat and mentioned that he himself has not yet attained enlightenment:
The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said “Master, I have heard that objects are a very good thing — is this true?” Qc Na looked pityingly at his student and replied, “Foolish pupil — objects are merely a poor man’s closures.”
Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire “Lambda: The Ultimate…” series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.
On his next walk with Qc Na, Anton attempted to impress his master by saying “Master, I have diligently studied the matter, and now understand that objects are truly a poor man’s closures.” Qc Na responded by hitting Anton with his stick, saying “When will you learn? Closures are a poor man’s object.”
At that moment, Anton became enlightened.
Never one to miss a chance to put on my teaching hat, I came up with the following little example and the accompanying explanation and now finally took the time to turn it into a blog post.
Basic premise
A closure “closes” over it’s environment, so it combines state (said
environment) and behavior (i.e. what the closure actually does when executed).
It’s basically an object with a single method, which in the case of Ruby is
call
(also aliased to .()
). Does that sound familiar? It should, because
after all an object also consists of state (instance variables) and behavior
(methods).
Example code
# Closure
def make_counter
i = 0
-> { i += 1 }
end
counter1 = make_counter
counter1.()
#=> 1
counter1.()
#=> 2
counter1.()
#=> 3
counter2 = make_counter
counter2.()
#=> 1
# Object
class Counter
def initialize
@count = 0
end
# Let's name this method `call` for symmetry with the closure
def call
@count += 1
end
end
counter3 = Counter.new
counter3.()
#=> 1
counter3.()
#=> 2
counter4 = Counter.new
counter4.()
#=> 1
Looking at make_counter
the assignment of a starting value to i
sets up the
environment that will be “closed over”. It’s not hard to see how this
essentially equates to object initialization. We then return a lambda, which
has an initial state (i == 0
) and a method (.call
) to manipulate this state
in order to produce the next number. For our code’s users, counter1
and
counter2
which were created with *make_counter *behave in exactly the same way
as counter3
and counter4
which are instances of the Counter
class.
Doubt
At this point my colleague started to understand where this was headed, but he still had his doubts:
oh, i see, but it is a function not an object…
This is obviously true, but apart from lambdas being regular objects in Ruby,
our counters are more than “just” functions. Like objects they have internal
state (the closed over i
variable) and a way of modifying said state (calling
the lambda). This is functionally (no pun intended) equivalent to an object that
has an instance variable and a method for changing it.
Enlightenment
OK I see 🙂
The term “enlightenment” is of course used jokingly here, but my coworker finally did understand this parable, and I count this as another small victory in my ongoing quest of convincing people that functional programming and object-oriented programming are orthogonal, as opposed to say functional vs imperative programming.