πΊπ¦ 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:
- Non-local variables and closures
Functions that maintains the environment (and therefore the variables) that was in-scope when the function was created. - 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. - 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