Lambda Functions#
The lambda
is an anonymous function - it has a definition (a body), but it is not bound to an identifier. In contrast to the method, lambda
does not belong to any object. This is quite similar to Rubys block, but, unlike the block, lambda
in an object, so it can be stored in the variable or passed to methods as an argument.
In Ruby, lambda function syntax is lambda block
or ->(variables) block
. Block can be given either with do ... end
or with parenthesis { ... }
. Good practice is to use keyword lambda
when defining longer functions and leave the arrow syntax for one-liners.
lambda do
|x|
x * x
end
#=> #<Proc:0x007fd8411ea3e0@(irb):1 (lambda)>
->(x) { x * x }
#=> #<Proc:0x007fd8409829c8@(irb):5 (lambda)>
As you can see, in Ruby lambda is just a Proc
object instance. There are more ways to create Proc
instance, like with Proc.new
or using keyword proc
, but it is not in a scope of this book. Anyway it is recommended to learn about it and know the difference between the lambda
and the proc
.
Running Lambdas#
To execute the Proc
object, run call
method on its instance. The arguments of this method will be passed to the lambda. In the following example we run the anonymous function directly - ->(x) { x * x }.call(8)
. Interesting part comes later: we can assign the function to the variable - as we do with any object in Ruby. Now we are able to execute this function with call
method:
->(x) { x * x }.call(8)
#=> 64
square = ->(x) { x * x }
#=> #<Proc:0x007fd840993908@(irb):10 (lambda)>
square.call(10)
#=> 100
One of the most interesting properties of lambda functions is a possibility to pass it to the method as an argument. It is similar to block - blocks are indeed the anonymous functions passed to the methods with the special syntax. Lambdas are more flexible - you can pass as many of them as you want, no need to check if block_given?
, etc. Consider the simple example: method run
to execute lambda given as an argument:
def run(function, argument)
function.call(argument)
end
run square, 16
#=> 256
double = ->(x) { x * 2 }
run double, 16
#=> 32
Not a very sophisticated example, right. But the lambda functions do not have to be store in variables only. Let’s put them into an Array and then let’s run all the given functions one by one - first with the given argument, second with the return value of the first function and so on. The snippet below applies three function to the string: first strip
to remove leading and trailing whitespaces, then gsub
to remove all dots (replace all dots with empty string) and finally, capitalize
. The result is the same as running strip.gsub(‘.’,’’).capitalize
methods on the given string:
def run(functions, argument)
ret = argument
for f in functions
ret = f.call ret
end
return ret
end
run [->(x) {x.strip}, ->(x) {x.gsub('.','')}, ->(x) {x.capitalize}], " the quick brown fox."
#=> "The quick brown fox"
" the quick brown fox.".strip.gsub('.','').capitalize # the same effect
#=> "The quick brown fox"
So why to bother with lambdas if we can have the same results using traditional methods? Notice that when we pass the Array with lambdas to the method, we can allow user to modify the Array content. He can choose which functions should be applied.
Real World Example: CSV#
After this boring theoretical part it is time for something closer to reality. Standard Ruby distribution includes a library for processing the Comma-Separated Values files (CSV) - the files with columns separated by comma and rows separated by the newlines. You probably dealed with it many times as Ordinary Users like to export them from MS Excel and give to administrators to process. Below is the simple CSV with two records, each containing three fields: the url, the company name and some number.
http://www.google.com,google,1
http://www.bing.com,microsoft,2
Reading this file in Ruby is very easy - the CSV::read
method reads the file and returns the coresponding matrix (an Array of Arrays).
require 'csv'
#=> true
servers = CSV::read 'servers.csv'
#=> [["http://www.google.com", "google", "1"],
# ["http://www.bing.com", "microsoft", "2"]]
Notice that the numbers appears as the string - by default CSV treats everything as a String. But in the most cases we want numbers to be Integer or Float. For this, we can use built-in converters - the functions which converts the value on the fly, while loading CSV file. There is a number of converters we can use; in this case let’s apply :integer
converter to cast all number to Integer:
servers = CSV::read 'servers.csv', converters: :integer
#=> [["http://www.google.com", "google", 1],
# ["http://www.bing.com", "microsoft", 2]]
Chat was just a built-in converter. What are the others? We can find out in the documentation that all of them are kept in the Hash, CSV::Converters
, where the key is a converter name and value is a lambda function which will be applied to the field. We can find out what converters are built-in by just displaying the content of the hash:
CSV::Converters.keys
#=> [:integer, :float, :numeric, :date, :date_time, :all]
pp CSV::Converters
{:integer=>
#<Proc:0x007fefb3991fb0@/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/csv.rb:946 (lambda)>,
:float=>
#<Proc:0x007fefb3991f88@/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/csv.rb:949 (lambda)>,
:numeric=>[:integer, :float],
:date=>
#<Proc:0x007fefb3991f38@/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/csv.rb:953 (lambda)>,
:date_time=>
#<Proc:0x007fefb3991f10@/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/csv.rb:961 (lambda)>,
:all=>[:date_time, :numeric]}
pp
stands for pretty-print and it is a method similar toputs
, but it displays the standard Ruby objects, like Hashes or Arrays, more human-readable.
According to what is inside the CSV::Converters
hash, we can take a look at the lambdas source code. For example, the :integer
converter is in the file csv.rb
, line number 946:
Converters = { integer: lambda { |f|
Integer(f.encode(ConverterEncoding)) rescue f
},
....
This lambda function returns Integer instance (because Integer(‘42’)
creates the number 42
) or, if the conversion went wrong, returns a value of the field itself (there will be an explanation what the rescue
is in the chapter about exceptions).
The list of available converters is not closed, we can extend it by creating our own one. The only thing is to build a lambda and assign it to the CSV::Converters
hash. Assume we want to change the first letter of the companies to capital, but we do not want to capitalize the URLs.
CSV::Converters[:capitalize] = lambda do |f|
if f.start_with? "http"
f # return field itself if it is an URL
else
f.capitalize # String#capitalize does the job
end
end
servers = CSV::read 'servers.csv', converters: [:integer, :capitalize]
#=> [["http://www.google.com", "Google", 1],
# ["http://www.bing.com", "Microsoft", 2]]
We just stored the lambda function in the Hash and passed it to CSV::read
method. By the way, this method is flexible, so if you do not want to, you do not need to extend the CSV::Convertes
hash, but pass the lambda directly as an argument. The next example is how to change all URLs to IP addresses. Notice that the Array passed to CSV::read
contains now three elements: two Symbols used with CSV::Converters
hash, and a variable which is bounded to the lambda function.
require 'socket' # needed to get IP from the hostname
url_to_ip = lambda do |f|
if f.start_with? "http"
# remove 'http://' part from the string
# and then get IP address from it
IPSocket.getaddress f.gsub('http://', '')
else
f
end
end
servers = CSV::read 'servers.csv', converters: [:integer, :capitalize, url_to_ip]
#=> [["173.194.65.99", "Google", 1],
# ["204.79.197.200", "Microsoft", 2]]