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?

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.