Ruby has economy-class functions

Ruby has economy-class functions

πŸ‡ΊπŸ‡¦ I’m joining the Ruby community in calling for an end to Russia’s unjust and illegal war in Ukraine! Please donate to Ukrainian efforts here and learn more here. πŸ‡ΊπŸ‡¦

Ruby has economy-class functions, not first-class functions. And that’s okay! It’s a great language anyway.

Let’s talk about what first-class functions look like. Per the Wikipedia entry, a programming language is considered to support first-class functions if it:

  1. Non-local variables and closures
    Functions that maintains the environment (and therefore the variables) that was in-scope when the function was created.
  2. Anonymous and nested functions
    Functions that aren’t bound to a variable. Why? So we can pass them into other functions without first needing to assign them to a variable. And “nested function” just means a function that is defined while calling another function.
  3. Higher order functions
    Functions that can take other functions as arguments and return them as well.

The good news is that Ruby supports most of these use cases! But not in a way that I’d consider first-class. Economy, perhaps, but not first-class. Let’s talk about each one in turn

Non-local variables and closures

Ruby supports this aspect of first-class functions!

a = 1
#=> 1
b = 2
#=> 2
d = lambda {|c| a + b + c}
#=> #<Proc:0x0000000108d29d90 (irb):3 (lambda)>
d.(3)
#=> 6

Verdict: βœ… Arguably Ruby popularized the use of anonymous functions in other languages, even to the extent of inspiring Rust’s syntax for anonymous functions.

Anonymous and nested functions

Again, another case where Ruby shines. If you’ve provided a block to an argument, you’ve used Ruby’s fantastic language-level support for anonymous functions:

array = (1..15).to_a
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
             πŸ‘‡ anonymous function!
array.select {|number| number.even?}
#=> [2, 4, 6, 8, 10, 12, 14]

Nesting functions is a less common pattern, but it is slightly more awkward, and you have to know things:

def uses_a_function(func)
      πŸ‘‡ but why?
  func.call(1,2)
end
                πŸ‘‡ πŸ€”
uses_a_function(->(a, b) {a + b})
#=> 3

If this particular knowing of things is troubling to you, it should give you a sense of deep foreboding for the next section.

Verdict: βœ… who needs nested functions, anyway?

Higher order functions

Next, let’s try to return functions from functions in Ruby:

def returns_function
  def addition(a, b)
    a + b
  end
end

Easy! Well, not really:

add = returns_function
#=> :addition
add(1,2)
(irb):10:in `<main>': undefined method `add' for main:Object (NoMethodError)

You can do this pretty handily in Javascript:

let returnsFunction = function() {
  return function(a,b) {return a + b };
}
let addition = returnsFunction();
addition(1,2);
// 3

You can do this in Ruby, it’s just more…economy class. The aisles are tighter and you have a longer line to board:

def returns_function
  Proc.new do |a,b|
    a + b
  end
  # or you could use a lambda:
  ->(a,b) {a + b}
  # ...which is shorthand for:
  lambda {|a,b| a + b}  
end
addition = returns_function()

And then you can use it like a normal function…except that you can’t because it’s a Proc (or a Lambda) and not a method as supported by the language:

irb(main):008:0> addition(1,2)
(irb):8:in `<main>': undefined method `addition' for main:Object (NoMethodError)

So you have to get back in the economy class line:

addition.call(1,2)
#=> 3
# or the shorthand version:
addition.(1,2)
#=> 3

Alternately, you can use the def...end syntax in Ruby, as long as you know that a method definition returns the method’s Symbol instead of the actual Method object:

def returns_function
  def addition(a, b)
    a + b
  end
end
add = returns_function()
#=> :addition
# Note that calling returns_function has actually defined the addition method in the caller's scope, but let's pretend we didn't know that
add.class
#=> Symbol
# Now we go from a Symbol to a method with the Object#method class:
add = method(add)
=> #<Method: Object#addition(a, b) (irb):2>
# And now we can finally call it:
add.(1,2)
#=> 3

You can do it, but the obstacles with the language dissuade you from currying, passing functions around, and calling them. Furthermore, even if you were to shoehorn Ruby into a first-class function usage patterns, you would have to know the differences between methods, procs, and lambdas, and how to call them. As a result, the large majority of the Ruby ecosystem does not use functions in a first-class manner, which is disappointing because the support for it is so close.

A large portion of support would be getting method references, i.e. a shorthand for Object#method which would let us refer to methods without the ceremony of calling method(:some_method's_symbol). In fact, support for method references was added at some point, but then removed. So instead of calling method(:some_method's_symbol), the proposal was to create a language level syntactical sugar for it using .:some_method's_symbol. And here is the feature request in Ruby’s bug tracker: https://bugs.ruby-lang.org/issues/13581#note-45.

The other part would be the awkwardness around calling the returned method, but that seems unavoidable at this point due to the ambiguity caused by Ruby not requiring parentheses in order to call a method.

Verdict: ⚠️ technically yes, but realistically no. It’s not great and it doesn’t lead to the sorts of usage patterns that you see in other languages with first-class functions.

Note: I have to point out that the .: syntax was proposed by Victor Shepelev aka zverok, who is a Ukrainian Ruby core member who is currently sheltering in Kharkiv, one of the many cities under attack by Russian forces. Read recent developments here, and zverok’s blog here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s