Escopo Léxico e Lazy-evaluation no R

Como já falei aqui, vou dar um curso de R, e começa na semana que vem. Estou analisando o material que já tenho pronto e percebi que não tenho nada muito estruturado sobre escopo e lazy-evaluation no R. Acho que isso é importante e estou inclinado a gastar algumas horas do curso falando sobre o tema. Então, o que segue são algumas anotações não estruturadas, para mim mesmo, mas que eventualmente podem ser úteis para outras pessoas.

Segundo a wikipedia, o escopo é “um contexto delimitante aos quais valores e expressões estão associados”. Em outras palavras, quando fazemos um programa, o escopo determina o que faz parte daquele contexto e o que não faz parte. Por exemplo, quando pedimos para o computados avaliar x + 5, precisamos de regras que definam onde acharmos o valor de x. Essas regras definem como é formado o escopo da linguagem. Na prática, isso é importante para evitar, por exemplo, que variáveis com o mesmo nome em contextos diferentes gerem conflitos na execução do programa da parte do computador. E por isso é importante que o programador conheça como funciona o escopo da linguagem em que ele está trabalhando. Aqui nós temos um paper dos criadores do R sobre o escopo da linguagem.

Algumas definições devem ser introduzidas, para que possamos entender como funciona o escopo no R. Vou introduzir as definições a partir de um exemplo do paper citado acima. Considerem a função abaixo.

f <- function(x) {
y <- 2 * x 
print(x)
print(y)
print(z)
}

Na função acima, x é um argumento da função ou parâmetro formal da função, y é uma variável local (pois é criada e definida no interior da função) e z é uma variável livre (não é criada no interior da função). Quando formos rodar (avaliar) essa função, precisamos passar o argumento da função, por exemplo, f(10). A função então sabe que y é 20 e pode imprimir o valor de x e de y, mas não de z. O que as regras de escopo devem determinar é onde a função deve olhar para determinar quanto é x, y e z.  Se z não tiver sido definido fora da função no ambiente global, o R não conseguirá determinar o seu valor e, portanto, retornará o erro. Se vocês rodarem f(10), verão que o R imprime o valor de x, de y e retorna um erro para z. Se, por outro lado, criarmos uma outra função, g, definida abaixo, e rodarmos g(10), o R retornará apenas um erro.

g <- function(x) {
y <- 2 * x 
print(z)
print(y)
print(x)
}

Em computação, nós dizemos que o R tem lazy-evaluation, e não strict evaluation. Ou seja, o R só tenta executar um comando no momento em que ele é chamado. Ele vai executando a função sequencialmente e checando se encontra os valores necessários enquanto executa o código.  Assim, uma função como a do código abaixo pode funcionar perfeitamente em R:

h <- function (x) {
  y <- 3*abs(x) + 1
  if (y < x) {
    z <- minha_funcao_nao_definida_em_lugar_algum()  
  }
  return(y)
}

Como y nunca será menor do que x, o que está dentro do if nunca é avaliado, e a função não retorna um erro. Essa é uma lição importante, porque se o que queremos executar dentro de um if estiver errado, mas a condição do if for verdadeira apenas em algumas condições, podemos testar nossa função e nunca ver um erro, e no entanto encontrarmos algum erro numa utilização em produção.

Retornando ao escopo. Esse post discute as diferenças conceituais entre escopo dinâmico e escopo léxico (que é o default do R). Eu não vou entrar aqui nas minúcias conceituais das definições, mas vocês podem ler o post se tiverem interesse no tema. Mais interessante para entender a diferença entre os dois tipos de escopo é o exemplo do post, que reproduzo abaixo.

a=1
b=2
f<-function(x)
{
  a*x + b
}
g<-function(x)
{
  a=2
  b=1
  f(x)
}
g(2)

Leia o código e me diga, qual a resposta para g(2)? A resposta eu dou logo abaixo, mas deve ser óbvio (se você entendeu o código minimamente) que g(2) deve retornar 4 ou 5. No escopo dinâmico, g(2) retornaria 5, num escopo léxico, retornaria 4 (que é o do R). A razão para tal é que no escopo léxico, o escopo de uma função é definido pelo ambiente em que ela foi criada. A função f foi criada no ambiente global e, portanto, as variáveis livres ‘a’ e ‘b’ terão seus valores determinados globalmente (que é o ambiente onde f foi criada). Numa linguagem de escopo dinâmico, o que importa é onde a função está sendo chamada. Nesse caso, as variáveis livres teriam seus valores determinados primeiramente localmente, dentro de g e, apenas caso não existissem esses valores em g, procuraríamos no ambiente global. Vale notar, ainda, que se a f tivesse sido criada localmente em g, então g(2) retornaria 5.

