Find an article

Start typing to search the archive.

← All articles

Improve code readability with closures in Ruby

Reduce Ruby code complexity with closures.

Updated

Code readability is a very critical parameter for any project’s maintainability. A serious business needs to react to failures and fix bugs as soon as possible. That helps retain existing clients and make the service more attractive for new ones.

But how do we improve readability? One might think that installing, configuring, and following automatic tools such as Rubocop or Reek is more than enough. Indeed, these tools are great. They are highly recommended and really help a lot. Unfortunately, they are not a silver bullet. They cannot eliminate one important factor - distance to natural language. The closer code is to natural language, the better its readability. This is what everyone should strive for.

Languages that support higher-order functions have a nice concept called closures. Closures are just functions (anonymous or not) that are received by other functions as arguments. The receiving functions can optionally call the passed closures whenever it’s needed. An interesting fact is that closures are bound to the context from which they are passed. That allows intervening in the execution of distant contexts.

If that definition sounds cumbersome and scary, I recommend looking at this introduction by an example based on JavaScript.

Ruby has function equivalents called methods. Even though Ruby doesn’t allow having method names as arguments, it’s possible to implement closures.

Check out this example demonstrating that methods cannot be used as arguments:

def foo
  puts 'hello from foo'
end

def bar(f)
  f()
end

bar(foo) # => NoMethodError (undefined method `f' for main:Object)

Ruby has lambdas and procs to implement the closures concept.

Moving further, a method is an object like anything else. That means it’s possible to get an object that represents a method. Here is how to do that:

method(:foo) # => #<Method: main.foo>

But the example above will still not work:

bar(method(:foo)) # => NoMethodError (undefined method `f' for main:Object)

Execution of lambdas, procs, and Method objects with () is not possible. They have a #call method for that. Applying a fix with that knowledge:

def bar(f)
  f.call()
end
bar(method(:foo)) # => hello from foo

Ta-da! Finally, the closure works!

By the way, there is a shorthand for f.call(), that’s f.():

def bar(f)
  f.()
end
bar(method(:foo)) # => hello from foo

It’s rare in commercial development, but often used in popular open-source projects.

Enough theory. Switching into real work. Assume, we need to write an application that sends text messages to particular recipients. Look at the following code one might come up with implementing that idea:

User = Struct.new(:phone, :active)

class SMSGateway
  # @param phone [String]
  # @param message [String]
  def self.send_message(phone, message)
    puts "Hello #{phone}, #{message}"
  end
end

class MessageService
  # @param message [String]
  # @param recipients [Array<User>]
  def broadcast(message, recipients)
    recipients.each do |recipient|
      SMSGateway.send_message(recipient.phone, message) if recipient.active
    end
  end
end

recipients = [
  User.new('+12222222222', true),
  User.new('+13333333333', false)
]
service = MessageService.new
service.broadcast('have a good day!', recipients)

Now, picture how humans read the code. I will speak for myself. In order to grasp the code idea fast, I start from learning the application programming interface (API) first. I don’t start from nitty-gritty details. That works at least for me and I find this way to learn the code very useful.

So, I begin with these two lines:

service = MessageService.new
service.broadcast('have a good day!', recipients)

I see the API immediately. The MessageService#broadcast method is our “entry point”. At this point it’s already clear what it does intuitively. That’s because the method has a good name.

It’s not always like that, unfortunately. Imagine if someone named this method call or perform, then its responsibility would not be so obvious. In practice, there are examples that are completely confusing and even worse than an obscure call/perform/whatever. API and naming are very important! See that in this example.

Then I want to learn more details about #broadcast and jump into its implementation:

def broadcast(message, recipients)
  recipients.each do |recipient|
    SMSGateway.send_message(recipient.phone, message) if recipient.active
  end
end

It’s pretty straightforward, but nevertheless it already forces me to pause for a while and think. I need to keep at least 3 variables in my memory to understand this code. For this particular code, it’s not a problem. But in the real world a method like this is not just 3 lines of code, even though it sits at the very top of the API. It’s usually a huge piece of … (you know what I mean, right?) Therefore, the question of its improvement is of paramount importance.

Here is my solution that makes it better using the theory above:

class MessageService
  # @param message [String]
  # @param recipients [Array<User>]
  def broadcast(message, recipients)
    recipients.each(&send_message(message))
  end

  private

  # @param message [String]
  # @return [Proc] a closure sends the given message to the recipient
  def send_message(message)
    ->(recipient) { SMSGateway.send_message(recipient.phone, message) if recipient.active}
  end
end

Working example is located here.

I move all low-level details down to #send_message and use another transition from method to proc (that’s what & does). That allows me to use the method as a block expected by each.

#send_message returns a closure in the form of a proc constructed with -> shorthand. Consider it as a deferred message sent to a recipient that comes from the recipients.each iterator.

Read more about the “method to proc” technique here.

Agree or not, but the resulting #broadcast method implementation is way more readable than the previous version:

# @param message [String]
# @param recipients [Array<User>]
def broadcast(message, recipients)
  recipients.each(&send_message(message))
end

When I read it I immediately see what is happening just using my intuition and natural language knowledge. If I need more details I can jump into #send_message and learn even more details. When tracing a bug, stopping at this point could be enough. This point doesn’t have a lot of details anymore. Hence, it’s easier and faster to spot a bug.

As a bonus, the method separation gives an opportunity to document it and outlining types for params and returned values. Lack of types that draw a strict API is another pain in Ruby and we should do everything to improve it, I believe.

Happy complexity relief in your Ruby code!