Back-End

26 jun, 2012

Entendendo Blocks, Procs e Lambdas no Ruby

Publicidade

Texto original disponível em http://www.robertsosinski.com/2008/12/21/understanding-ruby-blocks-procs-and-lambdas/

?

Blocks, Procs e Lambdas (definidas closures na Ciência da Computação) são uns dos aspectos mais poderosos do Ruby, e também os mais incompreendidos. Isso provavelmente acontece porque o Ruby lida com closures de uma maneira única. O que deixa as coisas mais complicadas é o fato de o Ruby ter quatro maneiras diferentes de usar closures, cada uma delas um pouquinho diferente, e às vezes sem sentido. Existe um bom número de sites com informações muito boas sobre como trabalhar com closures dentro do Ruby. Mas eu ainda tenho que encontrar um guia definitivo por aí. Espero que este artigo assuma essa função.  

Primeiro, Blocks

O jeito mais fácil, comum e comprovado de usar closures no Ruby é dentro de blocks. Eles têm a seguinte sintaxe:

array = [1, 2, 3, 4]

array.collect! do |n|
n ** 2
end

puts array.inspect

# => [1, 4, 9, 16]

Então, o que está acontecendo aqui?

  1. Primeiro, nós mandamos o método collect! para um Array com um bloco de código.
  2. O bloco do código interage com uma variável usada dentro do método collect! (nesse caso n) e o eleva ao quadrado.
  3. Cada elemento dentro do Array agora está elevado ao quadrado.

Usar um block com o método collect! é bem fácil, nós apenas temos que pensar que esse collect! vai usar o código dado dentro de cada elemento no Array. No entanto, e se quisermos fazer nosso próprio método collect!? Como ele vai ser? Bem, vamos construir um método chamado iterate! e vamos ver.

class Array
def iterate!
self.each_with_index do |n, i|
self[i] = yield(n)
end
end
end

array = [1, 2, 3, 4]

array.iterate! do |n|
n ** 2
end

puts array.inspect

# => [1, 4, 9, 16]

Para começar, nós reabrimos a classe do Array e colocamos o método iterate! dentro dela. Nós vamos continuar com as convenções do Ruby e colocar um “bang” no final, dizendo aos nossos usuários para tomar cuidado, pois esse método pode ser perigoso! Então usamos nosso método iterate! da mesma maneira que o Ruby fez no método collect! No entanto, a melhor parte está bem no meio da nossa definição de método iterate!.

Diferentemente de atributos, você não precisa especificar o nome dos blocks dentro dos seus métodos, você pode usar a palavra-chave yield. Ao chamar essa palavra-chave, ela vai executar o código dentro do block fornecido ao método. Além disso, note como estamos passando o n (o número inteiro com que o método each_with_index está trabalhando no momento) ao yield. Os atributos passados ao yield correspondem às variáveis especificadas na piped list do block. Esse valor agora está disponível para o block, e devolvido pelo chamado yield. Para recapitular o que está acontecendo:

  1. Mande iterate! para o Array de números.
  2. Quando o yield é chamado com o número n (primeira vez é 1, segunda vez é 2 etc…), passe o número para o block do código fornecido.
  3. O block tem o número disponível (também chamado n) e o eleva ao quadrado.  Como este é o último valor gerenciado pelo block, ele é devolvido automaticamente.
  4. O yield devolve o valor retornado pelo bloco e sobreescreve o valor no Array.
  5. Isso continua para cada elemento no Array.

O que nós temos agora é uma maneira flexível de interagir com o nosso método. Entenda os blocks como aqueles que estão dando a seu método uma API, na qual você pode determinar que cada valor do Array seja elevado ao quadrado, ou ao cubo, ou converter cada número em uma string e imprimi-los na tela. As opções são infinitas, deixando seu método bem flexível e, assim, muito poderoso.

No entanto, esse é apenas o começo. Utilizar o yield é uma maneira de usar seu block de código, mas existe um outro, chamado de Proc. Dê uma olhada.