As coisas começam, porém, a complicar quando temos uma f definida globalmente e uma f definida localmente. Vejam o código abaixo:

f<-function(x)
{
  a*x + b
}
 
g<-function(x)
{
  a=1
  b=2
  f<-function(x)
  {
    b*x + a
  }
  f(x)
}
g(2)

Nós temos uma f definida globalmente e uma f definida localmente. Quando chamarmos g, qual função ‘f’ o R executará? Meu entendimento é que, do mesmo jeito que definimos variáveis locais e livres, podemos pensar nas funções como sendo locais ou livres (também podem ser argumentos, mas deixemos isso de lado por ora). No exemplo acima, a f é definida localmente, e portanto não há necessidade de procurar em outro ambiente a f. No exemplo anterior, a f era uma função livre e, portanto, o R procurava no ambiente global pela função f.

Se é verdade que nós conseguimos entender como o R opera seguindo suas regras de escopo, nem sempre isso funciona. Os problemas de escopo no R foram ilustrados muito bem, eu acho, pelo Christian Robert no blog dele há algum tempo. Considere o código abaixo:

f <- function() {
if (runif(1) > .5)
x <-  10
x
}

O valor de x é aleatoriamente local e global! Se a condição do if for verdadeira, x recebe 10, se não, x é o que quer que esteja definido globalmente! Se x não estiver definido globalmente, é possível tanto obter um erro “error in f() : object ‘x’ not found” como o valor 10.

Pra terminar esse post. Se você realmente entendeu o que eu escrevi, então deveria ser capaz de responder às perguntas 2 e 5 desse quiz feito pelo Hadley (do pacotge ggplo2).

Anúncios

Sobre Manoel Galdino

Corinthiano, Bayesiano e Doutor em ciência Política pela USP.
Esse post foi publicado em Política e Economia. Bookmark o link permanente.

2 respostas para Escopo Léxico e Lazy-evaluation no R

  1. brandizzi disse:

    Olá, Manoel!

    Não sei se é comum este uso da expressão “lazy evaluation” na comunidade R, mas ao menos fora ela tem um sentido bem diferente. Lazy evaluation refere-se à habilidade da linguagem de não executar as expressões que são passadas por parâmetros ou adicionadas a variáveis. R, de fato, usa lazy evaluation nos parâmetros. Considere o código abaixo:

    f <- function(a, b) {
    print('1st value will be printed')
    print(a)
    }

    f(2, print('ok'))
    # Output:
    # [1] "1st value will be printed"
    # [1] 2

    f(print('ok'), 2)
    # Output:
    # [1] "1st value will be printed"
    # [1] "ok"
    # [1] "ok"

    Note que na primeira chamada "ok" nunca é impresso, e na segunda é impresso logo após a mensagem. Agora, considere o exemplo em Python 3.2:

    def f(a, b):
    print("1st value will be printed")
    print(a)

    f(2, print('ok'))
    # Output:
    # ok
    # 1st value will be printed
    # 2

    f(print('ok'), 2)
    # Output:
    # ok
    # 1st value will be printed
    # None

    Como Python usa eager evaluation nos parâmetros, nota-se que "ok" sempre é impresso (e sempre antes da função ser executada), mesmo se o valor retornado por "print('ok')" não for usado.

    Em certo sentido, de fato, o "if" do seu exemplo usa lazy evaluation. Entretanto, nunca vi alguém usar a expressão para se referir a comandos de controle de fluxo, como if, for etc. Quase sempre, quando se fala de lazy evaluation, fala-se de lazy evaluation ao atribuir um valor a uma varável (em R, apenas a parâmetros, já que "v >> def h(x):
    … y = 3*abs(x) + 1
    … if y >> h(-2)
    7
    Entretanto, nunca vi alguém dizer que Python tem lazy evaluation (neste sentido; há quem mencione referindo-se a generators).

    Resumindo, temo que usar a expressão “lazy evaluation” para o que vocẽ exemplificou vai causar mais confusão que esclarecimento. O que seu exemplo mostra é que R não exige que uma função ou variável seja *declarada* antes de ser usada (como se exigem em Java, C[1] etc.) Se quiser um exemplo de lazy evaluation menos controverso, acredito que seja melhor falar dos parãmetros.

    Até!

    [1] Em runtime, aliás; dá para compilar um módulo com funções e variáveis não declaradas.

  2. brandizzi disse:

    O código ficou bem ruim :-/ Vai no pastebin:

    Lazy evaluation em R: http://pastebin.com/5HpFqbGh
    Eager evaluation em Python: http://pastebin.com/mrJN6SRK
    Python não demandando declarações: http://pastebin.com/hCAkn3EB

    Até!

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s