sexta-feira, 26 de outubro de 2012

Blocos em Ruby

Neste primeiro post sobre a linguagem Ruby, acho que um dos tópicos mais interessantes de abordar é a forma como Ruby lida com blocos.

O que são blocos, como criar, como passar por parâmetro, como reaproveitar, enfim, como eles se fazem constantemente presentes no dia-a-dia de um desenvolvedor Ruby e quais possibilidades oferecem. Sobre tudo isso pretendo dar uma visão geral aqui.



Como vários outros recursos interessantes do Ruby (que pretendo abordar futuramente), este surpreende por sua simplicidade e, ao mesmo tempo, extrema flexibilidade. A linguagem Java, na sua versão 8, oferece algo parecido, porém a linguagem não favorece tanto a sintaxe como em Ruby.

É importante observar que o framework Rails faz extensivo uso dos recursos e versatilidade da linguagem Ruby, por isso recomenda-se que desenvolvedores Ruby on Rails considerem especialmente importante conhecer a fundo a linguagem por trás dos recursos do framework de forma a compreendê-lo muito melhor, como um todo, e, por que não, extendê-lo com seus próprios plugins.

O que é um bloco em Ruby


Nos dois exemplos abaixo, é feita a iteração sobre um Array através do método each. As duas formas estão corretas, ou seja, é possível definir um bloco com a utilização de chaves (como em linguagens derivadas de C) ou entre do e end.

[1,2,3].each do |n|
   puts n
end

[1,2,3].each { |n|
   puts n
}

O resultado será idêntico: a impressão no console (através do método puts) de cada um dos valores contidos no Array.


Blocos e métodos


Em um primeiro contato, blocos podem dar a impressão de serem métodos, pois além de possuírem um corpo de linhas de comando também podem receber parâmetros. Porém métodos e blocos não são a mesma coisa, apesar de trabalharem juntos.

Métodos, em um exemplo bem simples, são assim:

def imprimir( p )
   puts p
end

O comando imprimir('Teste') simplesmente imprime o texto Teste no console.

Há, porém, métodos que recebem blocos como parâmetro. Mais adiante, veremos detalhadamente como criar um método que recebe um bloco, porém, por enquanto, basta citar alguns exemplos já oferecidos pela API, como o método each exemplificado antes.

Nessas alturas, um conceito básico merece destaque:

Em Ruby, blocos podem ser tornados em objetos!

Por isso é que podem ser passados por parâmetro e, como veremos também mais adiante, podem ser criados, armazenados em variáveis e depois utilizados da forma que for mais adequada quantas vezes for necessário.

Mais um exemplo ilustra bem a diferença entre métodos é blocos:

Dado o seguinte Range:

numeros = 1..5

Utilizando um método para imprimir um por um dos itens do Range (de 1 a 5)

def imprimir ( nums ) do
   for n in nums
      puts n
   end
end

imprimir numeros

E o mesmo realizado com bloco através do método each do objeto numeros do tipo Range:

numeros.each { |n|
   puts n
}

Ou ainda em uma única linha:

numeros.each { |n| puts n }

Como é possível ver, alguns métodos, especialmente aqueles que iteram sobre elementos de um Array ou um Hash, fazem uso de blocos para simplificar consideravelmente a quantidade de código necessário de ser escrito. Recomendo uma navegada na API dessas duas classes para conferir.


Blocos como objetos


Conforme mencionado antes, um bloco em Ruby não é um objeto por padrão, mas pode ser tornado em um objeto da classe Proc, e existe mais de uma forma de realizar isso.

bloco1 = Proc.new{|s| s.capitalize!; puts(s) }
bloco2 = lambda{|s| s.capitalize!; puts(s) }
bloco3 = proc{|s| s.capitalize!; puts(s) }

Os três blocos são idênticos:

  • Recebem uma variável s (que, implicitamente, deve ser do tipo String)
  • invocam o método capitalize! que converte a primeira letra para maiúscula e as demais para minúscula
  • imprimem o objeto no console através do método puts


Dica: Alguns métodos estão definidos na API com um ponto de exclamação. Isso indica que este método não somente retorna o resultado, mas ainda altera o objeto original no qual ele é invocado. No caso do exemplo acima, invocar somente capitalize retornaria uma String alterada mas manteria o objeto da variável s com seu valor original, sem alteração. O ponto de exclamação não produz este efeito por si só, mas é, na verdade, uma convenção da linguagem Ruby para que o desenvolvedor saiba, por exemplo, a diferença entre os métodos capitalize! e capitalize.



Os métodos utilizados para a criação dos blocos foram Proc.new, proc e lambda. Finalmente, as variáveis bloco1, bloco2 e bloco3 guardam referência a um bloco cada, e podem ser acessadas diretamente ou parametrizadas em outros métodos para operações posteriores. O que importa é que para executar o bloco contido em cada variável basta invocar o método call.

bloco1.call("pRIMEIRA")
bloco2.call("lETRA")
bloco3.call("mAIUSCULA")

