Closures: block, proc e lambda - Ruby

Blocks, Procs e Lambdas são referenciados de maneira geral como “closure” e que por sua vez são uns dos aspectos mais poderosos do Ruby e também um dos mais complexos.

Para entedermos melhor que são closures precisamos compreender alguns conceitos:

Escopo léxico

O escopo léxico serve para identificar qual o valor de uma variável em determinado lugar do código, daí o conceito chamado Closest Variable Wins (A variável mais próxima vence), ou seja, a atribuição de valor mais próxima dessa variável, será quem define o valor da mesma.

Ou seja, definindo as variáveis com o mesmo valor, mais próxima do comando puts "#{prefix} #{name}", estas variáveis dentro do escopo terá o valor "Sra" e "Loren" e não mais "Sr" e "Diego" como foram definidas anteriormente. Essa é uma regra do escopo léxico, a variável mais próxima, vence.

prefix = 'Sr'
name = 'Diego'

4.times do
    prefix = 'Sra'
    name = 'Loren'
    msg = 'Olá!'

    puts "#{msg} #{prefix} #{name}"
end

=> Sra Loren
=> Sra Loren
=> Sra Loren
=> Sra Loren

Variável livre

Variável livre é uma variável definida em um escopo acima do escopo atual, ou seja no escopo pai. A variável livre no exemplo acima, seriam então as variáveis prefix e name definidas nas primeiras linhas. Ja a variável msg utilizada está dentro do mesmo escopo de execução, portanto não é uma variável livre.

Closures

"Uma função que pode ser guardada como variável", talvez essa seja a definição mais famosa de Closure. Segundo Wei Hao em seu livro Mastering Ruby Closures, os conceitos acima comentados (escopo léxico e variável livre) são alguns dos principais conceitos para se entender Closures, como o mesmo explica, "Closure é uma função que o seu corpo referencia uma variável declarada no escopo pai". Segundo o Ruby Guides as Closures "capturam o escopo de execução atual" e "carregam variáveis e métodos vindos do contexto atual em que foram definidas", mas "não carregam valores e sim referências, portanto se as variáveis forem alteradas após criadas, as Procs sempre terão a última versão dessa variável".

Closure é uma funcionalidade que permite escrever um pedaço de código que:

  • Pode ser atribuído e ou passado como parâmetro.

  • Uma função que pode ser guardada como variável

  • Pode ser executado em qualquer lugar.

  • E referenciam variáveis no contexto onde são criados.

Block

Ruby blocks como o Ruby Guides define, "são pequenas funções anônimas que podem ser passadas nos métodos". Podemos identificar Ruby Blocks quando vemos ou escrevemos no código comandos dentro dos do / end ou entre chaves {block} e os argumentos vão dentro de pipes |args|. Métodos famosos como o .each, .times e outros são bons exemplos de Blocks.

method { |i| ... }

method do |i|
  ...
end

Ex. usando o each:

[1,2,4,5].each do |number|
  puts number
end

Ex. usando o times:

prefix = 'Sr'
name = 'Diego'

4.times do
    puts "#{prefix} #{name}"
end

=> Sr Diego
=> Sr Diego
=> Sr Diego
=> Sr Diego

Como criar um block?

Imagine o seguinte cenário, onde devemos criar um método para ira concatenar por meio de interpolação o seu primeiro nome e seu ultimo nome

def full_name
    first_name = 'Diego'
    last_name = 'Novais'

    "#{first_name} #{last_name}"
end

puts full_name

=> Diego Novais

Se formos refatorar o método usando block:

def full_name
    yield
end

full_name do
    first_name = 'Diego'
    last_name = 'Novais'

    "#{first_name} #{last_name}"
end

=> Diego Novais

Podemos deixar nosso block mais dinâmico passando argumentos para nosso método e assim utilizando como parâmetros em nosso block:

def full_name(prefix)
    puts prefix + yield
end

full_name('Sr. ') do |prefix|
    first_name = 'Diego'
    last_name = 'Novais'

    "#{first_name} #{last_name}"
end

=> Sr. Diego Novais

Como ultimo exemplo para fixar vamos criar nosso próprio each :

def my_each(array)
  i = 0

  while i < array.size do
    yield array[i]
    i += 1
  end
end

array = [1, 2, 3, 4, 5]

my_each(array) do |number|
  puts number
end

Podemos também passar um block como parâmetro para um método:

def full_name(&block)
    block.call
end

full_name do
    first_name = 'Diego'
    last_name = 'Novais'

    "#{first_name} #{last_name}"
end

=> Diego Novais

Mais um exemplo:

def full_name(prefix, &block)
    puts prefix + block.call
end

full_name('Sr. ') do |prefix|
    first_name = 'Diego'
    last_name = 'Novais'

    "#{first_name} #{last_name}"
end

=> Sr. Diego Novais

Proc

Um Proc se parece bastante com uma lambda, porém, caso não passe os argumentos não será disparado nenhum erro, pois um proc não se importa se irá passar um argumento ou não., vamos analisar isso com um pouco mais de código:

say_something = proc { puts "Something..."  }
say_something.call

=> Something...

ou ...

say_something = Proc.new do
    puts 'Something...'
end

say_something.call

=> Something...

Podemos também passar argumentos para um Proc:

say_something = Proc.new do |name|
    puts "Hello #{name}"
end

say_something.call('Diego')

=> Hello Diego

Se não passarmos o argumento para um Proc, ele não ira se importar e não ira disparar um erro, considerando o exemplo anterior:

say_something.call

=> Hello

Lambda

Uma Lambda se parece mais com uma função do que uma Proc, principalmente pelo fato de respeitar os argumentos e pelo seu retorno, vamos analisar isso com um pouco mais de código, começando por algumas formas de definir um Lambda:

# say_something = lambda { puts "Something..."  }
# ou...

say_something = -> { puts "Something..."  }
say_something.call

=> Something...

Podemos também passar argumentos para uma lambda:

say_something = -> (name) { puts "Hello #{name}"  }
say_something.call('Diego')

=> Hello Diego

Obs.: se não passarmos o argumento um lambda dispara um erro.

Lambda como blocos

Podemos também criar lambdas como blocks:

say_something = lambda do |name|
    puts "Hello #{name}"
end

say_something.call('Diego')

Espero que este conteúdo possa ter te ajudado a compreender mais sobre Closures, block, proc e lambda. Te vejo no próximo conteúdo.