Unlocking the Power of Lambdas in Ruby
Lambdas are a powerful feature of the Ruby language. They allow you to wrap logic and data into a portable package. In this post, we’ll cover how and when to use lambdas. You'll also learn about the difference between lambdas and Procs, and the performance profile of lambda functions. The code examples have been tested with 2.6 and 2.7 and should work with most modern Rubys.
Use the links below to skip ahead in the tutorial:
- What is a Lambda Function
- What Purpose to Lambdas Serve
- Ruby Proc vs. Lambda
- Ruby Lambdas in Use
- Performance
- Closing Thoughts
What is a Lambda Function?
A lambda function is a general software concept, not specific to Ruby. They are available in many programming languages. A lambda function encapsulates control flow, parameters and local variables into a single package assigned to a variable or used inline. If assigned to a variable, it can be passed to other functions or stored in data structures, just like a more typical variable containing a string or float. A lambda function can then be executed far from the code location where it was defined. Lambda functions are often called anonymous functions or a function literal.
Languages that support lambda functions are often said to have ‘first-class functions’. The lambda function is a middle ground between normal functions, which have no state, and full-blown objects, with state and multiple methods to operate on that state. Because of their simplicity and power, lambda functions can help you write compact, elegant programs.
What is a Lambda in Ruby?
With Ruby, the lambda
keyword is used to create a lambda function. It requires a block and can define zero or more parameters. You call the resulting lambda function by using the call
method.
Here’s a normal Ruby function:
def my_function
puts "hello"
end
You call this using the name:
my_function
The output:
hello
You can define a lambda function with the same output:
my_lambda = lambda { puts "hello" }
Using the name outputs nothing as the lambda function is not executed:
my_lambda
The call method takes as many arguments as you’ve defined, in this case zero:
my_lambda.call
The output:
hello
There is more than one way to call a lambda function:
my_lambda = lambda { puts "hello" }
my_lambda.call
my_lambda.()
my_lambda.[]
my_lambda.===
The output:
hello
hello
hello
hello
You can also create a lambda with the literal lambda operator, which looks like this and can have zero or more arguments: ->
my_lambda = -> { puts "hello" }
my_lambda_with_args = -> (v) { puts "hello "+v }
my_lambda.call
my_lambda_with_args.call("newman")
The output:
hello
hello newman
The literal operator is succinct and commonly used. However, in the interests of clarity, I’ll use the lambda
keyword for the rest of this post.
You can also use default arguments with a Ruby lambda:
my_lambda = lambda {|name="jerry"| puts "hello " +name}
my_lambda.call
my_lambda.call("newman")
The output:
hello jerry
hello newman
Finally, the block you are passing to a lambda can be either a single line block with curly braces or a multi-line block with do and end:
my_one_line_lambda = lambda { puts "hello" }
my_one_line_lambda.call
my_multi_line_lambda = lambda do
puts "hello"
end
my_multi_line_lambda.call
The output:
hello
hello
Please note that a Ruby lambda function is different from an AWS Lambda function, which is an AWS service that executes code without requiring you to run a server. AWS Lambda functions can be written in Ruby but are entirely different from Ruby lambda functions.
What Purpose do Lambdas Serve?
The additional indirection that lambda functions provide give you flexibility when writing a Ruby program. For instance, you can pass a lambda to a function:
double_it = lambda { |num| num * 2 }
triple_it = lambda { |num| num * 3 }
def apply_lambda(lmbda, number)
puts lmbda.call(number)
end
apply_lambda(double_it, 10)
apply_lambda(triple_it, 20)
The output:
20
60
You can also create an array of lambdas to execute in a pipeline:
double_it = lambda { |num| num * 2 }
triple_it = lambda { |num| num * 3 }
half_it = lambda { |num| num / 2 }
value = 5
lambda_pipeline = [double_it, triple_it, half_it]
lambda_pipeline.each do |lmb|
value = lmb.call(value)
end
puts value
The output:
15
Of course, this pipeline is overengineered. This calculation would be better done with a statement ( num = num * 2 * 3 / 2
). If the lambda functions are defined elsewhere, pulled from configuration at runtime or are more complex, a processing pipeline lead to clearer code.
A lambda has an execution context, represented in Ruby by a Binding object. This is the environment in which your code executes, including, among other things, local variables. This means that lambdas can be closures which allow the code in the function to access these captured local variables. Here’s an example:
def build_lambda
output = "output from function"
return lambda { puts output }
end
output = "output from top level"
my_lambda = build_lambda
my_lambda.call
What do you think will be printed? The top-level output variable or the output variable from within the lambda?
The output:
output from function
We see “output from function”. The lambda function retains the local variable values at the time it was defined. To illustrate this further, here is a dynamic lambda creation function:
def build_lambda(text)
my_text = text
return lambda { puts my_text }
end
my_lambda = build_lambda("first function")
another_lambda = build_lambda("second function")
my_lambda.call
another_lambda.call
The output:
first function
second function
The following are specific situations in which you might want to use a Ruby lambda.
Encapsulating complicated logic
With a Ruby lambda, you may put logic and pass it to other code as a parameter. You could do the same by creating a class with a method, creating an object, and then passing that object. But if you don’t need much state and only need one function on your object, a lambda function can provide a simpler way to convey the intent.
An in-memory state machine or data pipeline
As seen above, you can chain lambdas. If you have in-memory data that transitions between states in a deterministic manner, such as in a state machine or workflow, you can represent each of the transformations as a lambda function. This will allow you to assemble and reorder such transformations easily.
Callbacks
Lambdas are perfect for simple callbacks. When used in that way, they can be defined close to where they will be executed, or even inline.
ActiveRecord scopes
ActiveRecord scopes, used in Rails applications, are commonplace to see a lambda function, at least for web developers. These scopes must be callable because they should be evaluated at run time.
For instance, if you want your controller to show articles published in the last week, you’d write a scope like this:
scope :new_posts, lambda { where("created_at > ?", 1.week.ago) }
If this wasn’t a lambda, then 1.week.ago
would be evaluated when the class is loaded, rather than when the lambda runs. That is not the correct behavior and would be more incorrect as time went on.
Rails checks and doesn’t allow such behavior for scopes. This code will not execute and throws a message: "The scope body needs to be callable"
scope :new_posts_broken, where("created_at > ?", 1.week.ago)
Preventing a collection from being preloaded in active admin
Similar to ActiveRecord, you can use lambdas to evaluate collections at run time in ActiveAdmin. You can speed up the index page load times by using lambdas to lazily load some of your UI elements:
“The :check_boxes and :select types accept options for the collection. By default, it attempts to create a collection based on an association. But you can pass in the collection as a proc to be called at render time.” - https://activeadmin.info/3-index-pages.html
Ruby Proc vs. Lambda - What’s the Difference?
Lambdas are closely related to Ruby Procs. In fact, creating a lambda is almost “equivalent to Proc.new”. There are, however, a few differences that may catch you unawares.
Lambdas enforce argument count
Lambda functions check the number of parameters passed when they are called. If you create a lambda function that accepts one parameter, and you call the lambda with zero parameters, it will fail. If you call it with two or more parameters, it will also fail. The lambda must be called with exactly one parameter.
Procs, on the other hand, accept any number of arguments. If they are passed too few arguments, the unpassed arguments are set to a value of nil. If they are passed too many arguments, the extraneous arguments are dropped silently. Here’s some example code to illustrate the point:
my_proc = Proc.new {|name| puts "proc says hello " + name.to_s }
my_lambda = lambda {|name| puts "lambda says hello " + name.to_s }
my_proc.call("jerry")
my_lambda.call("jerry")
my_proc.call
my_lambda.call
The output with the last call to the lambda throwing an exception:
proc says hello jerry
lambda says hello jerry
proc says hello
Traceback (most recent call last):
1: from proc_vs_lambda.rb:8:in `<main>'
proc_vs_lambda.rb:2:in `block in <main>': wrong number of arguments (given 0, expected 1) (ArgumentError)
You can, of course, use the splat operator allow a lambda to take multiple arguments:
my_lambda = lambda do |*args|
args.each do |arg|
puts "I saw " +arg
end
end
my_lambda.call("a", "b", "c")
The output:
I saw a
I saw b
I saw c
The behavior of the return statement
The return statement is handled differently as well. The return statement in a lambda function stops the lambda and returns control to the calling code. The return statement in a Proc, in contrast, returns from both the Proc and the calling code. Here’s an example:
my_lambda = lambda do |name|
puts "lambda says hello " + name
return "lambda done"
end
my_proc = Proc.new do |name|
puts "proc says hello " + name
return "proc done"
end
def call_lambda(lmbda)
value = lmbda.call("jerry")
puts value
end
call_lambda(my_lambda)
def call_proc(prc)
value = prc.call("jerry")
puts value
end
call_proc(my_proc)
The output:
lambda says hello jerry
lambda done
proc says hello jerry
You see “lambda done” but not “proc done” because the return statement in the proc terminates the program flow.
Ruby Lambdas in Use
Ruby Lambdas can be used in a number of situations. Sometimes, there’s no need for state, so an object would be overkill, but the logic is complicated enough to be pulled out to a separate, portable variable. In other cases the value of writing a lambda is the runtime flexibility.
ActiveRecord scopes
As mentioned previously, ActiveRecord scopes are a common use of Ruby lambdas. Here’s an ActiveRecord scope on an Article model which only displays published articles:
scope :published, lambda { where(published: true) }
In the controller you can call the scope like this:
@articles = Article.published.all
In the Rails ActiveRecord guide, the scopes are all written in the lambda literal syntax, but the functionality is the same:
scope :published, -> { where(published: true) }
Callbacks
Lambdas are great choices for simple callbacks. You can define them right before you use them or inline, without the cognitive overhead of an object or the namespace pollution of a function. In the Rails codebase, lambdas are used to capture success or failures in tests. Here’s an ActionCable test:
test "#subscribe returns NotImplementedError by default" do
callback = lambda { puts "callback" }
success_callback = lambda { puts "success" }
assert_raises NotImplementedError do
BrokenAdapter.new(@server).subscribe("channel", callback, success_callback)
end
end
Dynamic mapping
If you want to map over a collection, but the map function changes based on the input, you can use a lambda to encapsulate the changing logic. Here’s an example of code in which the mapping function differs based on the input--any number that is a multiple of 5 is omitted and any odd number is doubled. This logic could also be defined and tested elsewhere.
collection = [1,2,3,4,5,6,7,8]
my_lambda = lambda do |num|
if num % 2 == 0
return num
end
if num % 5 == 0
return
end
num*2
end
new_collection = collection.map { |c| my_lambda.call(c) }.compact
puts new_collection
The output:
2
2
6
4
6
14
8
A faux hash
Because a lambda can be called with the syntax: my_lambda[“argument”], you can create a hash-like read-only object which returns values from a key based on code. Here’s an example of a lambda that disallows certain keys that are specified at creation. All other keys are concatenated with their value.
def build_lambda(restricted_values)
my_hash = {}
my_lambda = lambda do |key|
if restricted_values.include? key
return "n/a"
else
return key + key
end
end
my_lambda
end
my_multiplying_hash_like_object = build_lambda(['hi'])
puts my_multiplying_hash_like_object["hi"]
puts my_multiplying_hash_like_object["bye"]
The output:
n/a
byebye
If you were providing read-only access to configuration values pulled from a database or a file and wanted to allow hash-like access, you could use such a lambda. Beware, if you use any of the other hash methods, the lambda fails:
puts my_multiplying_hash_like_object.keys
The output:
undefined method `keys' for #<Proc:0x00000000026972b8@lambda_as_hash.rb:3 (lambda)> (NoMethodError)
Performance
If you are using a lambda in a tight loop or other high-performance situation, benchmark it to make sure your needs are met. As always, premature optimization is the root of all evil. From my testing, it looks like lambda functions are about 30% slower than method or function calls, but here’s a benchmark script to run on your system:
require 'benchmark'
def my_function(a,b)
return a*b
end
my_lambda = lambda {|a,b| return a*b }
class Calc
def mult(a,b)
return a*b
end
end
puts "function"
puts Benchmark.measure {
50_000_000.times do
my_function(rand, rand)
end
}
puts "lambda"
puts Benchmark.measure {
50_000_000.times do
my_lambda.call(rand, rand)
end
}
puts "object method"
calc = Calc.new
puts Benchmark.measure {
50_000_000.times do
calc.mult(rand, rand)
end
}
Closing Thoughts
Ruby lambdas allow you to encapsulate logic and data in an eminently portable variable. A lambda function can be passed to object methods, stored in data structures, and executed when needed. Lambda functions occupy a sweet spot between normal functions and objects. They can have state but don’t have the complexity of a full-fledged object. While many folks are familiar with lambdas because of Rails model scopes, they can be useful in other areas of your codebase.