iwtraining

Blocos, Iteradores e Closures em Ruby Back-end

Blocos, Iteradores e Closures em Ruby

Blocos, Iteradores e Closures em Ruby

Este é um dos tópicos mais importantes no aprendizado do Ruby, já que a maior parte dos problemas computacionais envolve lidar com coleções de dados e manipulação de rotinas funcionais.

Em Ruby, iteradores e blocos andam juntos, sendo pré-requisitos para entendimento completo dos exemplos listados nesta seção.

As classes mais comuns para operar coleções, como Array, Hash e Range, utilizam funcionalidades comuns, implementadas no módulo Enumerable, que é incluído nas classes citadas.

Iremos começar exemplificando as interações entre blocos e a classe Array.

Embora o ruby possua laços como o for como palavra reservada para fazer iterações, este não é o estilo recomendado nem oferece mesmo nível de performance do que algumas dentre as opções disponíveis.

Para iterar em um array e mostrar no terminal o conteúdo de cada elemento utilizando um bloco:

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

Outro método útil é o map, que permite transformar/gerar uma coleção a partir de outra, como no exemplo:

[1, 2, 3].map { |i| i * 10 }
[1, 2, 3].map { |i| “Número: #{i}” }

No primeiro exemplo um outro array é gerado com os elementos [10, 20, 30], enquanto o segundo gera um array de strings.

Como o retorno desses métodos são arrays, é possível encadear as chamadas de métodos:

[1, 2, 3].map { |i| i * 10 }.map { |i| “Número: #{i}” }

Escopo nos blocos

O uso de bloco no contexto de programas longo necessita de atenção adicional. Na realidade não faz partes das práticas amplamente adotadas pela comunidade a escrita de métodos longos.

De qualquer forma, ao escrever um bloco dentro de um programa grande, as variáveis disponíveis no escopo externo ao bloco podem ser utilizadas dentro do bloco, ou seja, podem ter sua referência para o objeto em memória alterada acidentalmente.

Dentro de um bloco a resolução de nomes de variáveis ocorre normalmente, do escopo mais interno para o mais externo. Se um bloco tiver um parâmetro com o mesmo nome de uma variável no escopo externo, o parâmetro será utilizado dentro do bloco, e não a variável externa:

value = “algum valor aqui”
[1, 2].each { |value| puts value }
puts value

Nesse caso a variável value terá o seu valor string apresentado na terceira linha de código de acordo com aquele instanciado na primeira.

Implementando iteradores utilizando blocos

Podemos implementar métodos que usem blocos para iterar:

def executar_tres_vezes
    yield
    yield
    yield
end

executar_tres_vezes { puts “hello!” }

A palavra-chave yield invoca o bloco associado. Caso necessário, o método block_given? pode ser utilizado para checar se a chamada ao método realmente definiu um bloco.

Métodos podem receber parâmetros propriamente ditos em conjunto aos blocos:

def executar_n_vezes(n)
    n.times {   yield }
end

executar_n_vezes(1) { puts “hello” }
executar_n_vezes(2) { puts “world” } 

Outra funcionalidade essencial é a parametrização dos blocos. Essa é a forma padrão que um método tem de se comunicar com o bloco a ser chamado:

def executar_n_vezes(n)
    n.times {   |i| yield i }
end

executar_n_vezes(2) { |i| puts i }

Esse estilo de programação permite manter código bastante sucinto, melhorando o encapsulamento ao reduzir os detalhes que o chamar precisa saber.

A API do Ruby e da stdlib utilizam blocos para essa finalidade extensamente, como ao ler um arquivo:

IO.readlines(“arquivo.txt”) { |linha| puts linha }

O código cliente não precisa tratar detalhes de fechamento de descritores de arquivo ou tratar exceções específicas.

O iteradores mais usados

Outro método utilizado frequentemente é o inject, que apresenta um valor inicial a cada iteração pela coleção:

[1, 2, 3, 4].inject(0) { |soma, i| soma + i }

No exemplo, o valor da variável soma inicial com o valor parametrizado com 0 e é substituído a cada iteração pela soma dele mesmo com o elemento que está sendo percorrido, ou seja, está funcionando nesse caso como um acumulador. O equivalente para calcular o produto seria:

[1, 2, 3, 4].inject(1) { |produto, i| produto * i }

O elemento inicial nesse caso precisa ser o 1, que é o elemento neutro da multiplicação.

O método inject também funciona sem valor inicial como parâmetro. Nesse caso ele utiliza internamente o primeiro valor da coleção como acumulador e segue o processo. Nos exemplo de soma e produto, podemos utilizar esse recurso para reescrever de forma mais sucinta:

[1, 2, 3, 4].inject { |soma, i| soma + i }
[1, 2, 3, 4].inject { |produto, i| produto * i }

Por fim, chegamos ao método reduce, utilizado para operações mais específicas que o inject, onde o objetivo é reduzir uma coleção a um único valor. Ele pode ser usado nesses exemplos para sem precisar sequer de um bloco. É possível passar o nome de um método como símbolo para que o mesmo seja aplicado (chamado) nos elementos da coleção:

[1, 2, 3, 4].reduce(:+)
[1, 2, 3, 4].reduce(1, :*)

Blocos como objetos

Bloco também podem ser referenciados como uma variável recebida na assinatura do método. Por padrão, o bloco recebido por um método fica disponível como último parâmetro da assinatura, precedido por &:

class ExemploParametro
    def passando_o_bloco(&bloco)
        @bloco = bloco
    end
    def chamando_o_bloco(param)
        @bloco.call(param)
    end
end

ex = ExemploParametro.new
ex.passando_o_bloco { |param| puts “Valor: #{param}” }
ex.chamando_o_bloco(10)

Mesmo com a bloco passado sendo tratado como variável no parâmetro, é possível utilizar o método block_given?.

Como visto no exemplo, a linguagem é muito flexível na manipulação e armazenamento de blocos.

Existe uma sintaxe especial para criar bloco e guardar em variáveis diretamente:

bloco = lambda { |param| puts “Param: #{param}” }
bloco.call 1
bloco.call “hello!”

O bloco declarado é armazenando em um objeto da classe Proc e referenciado pela variável atribuída, podendo ser chamado quantas vezes for necessário.

A partir da versão 1.9, uma nova sintaxe utilizando uma seta (->) foi introduzida e se tornou a mais comumente utilizada. O mesmo bloco do exemplo acima pode criado com a nova sintaxe:

bloco = -> param { puts “Param: #{param}” }

Blocos como closures

O funcionamento dos blocos no Ruby pode ser considerados como semelhante a closures em linguagens de programação funcionais: ao criar um bloco, o escopo ao qual o mesmo pertence é “lembrado” por ele, não importando se o bloco for chamado em outro contexto de execução.

Para exemplificar, iremos criar um método multiplicador que recebe apenas um parâmetro e o multiplica pelo primeiro parâmetro passado quando foi chamado pela primeira vez:

def multiplicador_com_memoria(multiplicador)
    -> n { multiplicador * n }
end

mul = multiplicador_com_memoria(10)
mul.call(1) # => 10
mul.call(2) # => 20

mul100 = multiplicador_com_memoria(100)
mul100.call(1) # => 100
mul100.call(2) # => 200

Cada instância de bloco criada e retornada pelo método se lembra e tem acesso às variáveis do seu escopo no momento da declaração do bloco. Assim, é possível ter várias instâncias do multiplicador parametrizadas com diferente números.

Conclusão

Nesse artigo descrevermos o uso de blocos em conjunto com iteradores para realizar a introdução ao funcionamento de closures em Ruby, um conceito fundamental para escrever programas mais sucintos, expressivos e performáticos, sendo uma peça fundamental do paradigma funcional.

Está com duvidas?

Aluno iwtraining tem acesso a um fórum exclusivo para discutir com os instrutores e outros alunos. Acesse agora mesmo!

Posts Relacionados

Deixe um comentário