E o resultado será:

Primeira
Letra
Maiuscula


Proc.new x proc x lambda


Há uma pequena diferença no comportamento dos blocos criados a partir de cada um desses três métodos. Quando um bloco é criado através de Proc.new, o bloco simplesmente ignora a quantidade de parâmetros que excede o que é esperado. Por exemplo, nossas três variáveis guardam blocos que recebem um único parâmetro (poderiam ser N).

No exemplo abaixo, a linha 1 executaria normalmente, ignorando os parâmetros excedentes. Porém as linhas 2 e 3 dariam erro.
bloco1.call("pRIMEIRA","Outros","parâmetros","quaisquer")
bloco2.call("lETRA","Outros","parâmetros","quaisquer")
bloco3.call("mAIUSCULA","Outros","parâmetros","quaisquer")


Definindo métodos que trabalham com bloco anônimo


def metodo
  yield
end

O exemplo acima é o mais simples. Ele pressupõe que o método metodo vai sempre ser chamado acompanhado de um bloco qualquer sem parâmetros. O comando yield é o responsável por executar este bloco.

metodo{
  puts "Olá"
}

O comando acima executaria o bloco produzindo "Olá". Vamos então a um exemplo com um bloco que espera parâmetros:

def somar(n1, n2)
  yield(n1, n2)
end

somar(3,7){ |a,b| puts(a + b) }

O resultado será o inteiro 10.


Definindo métodos que trabalham com blocos parametrizados


Até aqui os exemplos trabalharam com blocos anônimos, mas, uma vez que os blocos tenham sido tornados em objetos, eles poderão ser parametrizados para métodos que esperam receber blocos. Abaixo, um exemplo que recebe os blocos criados anteriormente neste post e executa um por um:

def executarBlocos(b1,b2,b3)
  b1.call("pRIMEIRA")
  b2.call("lETRA")
  b3.call("mAIUSCULA")
end

executarBlocos(bloco1, bloco2, bloco3)

Ou ainda uma combinação entre blocos parametrizados e um bloco anônimo executado através do yield:

def executarBlocos(b1,b2,b3)
  b1.call("pRIMEIRA")
  b2.call("lETRA")
  b3.call("mAIUSCULA")
  yield
end

executarBlocos(bloco1, bloco2, bloco3){ 
  puts "Blocos executados!" 
}

E o resultado será:

Primeira
Letra
Maiuscula
Blocos executados!


Misto de anônimo e parametrizado


Prefixando o parâmetro com um '&' o interpretador do Ruby entende que o parâmetro necessariamente será um bloco e que será informado via parâmetro ou como bloco anônimo. Os exemplos abaixo ilustram bem as duas possibilidades:
def executarBlocos(b1,b2,b3,&b4)
  b1.call("pRIMEIRA")
  b2.call("lETRA")
  b3.call("mAIUSCULA")
  yield
end

executarBlocos(bloco1, bloco2, bloco3){ 
   puts "Blocos executados!"
}

Ou
bloco4 = lambda{ puts "Blocos executados!"}

executarBlocos(bloco1, bloco2, bloco3, &bloco4)

E o resultado seria o mesmo se, em vez de yield, fosse chamado o método call com o nome do parâmetro:
def executarBlocos(b1,b2,b3,&b4)
  b1.call("pRIMEIRA")
  b2.call("lETRA")
  b3.call("mAIUSCULA")
  b4.call
end


block_given?


Ruby oferece um método chamado block_given? que retorna true se o método tiver recebido um bloco anônimo, conforme exemplo abaixo:
def executarBlocos(b1,b2,b3,&b4)
  b1.call("pRIMEIRA")
  b2.call("lETRA")
  b3.call("mAIUSCULA")
  if block_given?
    yield
  end
end

executarBlocos(bloco1, bloco2, bloco3)

executarBlocos(bloco1, bloco2, bloco3){ 
  puts "Blocos executados!"
}

A verificação da linha 5 é o que impede a incidência de erro na linha 10.


Closures


Outro conceito um pouco mais avançado, porém bastante relevante, é que blocos em Ruby são closures. Isso implica que um bloco nunca deixa de pertencer ao seu escopo de origem, podendo inclusive alterar variáveis deste escopo original mesmo após ser passado por parâmetro a outro método (talvez até de outra classe). O exemplo abaixo ilustra a situação:

a = "'a' original"

def metodo
  yield
end

puts( a )

metodo{ a = "'a' alterado"  }

puts( a )

A saída será:
'a' original
'a' alterado

Conclusão


Então, vimos uma porção de detalhes básicos e interessantes sobre blocos em Ruby. Para não estender demais, apresentei exemplos bem pontuais e omiti alguns detalhes mais avançados. Para tudo que não foi abordado neste post, recomendo o livro The Book of Ruby que, dos livros de Ruby que já pesquisei, parece ser o que apresenta em maior detalhe o funcionamento de blocos em Ruby.


Até a próxima!


2 comentários: