Lambda Functions

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 Fixnum 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 Fixnum:

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 to puts, 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]]