Applications of Lambda in Ruby
Thoughts about lambda in Ruby.
Table of Contents
Intro
When I stumbled upon lambda for the first time in some other Ruby book, my first thought was like 'Hm, interesting thing, but why should I ever use it?'.
Wikipedia defines lambda as a not bound anonymous function and gives some use cases as to when lambdas are used in programming.
There are a lot of articles about the differences between Proc, Block and Lambda in Ruby, but none of them (at least those I read) answered my question.
It took time before I realized that it's, in fact, a nice-to-have tool, which one can use to better express ideas and even optimize code.
Code Clarification
Let's have a look at the following code.
a = (1..10).to_a
#lets double it
a = a.map { |x| x*2 }
But is the example really clear? What if the block provided is a bit more complicated?
rs = (1..10).to_a
areas = rs.map { |r| Math::PI * r**2 }
It is still easy to read, but takes a little more effort to understand: we have to keep in mind that we have an array of some values
and we map it on some procedure that calulates power of 2 of its only argument and then multiplies it by PI. This procedure looks like a circle area calculation
algorithm. So areas
is the array of circle areas calculated based on the array of radiuses defined above. Too much effort for a single line of code, huh?
To make it clear we can use lambda.
calculate_circle_area = -> r { Math::PI * r**2 }
areas = rs.map(&calculate_circle_area)
Now, we first get knowledge about how to calculate the area of a circle and only then we map an array with the defined procedure. Also we
can use our lambda as many times as we want.
Sorting
a = (1..100).to_a.shuffle
a.sort
It is simple enough, but Ruby is a very expressive language, so why not make our code clearer?
Enumerable#sort has zero arguments, but accepts optional code block which defines the way sorting is performed. However, if
we try to pass it a lambda - interpretor explodes with Exception!
# let us do the same as above, but with lambda
in_ascending_order = -> left, right { left <=> right }
a.sort in_ascending_order
#=> ArgumentError: wrong number of arguments (given 1, expected 0)
Ok, it passes lambda as argument, which is erroneous. Of course, we have to pass it as code block! We can easily convert
lambda to block with unary & operator which just sends it a :to_proc message. Let's try again.
a.sort &in_ascending_order
And it worked as planned, more so: we gave our code expressiveness worth of Ruby.
Currying
This one is rather tricky. As we already know, lambdas can take arguments. With currying we can provide it with only some of them.
sum = -> a, b { a + b }
add_three = sum.curry.(3)
Now we have add_three
lambda which takes only one argument and adds it to 3. We can use it with Enumerable#map:
a = [*1..100]
a.map(&add_three)
If there are not enough arguments supplied to curry
function it returns new lambda with bound variables. Otherwise it just evaluates lambda with given parameters.
So we apply curried sum
with bound 3
to array with map
.
Higher-order functions
Such functions are just functions that take other functions (or Ruby code blocks) as arguments. And most of us have already used or at least seen them in code.
The most frequently used ones are in Enumerable. #map, #select, #reduce and others are all higher-order functions. So, let’s lets make our own!
class Example
def initialize(argument)
@value = argument
end
def apply(lambda)
lambda.(@value)
end
end
double = -> x { x * 2 }
Example.new(10).apply double
Closures
Lambda in Ruby can bind variables local to the context in which it was defined. Let me clarify this with an example.
def t(argument)
-> { puts argument }
end
t('one to bind them all').call
So, we defined method #t with arity of 1, which returns a lambda which, in turn, prints an argument to $stdout. Now we can
try something more useful.
def compare_with(threshold)
-> x { x > threshold }
end
# Array generation from Range using splat operator
a = [*1..100]
greater_than_10 = compare_with(10)
a.select &greater_than_10
Let's be more real and apply lambda to a more sophisticated case.
class CarBuilder
def initialize
@car = Struct.new(:body, :wheels, :engine).new
end
def add_body(body)
sleep 1
car.body = body
end
def add_wheels(wheels)
sleep 2
car.wheels = wheels
end
def add_engine(engine)
sleep 3
car.engine = engine
end
def get_result
car
end
private
attr_reader :car
end
Think of #sleep method as of some IO. To help ourselves build car faster with Threads here is complementary class.
class Pipeline
def initialize
@pipeline = []
end
def <<(callable, *args)
@pipeline << Thread.new(*args, &callable)
end
def sync
@pipeline.each(&:join)
clear
end
def clear
@pipeline.each(&:kill)
@pipeline = []
end
end
And now we can build our car in parallel Threads with lambdas.
c = CarBuilder.new
p = Pipeline.new
p << -> { c.add_engine("2,5l v8") }
p << -> { c.add_wheels("18'") }
p << -> { c.add_body("sedan") }
p.sync
puts c.get_result
Gotcha!
Closures may be dangerous. Have a look at this code.
i = 0
a = [*1..10].map do |x|
i = x
-> { i }
end
Lambda will capture i
by reference - not value. And if you now run this:
a[0].binding.local_variable_get(:i)
a[9].binding.local_variable_get(:i)
… the results will be same! So, watch your namespace.
This concludes some applications of lambda I found useful. Happy coding!