Feliz Ano Novo, e bem-vindo de volta à Ruby Magic! Neste episódio de inverno, vamos mergulhar nas encadernações e nos escopos. Então ponham os vossos esquis e sigam-nos até ao fundo da floresta.
Pois passado, olhámos para os fechamentos em Ruby comparando blocos, procs e lambdas. Além das diferenças entre os três tipos, tocamos no que define um fechamento.
Um fechamento é uma função de primeira classe com um ambiente. O ambiente é um mapeamento para as variáveis que existiam quando o fechamento foi criado. O fechamento mantém seu acesso a essas variáveis, mesmo que elas estejam definidas em outro escopo.
Exploramos o equivalente do Ruby a funções de primeira classe, mas convenientemente pulamos sobre os ambientes. Neste episódio, vamos ver como esse ambiente funciona para fechamentos, classes e instâncias de classe, examinando como o Ruby lida com o escopo léxico através de seus bindings.
Escopo léxico
Na programação, escopo refere-se aos bindings disponíveis em uma parte específica do código. Um binding, ou ligação de nome, liga um nome a uma referência de memória, como o nome de uma variável ao seu valor. O escopo define o que significa self
, os métodos que podem ser chamados, e as variáveis que estão disponíveis.
Ruby, como a maioria das linguagens de programação modernas, usa um escopo estático, muitas vezes chamado de escopo léxico (ao contrário de escopo dinâmico). O escopo atual é baseado na estrutura do código e determina as variáveis disponíveis em partes específicas do código. Isto significa que o escopo muda quando o código salta entre métodos, blocos e classes, pois todos eles podem ter diferentes variáveis locais, por exemplo.
1 2 3 4 5 6 |
def bar foo = 1 foo end bar # => 1 |
Neste método, nós criamos uma variável local dentro de um método e a imprimimos para o console. A variável está no escopo dentro do método, como ela é criada lá.
1 2 3 4 5 6 7 |
foo = 1 def bar foo end bar # => NameError (undefined local variable or method `foo' for main:Object) |
Neste exemplo, nós criamos a variável fora do método. Quando chamamos a variável dentro de um método, obtemos um erro, uma vez que a variável está fora do escopo. Variáveis locais são bem delimitadas, significando que um método não pode acessar uma variável fora de si a menos que seja passada como argumento.
1 2 3 4 5 6 7 |
@foo = 1 def bar @foo end bar # => 1 |
Embora variáveis locais estejam disponíveis localmente, variáveis de instância estão disponíveis para todos os métodos de uma instância de classe.
Escopos e Encadernações de Processos Merecidos
Como vimos nos exemplos anteriores, o escopo é baseado na localização no código. Uma variável local definida fora de um método não está no escopo dentro do método, mas pode ser disponibilizada transformando-a em uma variável de instância. Métodos não podem acessar variáveis locais definidas fora deles porque os métodos têm seu próprio escopo, com seus próprios bindings.
Procs (incluindo blocos e lambda’s, por extensão) são diferentes. Sempre que um proc é instanciado, é criado um binding que herda referências às variáveis locais no contexto em que o bloco foi criado.
1 2 |
foo = 1 Proc.new { foo }.call # => 1 |
Neste exemplo, definimos uma variável chamada foo
para 1
. Internamente, o objeto Proc criado na segunda linha cria uma nova ligação. Ao chamar o proc, podemos pedir o valor da variável.
Desde que o binding é criado quando o proc é inicializado, não podemos criar o proc antes de definir a variável, mesmo que o bloco não seja chamado até que a variável seja definida.
1 2 3 |
proc = Proc.new { foo } foo = 1 proc.call # => NameError (undefined local variable or method `foo' for main:Object) |
Chamar o proc produzirá um NameError
, pois a variável não está definida nas encadernações do proc. Assim, qualquer variável acessada em um proc deve ser definida antes que o proc seja criado ou passado como argumento.
1 2 3 4 |
foo = 1 proc = Proc.new { foo } foo = 2 proc.call # => 2 |
Podemos, no entanto, alterar a variável depois de ter sido definida no contexto principal, uma vez que a encadernação do proc contém uma referência a ela em vez de copiá-la.
1 2 3 |
foo = 1 Proc.new { foo = 2 }.call foo #=> 2 |
Neste exemplo, podemos ver que a variável foo
aponta para o mesmo objecto quando está no proc como fora dele. Podemos atualizá-la dentro do proc para que a variável fora dele também seja atualizada.
Bindings
Para acompanhar o escopo atual, Ruby usa bindings, que encapsulam o contexto de execução em cada posição no código. O método binding
retorna um objeto Binding
que descreve os bindings na posição atual.
1 2 |
foo = 1 binding.local_variables # => |
O objeto binding tem um método chamado #local_variables
que retorna os nomes de todas as variáveis locais disponíveis no escopo atual.
1 2 |
foo = 1 binding.eval("foo") # => 1 |
Código pode ser avaliado no binding usando o método #eval
. O exemplo acima não é muito útil, pois simplesmente chamar foo
teria o mesmo resultado. No entanto, uma vez que um binding é um objeto que pode ser passado por aí, ele pode ser usado para algumas coisas mais interessantes. Vejamos um exemplo.
A Real-Life Example
Agora aprendemos sobre encadernações na segurança da nossa garagem, como levá-las para as encostas e brincar na neve. Para além do uso interno de encadernações em Ruby em toda a linguagem, existem algumas situações em que os objectos de encadernação são usados explicitamente. Um bom exemplo é o sistema de templating ERB-Ruby.
1 2 3 4 5 6 7 8 9 10 |
require 'erb' x = 1 def y 2 end template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`" |
Neste exemplo, criamos uma variável chamada x
, um método chamado y
, e um template ERB que faz referência a ambos. Passamos então o binding atual para ERB#result
, que avalia as tags ERB no template e retorna uma string com as variáveis preenchidas.
Atrás do hood, ERB usa Binding#eval
para avaliar o conteúdo de cada tag ERB no escopo do binding passado. Uma implementação simplificada que funciona para o exemplo acima poderia ser assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class DiyErb def initialize(template) @template = template end def result(binding) @template.gsub(/<%=(.+?)%>/) do binding.eval() end end end x = 1 def y 2 end template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`") template.result(binding) # => "x is 1, y() returns 2, self is `main`" |
A classe DiyErb
leva uma string de template na inicialização. O seu método #result
encontra todas as etiquetas ERB e substitui-as pelo resultado da avaliação do seu conteúdo. Para isso, chama Binding#eval
no binding passado, com o conteúdo das tags ERB.
Ao passar o binding atual ao chamar o método #result
, as chamadas eval
podem acessar as variáveis definidas fora do método, e mesmo fora da classe, sem ter que passá-las explicitamente.
Perdemos você na floresta?
Esperamos que você tenha gostado da nossa viagem de esqui na floresta. Fomos mais fundo em escopos e fechamentos, depois de os teres lustrado. Esperamos não te ter perdido no bosque. Por favor, informe-nos se você gostaria de aprender mais sobre encadernações, ou se tiver qualquer outro tópico Ruby que você gostaria de mergulhar em.
Poisas para nos seguir e, por favor, chute a neve de suas encadernações antes de deixá-las para o próximo esquiador. Se gosta destas viagens mágicas, talvez queira subscrever a Ruby Magic para receber um e-mail quando publicarmos um novo artigo cerca de uma vez por mês.