Lazy Evaluation (Avaliação Preguiçosa) Leonardo Lucena – IFRN, 2011 Adaptação das Transparências de Graham Hutton (http://www.cs.nott.ac.uk/~gmh/book.html) These slides may be used or modified for any educational purpose on a non-profit-making basis, provided that I am acknowledged as the original author. Até agora não vimos em detalhes como expressões de Scala são avaliadas De fato, a avaliação pode ser feita de duas formas • Avalição precoce (como em Java) • Avaliação preguiçosa 1 A avaliação preguiçosa é uma técnica de avalição simples que entre outras coisas: • Evita a avaliação desnecessária • Permite programas mais modulares • Permite escrever listas infinitas Linguagens funcionais com avaliação preguiçosa são chamadas de linguagens funcionais preguiçosas 2 Expressões são avaliadas, ou reduzidas, sucessivamente aplicando definições até que nenhuma outra simplificação for possível. Por exemplo def quadrado(n: int) = n*n 3 A expressão quadrado(3+4) pode ser avaliada usando a seguinte sequência de reduções = = = quadrado (3+4) quadrado(7) 7*7 49 4 Outra = = = forma de redução é a seguinte: quadrado(3 + 4) (3 + 4) * (3 + 4) 7 * (3 + 4) Desta 7 * 7 = 49 vez aplicamos quadrado antes de efetuar a adição, mas o resultado é o mesmo 5 A cada passo durante a avaliação de uma expressão podemos ter várias possíveis sub-expressões que podem ser reduzidas. Há duas estratégias para decidir qual sub-expressão reduzível escolher: • Redução mais interna Uma sub-expressão mais interna • Redução mais externa Uma sub-expressão mais externa Como comparar as duas estratégias? 6 Seja a seguinte definição def loop: List[Int] = loop.tail Vamos avaliar a expressão primeiro(1, loop) usando as duas estratégias de redução. 7 1. Redução mais Interna = = = primeiro(1, loop) primeiro(1, loop.tail) primeiro(1, (loop.tail).tail) primeiro(1, ((loop.tail).tail).tail) = … A estratégia não termina 8 2. Redução mais externa = primeiro(1, loop) 1 A estratégia dá o resultado em apenas um passo 9 A redução mais externa pode dar um resultado nos casos em que a redução mais interna falha em terminar; Para uma dada expressão, se existir alguma sequência de reduções que termina, então a redução mais externa também termina e com o mesmo resultado. 10 MAIS INTERNO MAIS EXTERNO = quadrado(3 + 4) = quadrado(7) = (3 + 4) * (3 + 4) = 7 * 7 = 7 * (3 + 4) 49 = 7 * 7 = quadrado(3 + 4) 49 A versão mais externa é ineficiente, a expressão 3+4 é avaliada duas vezes Fato: Redução mais externa pode exigir mais passos que a redução mais interna 11 O problema pode ser resolvido usando ponteiros para indicar compartilhamento de expressões durante a avaliação: = = = quadrado(3 + 4) ( • * • ) (3 + 4) 7 * 7 49 12 Assim, temos uma nova estratégia de redução • Avaliação Preguiçosa = Redução mais externa + Compartilhamento Fatos • Avaliação preguiçosa nunca requer mais passos de redução do que a redução mais interna. • Scala permite usar avaliação preguiçosa 13 Avaliação precoce (mais interna) def quadrado(n: Int) = n*n Avaliação Passa o valor por nome preguiçosa def quadrado(n: => Int) = { lazy val x = n x * x } Só avalia n quando for necessário 14 Só avalia o argumento a def primeiro[T](a: => T, b: =>T) = a Se ... Então .. Se não ... def se[T](b: Boolean, entao: => T, senao: => T) = { if (b) entao else senao } > se(true, println(“OK”), loop); OK > se(false, println(“OK”), loop); Exception in thread "main" java.lang.StackOverflowError 15 Além das vantagens de terminação, o uso de avaliação preguiçosa permite programar com listas infinitas de valores def uns: List[Int] = 1 :: uns > Uns 1 :: 1 :: 1 :: 1 :: ... Scala usa Stream para listas preguiçosas def uns:Stream[Int]=Stream.cons(1,uns) 16 Redução interna uns.head = = = = Avaliação (1::uns).head (1::1::uns).head (1::1::1::uns).head ... Preguiçosa uns.head = (1::uns).head = 1 17 Em geral: • Usando avaliação preguiçosa, expressões só são avaliadas à medida que são exigidas para produzir o resultado final > def uns:Stream[Int]=Stream.cons(1,uns) > println(uns) Stream(1, ?) > println(uns.take(5).sum) 5 > println(uns.sum) java.lang.OutOfMemoryError: Java heap space 18 Podemos gerar listas finitas pegando elementos de listas infinitas. > uns.take(5) List(1,1,1,1,1) Avaliação Preguiçosa permite criar programas mais modulares separando os controle dos dados dados uns.take(5) controle 19 Um procedimento simples para gerar a lista infinita de todos os números primos é: 1. Escreva a lista 2, 3, 4, ... ; 2. Marque o primeiro valor p na lista de primos; 3. Apague todos os múltiplos de p da lista; 4. 2Retorne 3 4 ao5passo 6 2. 7 8 3 5 7 5 7 7 9 10 11 12 ... 9 11 ... 11 ... 11 ... 11 ... 20 Este procedimento é conhecido como Crivo de Aristóteles. object Primos extends App { 1 def primos = crivo(Stream from 2) def crivo(a:Stream[Int]):Stream[Int]={ val (p, xs) = (a.head, a.tail) val ys = xs.filter( _ % p != 0) 3 Stream.cons(p,2 crivo(ys)) 4 } println(primos.take(5).force) } 21 Liberando a geração de primos da restrição de finitude, obtemos uma definição modular na qual diferentes condições de contorno podem ser impostas em diferentes situações. • Seleção dos primeiros primos primos.take(10) • Seleção do primos menores do que 15 primos.takeWhile(_ < 15) 22 1. Defina um programa contendo a função def fibs: Stream[Int] = … que gera a série infinita de Finonacci 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, … usando o seguinte procedimento: a. os dois primeiros números são 0 e 1; b. o próximo é a soma dos dois anteriores; c. retorne para o passo b. 2. Defina a função def fib(n: Int): Int = … que calcula o n-ésimo número de Fibonacci. 23