Functional Programming with Ruby

One lambda to rule them all

Arnau Sanchez <tokland@gmail.com>

Introduction (1)

x = x + 1

x - x = 1

0 = 1

pics/picard-facepalm.jpg

Introduction (2)

But isn't this a minor detail? We all know "x" is not a value here but a variable!

We have business to run here!

Let the loony bearded guys deal with it!!

pics/ritchie-thompson.jpg

No, it's important, I'll try to show you why.

Functional Programming: a bit of history

Functional Programming: the theory

Once assigned (value binding), a "variable" (a symbol) doesn't change its value.

pics/functional-programming-joke.png

Functional Programming: state

State is the root of all evil.

pics/anarchy.jpg

That's all there is? but what comes of it?

Referential transparency (1)

Referential transparency (2)

Insanity: Doing the same thing over and over again and expecting different results.

pics/einstein.jpg

Parallelization

result = fun1(a, b) + fun2(a, c)

We can execute fun1 and fun2 in parallel (a won't be modified)

Concurrence

With no shared data, concurrence gets a lot simpler:

  • No semaphores
  • No monitors
  • No locks
  • No race-conditions
  • No dead-locks

Easier debugging

Modularity and composition

Global state spreads complexity all over the code base. Instead, you should use take advantage of:

Write better code!

All right... all right... but apart from referential transparency, parallelization, easier debugging and modularization... what has Functional Programming ever done for us?

pics/romans.jpg

You'll write more elegant, concise, easier to understand, maintainable code. Let's see how.

But seriously, functional programming in Ruby?

Don't update, create

Don't update, create: strings

No (even if that's a name rebind and not a real update):

movie = "The Apartment"
movie = "#{movie} (by Billy Wilder)"
movie = "[1960] #{movie}"
# "[1960] The Apartment (by Billy Wilder)"

Yes:

movie = "The Apartment"
movie_with_director = "#{movie} (by Billy Wilder)"
complete_movie = "[1960] #{movie_with_director}"
# "[1960] The Apartment (by Billy Wilder)"

Don't update, create: arrays

No:

years = [2000, 2001]
...
years << 2002
years += [2003, 2004]
years # [2000, 2001, 2002, 2003, 2004]

Yes:

years = [2000, 2001]
...
all_years = years + [2003] + [2003, 2004]
# [2000, 2001, 2002, 2003, 2004]

Don't update, create: hashes

No:

ages = {"John" => 30}
...
ages["Mary"] = 28
ages # {"John" => 30, "Mary" => 28}

Yes:

ages = {"John" => 30}
...
all_ages = ages.merge("Mary" => 28)
# {"John" => 30, "Mary" => 28}

Blocks as higher-order functions (1)

write_block = proc do |file|
  file.write("hello")
end
open("file.txt", &write_block)

Which of course we'd write this way:

open("file.txt") do |file|
  file.write("hello")
end

Blocks as higher-order functions (2)

def get_adder(value)
  proc { |x| x + value }
end

adder5 = get_adder(5)
adder5.call(2) # 7
adder5.call(4) # 9

Blocks as higher-order functions (3)

a = 12 # a :: Integer
b = 12.3 # b :: Float
c = 'hello" # c :: String
d = [1, 'hi', 32.1] # d :: Array
p = proc { |a, b| a + b } # p :: [Numeric, Numeric] -> Numeric
class Car
  def collision?(other_cars)
    # ...
  end
end
# Car#collision? :: [Car] -> Boolean

Higher-order functions: map

No:

output = []
[1, 2, 3, 4].each do |x|
  output << x * 2
end
output # [2, 4, 6, 8]

Yes:

output = [1, 2, 3, 4].map do |x|
  x * 2
end # [2, 4, 6, 8]

Higher-order functions: select

No:

output = []
[1, 2, 3, 4].each do |x|
  output << x if x > 2
end
output # [3, 4]

Yes:

output = [1, 2, 3, 4].select do |x|
  x > 2
end # [3, 4]

Higher-order functions: detect

No:

output = nil
[1, 2, 3, 4].each do |x|
  if x > 2
    output = x
    break
  end
end
output # 3

Yes:

output = [1, 2, 3, 4].detect do |x|
  x > 2
end # 3

Higher-order functions: inject

No:

total = 0
[1, 2, 3, 4].each do |x|
  total += x
end
total # 10

Yes:

total = [1, 2, 3, 4].inject(0) do |acc, x|
  acc + x
end # 10

For simple cases like this:

total = [1, 2, 3, 4].inject(0, :+)

Higher-order functions: zip

No:

xs = [1, 2, 3]
ys = [:a, :b, :c]
output = []
0.upto(xs.length - 1).each do |idx|
  output << [xs[idx], ys[idx]]
end
output #=> [[1, :a], [2, :b], [3, :c]]

Yes:

xs = [1, 2, 3]
ys = [:a, :b, :c]
output = xs.zip(ys) #=> [[1, :a], [2, :b], [3, :c]]

Higher-order functions: each_xyz

pairwise_sums = []
[1, 2, 3, 4].each_cons(2) do |x, y|
  pairwise_sums << x + y
end
pairwise_sums #=> [3, 5, 7]

However, since 1.8.7 we can use them functionally, calling them without a block:

pairwise_sums = [1, 2, 3, 4].each_cons(2).map do |x, y|
  x + y
end #=> [3, 5, 7]

Example: Enumerable to hash

Compare:

values = ["ride", "the", "dragon"]
lengths = {}
values.each do |string|
  lengths[string] = string.length
end
lengths #=> {"the" => 3, "ride" => 4, "dragon" => 6}

With Facets' mash:

["ride", "the", "dragon"].mash { |s| [s, s.length] }
#=> {"ride" => 4, "the" => 3, "dragon" => 6}

Write your own extensions and refine them while you use them in your projects.

Higher-order functions: each

[1, 2, 3].each do |x|
  # Write some nasty side-effect here
end

Memoization (1)

module Math
  def self.fibs(n)
    n <= 1 ? n : fibs(n - 1) + fibs(n - 2)
  end
end

p Math::fibs(35)
$ time ruby fib.rb
14930352

real  0m19.852s

Memoization (2)

Using simple_memoize:

require 'simple_memoize' # https://github.com/tokland/simple_memoize

module Math
  def self.fibs(n)
    n < 2 ? 1 : fibs(n - 1) + fibs(n - 2)
  end
  cmemoize :fibs
end

p Math::fibs(35)
$ time ruby fib.rb
14930352

real  0m0.017s

Narrow down the scope by immutability

@title = "The apartment"
# 20 lines of code here
render(@title)

If you don't honor immutability, what's the value of @title when render is called? Easy:

Recursion (1)

Recursion (2)

To enable TCO in MRI-1.9

RubyVM::InstructionSequence.compile_option = {
  :tailcall_optimization => true,
  :trace_instruction => false,
}

Simple example:

module Math
  def self.factorial_tco(n, acc=1)
    n < 1 ? acc : factorial_tco(n-1, n*acc)
  end
end

Recursion (3)

class Node
  has_many :children, :class_name => "Node"

  def all_children
    self.children.flat_map do |child|
      [child] + child.all_children
    end
  end
end

Everything is an expression (1)

if found_dog == our_dog
  name = found_dog.name
  message = "We found our dog #{name}!"
else
  message = "No luck"
end

Control structures (if, while, case, ...) in Ruby also return an expression, let's take advantage of it.

Everything is an expression (2)

message = if found_dog == our_dog
  name = found_dog.name
  "We found our dog #{name}!"
else
  "No luck"
end

Everything is an expression (3)

FP code, being expressions, can be used to build data:

{
  :name => "M.Cassatt",
  :paintings => paintings.select { |p| p.author == "M.Cassatt" },
  :birth => painters.detect { |p| p.name == "M.Cassatt" }.birth.year,
  ...
}

Lazy evaluation

Lazy enumerators

In Ruby 2.0:

>> (0..Float::INFINITY).lazy.map { |x| 2*x }.take(5).to_a
#=> [0, 2, 4, 6, 8]

Example

"What's the sum of the first 10 natural number whose square value is divisible by 5?"

Imperative:

n, num_elements, sum = 1, 0, 0
while num_elements < 10
  if n**2 % 5 == 0
    sum += n
    num_elements += 1
  end
  n += 1
end
sum #=> 275

Functional:

Integer::natural.select { |x| x**2 % 5 == 0 }.take(10).inject(:+) #=> 275

Programming paradigms: Imperative

A schemer (arcus) once said:

Functional programming is like describing your problem to a mathematician. Imperative programming is like giving instructions to an idiot.

Programming paradigms: FP in OOP? (1)

John Armstrong (creator of Erlang) talks in "Coders at Work" about the reusability of code in FP:

"The problem with OO languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."
pics/gorilla.jpg

Programming paradigms: FP in OOP? (2)

So we cannot do FP in OOP? Yes, we can!

Efficiency

Breaking the law (1): Why

pics/judas-breaking-the-law.jpg

Breaking the law (2): Where

module Enumerable
  def mash(&block)
    output = {}
    each do |x|
      k, v = yield(x)
      output[k] = v
    end
    output
  end
end
updated_player = @player.next_state
@player = updated_player

We get it, FP is cool but... (1)

... why imperative languages still rule the galaxy?!

Is the imperative programming stronger? No, no, no. Only quicker, easier, more seductive.
pics/yoda-lambda.png

We get it, FP is cool but... (2)

... why is it harder to catch?!

Be a craftsman: Take your time to master the tools of your trade, and later build your own.
pics/violinmaker.jpg

Advice: learn at least one functional language

Thanks for coming!

Arnau Sanchez (tokland)

Software freelance developer (and occasional teacher).

pics/tokland.jpg

Email: tokland@gmail.com

Professional page: http://www.arnau-sanchez.com/en

Free projects and writings: http://code.google.com/p/tokland/

Share this!

Creative Commons Attribution 3.0
pics/license-cc.png