class Array
def iterate!(&code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

array = [1, 2, 3, 4]

array.iterate! do |n|
n ** 2
end

puts array.inspect

# => [1, 4, 9, 16]

Parece bem similar ao nosso exemplo anterior, no entanto existem duas diferenças. Primeiro, nós estamos passando um argumento ampersand chamado &code. Esse argumento é, convenientemente suficiente, o nosso block. O segundo é o meio da nossa definição de método iterate!, no qual em vez de usar o yield, mandamos um chamado para todos os nossos blocos de códigos. O resultado é exatamente o mesmo. No entanto, se isso é verdade, por que então temos essa diferença em sintaxe? Bom, ela nos ensina um pouco mais sobre o que blocks realmente são. Dê uma olhada:

def what_am_i(&block)
block.class
end

puts what_am_i {}

# => Proc

Um block é simplesmente um Proc! Dito isso, o que é um Proc?

Procedimentos, mais conhecidos como Procs

Blocks são bastante convenientes e sintaticamente simples, no entanto é melhor termos vários blocks diferentes à nossa disposição e usá-los múltiplas vezes. Assim, passar o mesmo block múltiplas vezes faria com que nós nos repetíssemos. Como o Ruby é completamente orientado ao objeto, isso pode ser contornado de uma maneira tranquila, ao salvar o código reutilizável como sendo um objeto. O objeto reutilizável é chamado Proc (abreviação para procedimento). A única diferença entre blocks e Procs é que um block é um Proc que não pode ser salvo e, assim, é uma solução única. Ao trabalhar com Procs, podemos começar a fazer o seguinte:

class Array
def iterate!(code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

array_1 = [1, 2, 3, 4]
array_2 = [2, 3, 4, 5]

square = Proc.new do |n|
n ** 2
end

array_1.iterate!(square)
array_2.iterate!(square)

puts array_1.inspect
puts array_2.inspect

# => [1, 4, 9, 16]
# => [4, 9, 16, 25]

Por que block em caixa baixa e Proc em caixa alta?

Eu sempre escrevo o Proc em caixa alta como se fosse uma própria classe dentro do Ruby. No entanto, blocks não têm uma classe (afinal eles são apenas Procs) e são apenas um tipo de sintaxe dentro do Ruby. Por isso, eu escrevo blocks em caixa baixa. Mais para frente neste artigo, você verá que eu também escrevo lambda em caixa baixa. Eu faço isso pelo mesmo motivo.

Note como nós não precedemos com um & o atributo do código no nosso método iterate!. Isso é porque passar Procs não é diferente de passar qualquer outro tipo de dado. Como os Procs são criados como qualquer outro objeto, podemos começar a nos divertir e a forçar o intérprete do Ruby a fazer algumas coisas interessantes. Tente isto aqui:

class Array
def iterate!(code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

array = [1, 2, 3, 4]

array.iterate!(Proc.new do |n|
n ** 2
end)

puts array.inspect

# => [1, 4, 9, 16]

Acima temos como a maioria das linguagens lida com closures, e é exatamente o mesmo que enviar um block. No entanto, se você pensou que isso não parece com o “jeito Ruby”, eu vou ter que concordar. A razão acima é exatamente o porquê de o Ruby ter blocos, e isso é para permanecer de acordo com a familiar sintaxe de conclusão “end”.

Se esse é o caso, por que não usar somente blocks? Bem, a resposta é simples, e se nós quisermos passar duas ou mais closures para um método? Se esse é o caso, blocks rapidamente se tornam muito limitados. No entanto, ao ter Procs, podemos fazer algo como:

def callbacks(procs)
procs[:starting].call

puts "Still going"

procs[:finishing].call
end

callbacks(:starting => Proc.new { puts "Starting" },
:finishing => Proc.new { puts "Finishing" })

# => Starting
# => Still going
# => Finishing

Então quando devemos usar blocks no lugar de Procs? Minha lógica é a seguinte:

  1. Block: Seu método está quebrando um objeto em pedaços menores, e você quer permitir que seus usuários interajam com esses pedaços.
  2. Block: Você quer executar expressões múltiplas automaticamente, como uma migração de banco de dados.
  3. Proc: Você quer reutilizar um bloco de código múltiplas vezes.
  4. Proc: Seu método vai ter um ou mais callbacks.

Lambdas

Até agora, você usou Procs de duas maneiras, passando-os diretamente como um atributo e salvando-ps como uma variável. Esses Procs agem de maneira muito similar ao que outras linguagens chamam de funções anônimas, ou lambdas. Para deixar as coisas mais interessantes, lambdas estão disponíveis no Ruby também. Dê uma olhada:

class Array
def iterate!(code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

array = [1, 2, 3, 4]

array.iterate!(lambda { |n| n ** 2 })

puts array.inspect

# => [1, 4, 9, 16]

À primeira vista, lambdas parecem ser exatamente iguais aos Procs. No entanto, existem duas diferenças sutis. A primeira é que, diferentemente de Procs, lambdas checam o número de argumentos passados.

def args(code)
one, two = 1, 2
code.call(one, two)
end

args(Proc.new{|a, b, c| puts "Give me a #{a} and a #{b} and a #{c.class}"})

args(lambda{|a, b, c| puts "Give me a #{a} and a #{b} and a #{c.class}"})

# => Give me a 1 and a 2 and a NilClass
# *.rb:8: ArgumentError: wrong number of arguments (2 for 3) (ArgumentError)

Vemos com o exemplo do Proc que variáveis extras são definidas no nil. No entanto, com lambdas, em vez disso, o Ruby gera um erro.

A segunda diferença é que lambdas têm retornos diminutos. O que isso significa é que, enquanto o retorno de um Proc vai parar um método e retornar o valor fornecido, lambdas vão retornar o seu valor ao método e deixá-lo continuar. Confuso? Vamos ver um exemplo:

def proc_return
Proc.new { return "Proc.new"}.call
return "proc_return method finished"
end

def lambda_return
lambda { return "lambda" }.call
return "lambda_return method finished"
end

puts proc_return
puts lambda_return

# => Proc.new
# => lambda_return method finished

Em proc_return, nosso método aciona a palavra-chave return, ele pára de processar o resto do método e retorna a string Proc.new. Por outro lado, nosso método lambda_return aciona nosso lambda – o que retorna o string lambda –, continua processando, encontra o próximo return e retorna  lambda_return. Por que a diferença?

A resposta está nas diferenças conceituais entre procedimentos e métodos. No Ruby, Procs são trechos dentro do código, não métodos. Por causa disso, o retorno do Proc é o retorno do método proc_return, e ele age adequadamente a isso. No entanto, lambdas agem como métodos, ao checar o número de argumentos e não substituir os métodos return. Por esse motivo, é melhor pensar em lambdas como uma nova maneira de escrever métodos, uma maneira anônima.

Então quando você deve escrever um método anônimo (lambda) em vez de um Proc? O código a seguir mostra um exemplo:

def generic_return(code)
code.call
return "generic_return method finished"
end

puts generic_return(Proc.new { return "Proc.new" })
puts generic_return(lambda { return "lambda" })

# => *.rb:6: unexpected return (LocalJumpError)
# => generic_return method finished

Parte da sintaxe do Ruby é que argumentos (um Proc neste exemplo) não podem ter uma palavra-chave return dentro deles. No entanto, um lambda age da mesma maneira que um método, o que pode ter um retorno literal, e assim passar ileso por esse requerimento! Essa diferença em semântica aparece em situações como a do exemplo a seguir:

def generic_return(code)
one, two = 1, 2
three, four = code.call(one, two)
return "Give me a #{three} and a #{four}"
end

puts generic_return(lambda { |x, y| return x + 2, y + 2 })

puts generic_return(Proc.new { |x, y| return x + 2, y + 2 })

puts generic_return(Proc.new { |x, y| x + 2; y + 2 })

puts generic_return(Proc.new { |x, y| [x + 2, y + 2] })

# => Give me a 3 and a 4
# => *.rb:9: unexpected return (LocalJumpError)
# => Give me a 4 and a
# => Give me a 3 and a 4

Aqui, nosso método generic_return está esperando a closure para retornar dois valores. No entanto, fazer isso sem a palavra-chave return se torna arriscado. Com o lambda, tudo é fácil. No entanto, com o Proc, temos que tirar vantagem do modo que o Ruby interpreta os Arrays com atribuições.

Então quando usar Proc em vez de lambdas, e vice-versa? Honestamente, além da checagem de argumentos, a diferença é simplesmente como você vê as closures. Se você tem a finalidade de passar blocks de códigos, fique com o Proc. Se mandar um método para outro método que pode retornar um método faz sentido para você, use lambdas. Mas se lambdas são apenas métodos em forma de objetos, podemos armazenar métodos existentes e passá-los como Procs? Para isso, o Ruby tem algo intrigante debaixo da manga.

Métodos dos objetos (Method Objects)

Então você já tem um método que funciona, mas você quer passá-lo para outro método como uma closure, e manter seu código. Para fazer isso, você pode tirar vantagem do método do Ruby:

class Array
def iterate!(code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

def square(n)
n ** 2
end

array = [1, 2, 3, 4]

array.iterate!(method(:square))

puts array.inspect

# => [1, 4, 9, 16]

Nesse exemplo, nós já temos um método chamado square (potência de 2) que funcionaria bem para essa tarefa. Assim, podemos utilizá-lo novamente como um parâmetro ao convertê-lo a um método do objeto e passando-o para o nosso método iterate!. Mas qual é esse novo tipo de objeto?

def square(n)
n ** 2
end

puts method(:square).class

# => Method

Bem, como você adivinhou, o square não é um Proc, mas um Método. A melhor parte é que esse método do objeto vai agir como um lambda, porque o conceito é o mesmo. Esse método, no entanto, é denominado método (chamado square), enquanto lambdas são denominadas métodos anônimos.

Conclusão

Para recapitular, vimos quatro tipos de closures no Ruby – blocks, Procs, lambdas e Methods. Nós também sabemos que blocks e Procs agem da mesma maneira que métodos. Finalmente, através de uma grande quantidade de exemplos de código, você está pronto para decidir quando usar cada um deles da maneira correta. Agora você deve ser capaz de começar a utilizar esse interessante e expressivo recurso do Ruby no seu próprio código, e começar a oferecer métodos flexíveis e poderosos aos outros desenvolvedores que trabalham com você.