scala através de exemplos · scala através de exemplos draft january 31, 2013 martin odersky...

147
Scala Através de Exemplos DRAFT January 31, 2013 Martin Odersky PROGRAMMING METHODS L ABORATORY EPFL S WITZERLAND TRADUZIDO POR:VINICIUS MIANA E ANTONIO BASILE

Upload: hakhanh

Post on 19-Sep-2018

240 views

Category:

Documents


0 download

TRANSCRIPT

Scala Atravésde Exemplos

DRAFTJanuary 31, 2013

Martin Odersky

PROGRAMMING METHODS LABORATORY

EPFLSWITZERLAND

TRADUZIDO POR: VINICIUS MIANA E ANTONIO BASILE

Índice

1 Introdução 1

2 Um primeiro exemplo 3

3 Programando com Atores e Mensagens 7

4 Expressões e Funções Simples 11

4.1 Expressões e Funções Simples . . . . . . . . . . . . . . . . . . . . . . . . 11

4.2 Parâmetros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

4.3 Expressões Condicionais . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

4.4 Exemplo: Raiz Quadrada pelo Método de Newton . . . . . . . . . . . . . 15

4.5 Aninhamento de Funções . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

4.6 Recursão de Cauda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

5 Funções de Primeira Classe 21

5.1 Funções Anônimas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

5.2 Currying . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

5.3 Exemplo: Encontrando Pontos Fixos de Funções . . . . . . . . . . . . . 25

5.4 Sumário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

5.5 Elementos da Linguagem Vistos Até Aqui . . . . . . . . . . . . . . . . . . 28

6 Classes e Objetos 31

7 Classes Case e Casamento de Padrões 43

7.1 Classes Case e Objetos Case . . . . . . . . . . . . . . . . . . . . . . . . . . 46

7.2 Casamento de Padrões . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

8 Tipos Genéricos e Métodos 53

8.1 Parâmetros Tipo Ligados . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

8.2 Anotações de Variância . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58

iv ÍNDICE

8.3 Lower Bounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60

8.4 Tipos Minimais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

8.5 Tuplas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

8.6 Funções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

9 Listas 65

9.1 Usando Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

9.2 Definição da classe List I: Métodos de Primeira Ordem . . . . . . . . . . 67

9.3 Exemplo: Merge sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71

9.4 Definição da classe List II: Métodos de Alta Ordem . . . . . . . . . . . . 72

9.5 Sumário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

10 For-Comprehensions 81

10.1 O Problema das N-Rainhas . . . . . . . . . . . . . . . . . . . . . . . . . . 82

10.2 Pesquisando com For-Comprehensions . . . . . . . . . . . . . . . . . . . 83

10.3 Tradução de For-Comprehensions . . . . . . . . . . . . . . . . . . . . . . 84

10.4 Laços For . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86

10.5 Generalizando For . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87

11 Estados Mutáveis 89

11.1 Objetos Mutáveis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89

11.2 Estruturas Imperativas de Controle . . . . . . . . . . . . . . . . . . . . . 93

11.3 Exemplo Estendido: Simulação de Eventos Discretos . . . . . . . . . . . 94

11.4 Sumário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

12 Computando com Streams 101

13 Iteradores 105

13.1 Métodos dos Iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

13.2 Construindo Iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

13.3 Usando Iteradores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

14 Valores Preguiçosos (Lazy) 111

15 Parâmetros Implícitos e Conversões 115

ÍNDICE v

16 Inferência de Tipos de Hindley/Milner 119

17 Abstracões para Concorrência 127

17.1 Sinais e Monitores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127

17.2 SyncVars . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

17.3 Futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

17.4 Computação Paralela . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

17.5 Semáforos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

17.6 Leitores/Escritores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

17.7 Canais Assíncronos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132

17.8 Canais Síncronos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133

17.9 Trabalhadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134

17.10Caixas Postais . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

17.11Actors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139

Capítulo 1

Introdução

Scala facilita a integração entre a programação orientada a objetos e a programaçãofuncional. Foi criada para expressar padrões de programação comuns de formaconcisa, elegante e fortemente tipada. Scala introduz diversas construções delinguagem inovadoras. Por exemplo:

• Tipos abstratos e composições mixin unificam conceitos de objetos emódulos de sistema.

• Casamento de padrões sobre hierarquias de classe unificam o acesso a dadosfuncional e orientado a objetos, simplificando bastante o processamento deárvores XML.

• Sintaxe flexível e sistema de tipos que habilitam a construção de avançadasbibliotecas e um novas linguagens específicas a um domínio.

Ao mesmo tempo, Scala é compatível com Java. Bibliotecas Java e frameworkspodem ser usados sem código extra ou declarações adicionais. Este documentointroduz Scala de um modo informal, através de uma sequência de exemplos.

Os capítulos 2 e 3 salientam alguns dos aspectos que tornam Scala interessante. Oscapítulos seguintes introduzem as construções da linguagem Scala de um modomais completo, começando com expressões e funções simples, e desenvolvendoaté objetos e classes, listas e streams, estado mutável, casamento de padrões atéexemplos mais completos que mostram interessantes técnicas de programação.A presente exposição informal pretende ser complementada por um Manual deReferência da Linguagem Scala, que especificarão Scala de modo mais detalhadoe preciso.

Reconhecimento. Temos um grande débito ao maravilhoso livro de Abelson eSussman “Structure and Interpretation of Computer Programs”[ASS96]. Muitosexemplos e exercícios deles também estão presentes aqui. Naturalmente a

2 Introdução

linguagem utilizada em cada caso foi mudada do Scheme para o Scala. Além disso,os exemplos fazem uso de construções da orientação a objetos do Scala onde istofoi considerado apropriado.

Capítulo 2

Um primeiro exemplo

Para começar, apresentamos um primeiro exemplo, a implementação de Quicksortem Scala.

def sort(xs: Array[Int]) {def swap(i: Int, j: Int) {val t = xs(i); xs(i) = xs(j); xs(j) = t

}def sort1(l: Int, r: Int) {val pivot = xs((l + r) / 2)var i = l; var j = rwhile (i <= j) {while (xs(i) < pivot) i += 1while (xs(j) > pivot) j -= 1if (i <= j) {swap(i, j)i += 1j -= 1

}}if (l < j) sort1(l, j)if (j < r) sort1(i, r)

}sort1(0, xs.length - 1)

}

A implementação se parece muito com a que você faria em Java ou C. Nósusamos os mesmos operadores e estruturas de controle. Existem também algumaspequenas diferenças sintáticas, particularmente:

• As declarações começam usando palavras reservadas. Particularmente,declarações de funções são iniciadas com a palavra def, declarações de

4 Um primeiro exemplo

variáveis são iniciadas com a palavra var e declaração de constantes(chamadas de valores) são iniciadas com a palavra val.

• O tipo de um parâmetro em uma função é declarado após o nome doparâmetro seguido de dois pontos(:). O tipo pode ser omitido quando ocompilador for capaz de inferi-lo pelo contexto.

• A declaração de vetores do tipo T é feita usando a expressão Array[T] ao invésde T[]. O i-ésimo elemento de um vetor a é acessado usando a(i) ao invés dea[i].

• Funções podem ser aninhadas umas dentro das outras. Funções aninhadaspodem acessar parâmetros e variáveis locais de suas funções externas. Porexemplo, o nome do vetor xs é visível nas funções swap e sort1, e portantonão precisam ser passadas como um parâmetro para elas.

Pelo que vimos, Scala se parece com uma linguagem bem convencional comalgumas peculiaridades sintáticas. De fato é possível escrever programas em estiloimperativo ou orientado a objetos. Isto é importante, porque é uma das coisas quefacilitam combinar componentes Scala com componentes escritos em linguagensconvencionais, tais como Java, C# ou Visual Basic. Entretanto, também é possívelescrever programas num estilo completamente diferente. Aqui está o Quicksortnovamente, desta vez escrito em estilo funcional.

def sort(xs: Array[Int]): Array[Int] = {if (xs.length <= 1) xselse {val pivot = xs(xs.length / 2)Array.concat(sort(xs filter (pivot >)),

xs filter (pivot ==),sort(xs filter (pivot <)))

}}

O programa funcional captura a essência do algoritmo quicksort de modo conciso:

• Se o vetor está vazio ou consiste de um único elemento, já está ordenado,então retorne imediatamente.

• Se o vetor não está vazio, escolha um elemento do meio do vetor como pivô.

• Particione o vetor em dois subvetores contendo, respectivamente oselementos que são menores que o elemento pivô, maiores que, e um terceirovetor que contém elementos iguais ao pivô.

• Ordene os dois primeiros subvetores por uma chamada recursiva da funçãode ordenação.1

1Isso não é exatamente o que o algoritmo imperativo faz; este último particiona o vetor em doissubvetores contendo elementos menores que ou maiores ou iguais ao pivô.

5

• O resultado é obtido pela concatenação dos três subvetores.

Tanto a implementação imperativa quanto a funcional tem mesma complexidadeassintótica – O(N l og (N )) no caso médio e O(N 2) no pior caso. Mas ondea implementação imperativa opera localmente, modificando o vetor original, aimplementação funcional retorna um novo vetor ordenado e deixa o vetor originalinalterado. A implementação funcional, portanto, requer mais memória transienteque a imperativa.

A implementação funcional faz parecer que Scala é uma linguagem especializadapara operações funcionais sobre vetores. De fato, não é; todas as operações usadasno exemplo são simples métodos de biblioteca de uma classe sequência Seq[T]

que é parte da biblioteca padrão Scala, e onde ela mesma é implementada emScala. Como vetores são instâncias de Seq, todos os métodos de sequência estãodisponíveis para eles.

Em particular, há o método filter que recebe como argumento uma funçãopredicado. Esta função predicado deve mapear elementos do vetor para valoresboleanos. O resultado de filter é um vetor consistindo de todos os elementos dovetor original para o qual a função predicado é verdadeira. O método filter de umobjeto tipo Array[T] portanto tem a assinatura.

def filter(p: T => Boolean): Array[T]

Aqui, T=>Boolean é o tipo das funções que recebem um elemento do tipo T eretornam um valor booleano do tipo Boolean. Funções como filter que recebemuma outra função como argumento ou retornam uma função como resultado sãochamadas funções de alta ordem.

Scala não faz distinção entre nomes de identificadores e nomes de operadores. umidentificador pode ser ou uma sequência de letras ou digitos que começam comuma letra, ou podem ser uma sequência de caracteres especiais, tais como “+”, “*”,ou “:”. Qualquer identificador pode ser usado como operador infixo em Scala.A operação binária E op E ′ é sempre interpretado como a chamada de métodoE .op(E ′). Isso vale também para operadores binários infixos que iniciam comuma letra. Consequentemente, a expressão xs filter (pivot >) é equivalenteà chamada de método xs.filter(pivot >).

No programa quicksort, filter é aplicado três vezes a um argumento de funçãoanônima. O primeiro argumento, pivot >, representa uma função que recebe umargumento x e retorna o valor pivot > x. Este é um exemplo de uma funçãoparcialmente aplicada. Um outro modo, equivalente de se escrever esta função,que torna o argumento oculto explicito é x => pivot > x. A função é anônima,ou seja, não é definida com um nome. O tipo do parâmetro x é omitido porqueum compilador Scala pode inferí-lo automáticamente a partir do contexto onde afunção é usada. Resumindo, xs.filter(pivot >) retorna uma lista consistindo detodos os elementos da lista xs que são menores que pivot.

6 Um primeiro exemplo

Olhando novamente em detalhes para a primeira, implementação imperativa doQuicksort, percebemos que muitos dos construtores da linguagem usados nasegunda solução estão presentes, embora de modo distinto.

Por exemplo, operadores binários padrão, tais como +, -, ou < não são tratados demodo especial. Assim como append, são métodos de seus operandos esquerdos.Consequentemente, a expressão i+1 é vista como a invocação de i.+(1) do método+ do valor inteiro de i. De fato, um compilador é livre (se moderadamente esperto,ainda que esperado) para reconhecer o caso especial da chamada de método +

sobre argumentos inteiros e para gerar código inline eficiente para isso.

Por eficiência e melhor detecção de erros o laço while é um construtor primitivoem Scala. Mas em princípio, poderia do mesmo modo ser uma função predefinida.Aqui está uma possível implementação para ele:

def While (p: => Boolean) (s: => Unit) {if (p) { s ; While(p)(s) }

}

A função While recebe como primeiro parâmetro uma função teste, que não recebeparâmetros e produz um valor boleano. Como segundo parâmetro recebe umafunção comando que também não recebe parâmetros e produz como resultadoo tipo Unit. While invoca a função comando enquanto a função teste produzirverdadeiro.

O tipo Scala Unit corresponde grosseiramente ao void no Java; é usado sempreque uma função não retornar um resultado interessante. De fato, porque Scalaé uma linguagem orientada a expressões, cada função retorna algum resultado.Se nenhuma expressão de retorno é explicitamente fornecida, o valor (), que épronunciado “unit”, é assumido. Este valor é do tipo Unit. Funções que retornam“unit” são também chamadas procedimentos. Aqui está uma formulação mais“orientada a expressão” da função swap na primeira implementação do quicksort,que explicita isto:

def swap(i: Int, j: Int) {val t = xs(i); xs(i) = xs(j); xs(j) = t()

}

O valor resultante desta função é simplesmente sua última expressão—uma palavrachave return não é necessária. Observe que funções que retornam um valorexplícito sempre precisam de um “=” antes de seus corpos ou expressões dedefinição.

Capítulo 3

Programando com Atores eMensagens

Aqui está um exemplo que mostra uma área de aplicação para a qual Scala éparticularmente indicada. Considere a tarefa de implementar um serviço deleilão eletrônico. Podemos usar um modelo de processo no estilo actor do Erlangpara implementar os participantes do leilão. Actors são objetos para os quais asmensagens são enviadas. Cada actor tem uma caixa de correio para suas mensagensde entrada que é representada para uma fila. Pode trabalhar sequencialmente nasmensagens da sua caixa de correio, ou buscar por mensagens que casam com algumpadrão.

Para cada item negociado há um actor leiloeiro que publica a informação sobre oitem negociado, que aceita ofertas de clientes e que se comunica com o vendedore com o vencedor do leilão para fechar a transação. Apresentamos uma visãosuperficial de uma implementação aqui.

Como primeiro passo, definimos as mensagens que são trocadas durante um leilão.Há duas classes base abstratas AuctionMessage para mensagens de clientes doserviço de leilão, e AuctionReply para respostas do serviço aos clientes. Para ambasas classes base há um número de casos definidos na Figura 3.1.

Para cada classe base, há um número de classes case que definem o formato demensagens particulares dentro da classe. Estas mensagens podem em último casoser mapeadas a pequenos documentos XML. Esperamos que hajam ferramentasautomáticas que convertam entre documentos XML e estruturas de dados internas,tais como as definidas acima.

A Figura 3.2 apresenta uma implementação Scala para a classe Auction para actorsdo leilão que coordenam os lances sobre um item. Objetos para esta classe sãocriados pela indicação

• Um actor vendedor que precisa ser notificado quando o leilão terminou,

8 Programando com Atores e Mensagens

import scala.actors.Actor

abstract class AuctionMessagecase class Offer(bid: Int, client: Actor) extends AuctionMessagecase class Inquire(client: Actor) extends AuctionMessage

abstract class AuctionReplycase class Status(asked: Int, expire: Date) extends AuctionReplycase object BestOffer extends AuctionReplycase class BeatenOffer(maxBid: Int) extends AuctionReplycase class AuctionConcluded(seller: Actor, client: Actor)

extends AuctionReplycase object AuctionFailed extends AuctionReplycase object AuctionOver extends AuctionReply

Listagem 3.1: Definição das Mensagens de um serviço de leilão

• o lance mínimo,

• a data de quando o leilão foi fechado.

O comportamento do actor é definido por seu método act. Este método selecionarepetidamente (usando receiveWithin) uma mensagem e reage a ela, até queo leilão seja fechado, o que é sinalizado por uma mensagem TIMEOUT. Antesde finalmente parar, permanece ativo para um outro período determinado pelaconstante timeToShutdown e replica para ofertas posteriores que o leilão estáfechado.

Aqui estão algumas explicações extras sobre os construtores usados nesteprograma:

• O método receiveWithin da classe Actor recebe como parâmetro um prazodado em milisegundos e uma função que processa mensagens na caixa decorreio. A função é dada por uma sequência de cases que especificam umpadrão e uma ação para mensagens que casam com o padrão. O métodoreceiveWithin seleciona a primeira mensagem da caixa de correio que casacom um destes padrões e aplica a ação correspondente a ele.

• O último case de receiveWithin é guardado por um padrão TIMEOUT. Senenhuma outra mensagem foi recebida nesse meio tempo, este padrão édisparado após o prazo que foi passado como argumento para o métodoenvolvente receiveWithin. TIMEOUT é uma mensagem especial, que édisparada pela própria implementação do Actor.

• Mensagens de resposta são enviadas usando sintaxe da formadestino ! AlgumaMensagem. ! é usado aqui como um operador binário

9

class Auction(seller: Actor, minBid: Int, closing: Date) extends Actor {val timeToShutdown = 36000000 // msecval bidIncrement = 10def act() {

var maxBid = minBid - bidIncrementvar maxBidder: Actor = nullvar running = truewhile (running) {receiveWithin ((closing.getTime() - new Date().getTime())) {

case Offer(bid, client) =>if (bid >= maxBid + bidIncrement) {

if (maxBid >= minBid) maxBidder ! BeatenOffer(bid)maxBid = bid; maxBidder = client; client ! BestOffer

} else {client ! BeatenOffer(maxBid)

}case Inquire(client) =>client ! Status(maxBid, closing)

case TIMEOUT =>if (maxBid >= minBid) {

val reply = AuctionConcluded(seller, maxBidder)maxBidder ! reply; seller ! reply

} else {seller ! AuctionFailed

}receiveWithin(timeToShutdown) {

case Offer(_, client) => client ! AuctionOvercase TIMEOUT => running = false

}}

}}

}

Listagem 3.2: Implementação do Serviço de Leilão

10 Programando com Atores e Mensagens

com um actor e uma mensagem como argumentos. Isto é equivalenteem Scala ao chamado de método destino.!(AlgumaMensagem), ou seja,a invocação do método ! do actor destino com a mensagem dada comoparâmetro.

A discussão precedente deu uma idéia de programação distribuída em Scala. Issodá a sensação que Scala tem um rico conjunto de construtores que suportamprocessos actor, envio e recebimento de mensagens, programação com timeoutsetc. De fato, o oposto é verdadeiro. Todos os construtores discutidos acima sãooferecidos como métodos na classe biblioteca Actor. Aquela classe é ele mesmaimplementada em Scala, baseado no modelo thread subjacente à linguagemhospedeira (Java ou .NET). A implementação de todas as características da classeActor usada aqui é dada na Seção 17.11.

As vantagens da abordagem baseada em biblioteca são a relativa simplicidadeda linguagem núcleo e a flexibilidade para os criadores de biblioteca. Como alinguagem núcleo não precisa especificar detalhes da comunicação dos processosde alto nível, pode ser mantida mais simples e geral. Como um modelo particularde mensagens numa caixa de correio é um módulo biblioteca, pode ser modificadolivremente se um diferente modelo for necessário em algumas aplicações. Aabordagem requer, entretanto, que a linguagem núcleo seja expressiva o suficientepara prover as abstrações linguísticas necessárias de um modo conveniente. Scalafoi criada com isto em mente; um de seus maiores objetivos de design foi deixá-laflexível o suficiente para atuar como uma conveniente linguagem hospedeira paradomínios de linguagens específicos implementados por módulos de biblioteca.Por exemplo, a construção de comunicação actor apresentada acima pode servista como um desses domínios específicos de linguagem, que conceitualmenteestendem o núcleo Scala.

Capítulo 4

Expressões e Funções Simples

Os exemplos anteriores deram uma ideia do que pode ser feito com Scala. Agora,introduzimos as suas construções de linguagem uma a uma de uma maneira maissistemática. Vamos começar com os menores elementos: expressões e funções.

4.1 Expressões e Funções Simples

Scala vem com um interpretador que pode ser visto como uma calculadorasofisticada. O usuário interage com a calculadora digitando expressões. Acalculadora retorna o resultado do cálculo e o seu tipo de dado. Por exemplo:

scala> 87 + 145unnamed0: Int = 232

scala> 5 + 2 * 3unnamed1: Int = 11

scala> "hello" + " world!"unnamed2: java.lang.String = hello world!

Também é possível dar nome a uma sub-expressão e usar o nome, ao invés daexpressão, nas expressões seguintes:

scala> def scale = 5scale: Int

scala> 7 * scaleunnamed3: Int = 35

scala> def pi = 3.141592653589793pi: Double

12 Expressões e Funções Simples

scala> def radius = 10radius: Int

scala> 2 * pi * radiusunnamed4: Double = 62.83185307179586

Definições começam com a palavra reservada def. Elas introduzem um nome querepresenta a expressão que vem depois do símbolo =. O intepretrador respondecom o nome introduzido e o seu tipo de dado.

Executando uma definição tal como def x = e não avaliará a expressão e. Ao invése é avaliado sempre que x for usado. Alternativamente, Scala oferece um valordefinição val x = e, o qual avalia o lado direito de e como parte da avaliação dadefinição. Se x é então usado subsequentemente, é imediatamente substituído pelovalor pré-computado de e, logo a expressão não precisa ser avaliada novamente.

Como as expressões são avaliadas? Uma expressão consistindo de operadores eoperandos é avaliada pela repetida aplicação dos seguintes passos de simplificação:

• escolha a operação mais a esquerda.

• avalie seu operando

• aplique o operador aos valores do operando.

Um nome definido por def é avaliado substituindo o nome pela definição do ladodireito (não avaliada). Um nome definido por val é avaliado pela substituição donome pelo valor da definição do lado direito. O processo de avaliação pára assimque encontrarmos um valor. Um valor é algum dado, tal como uma cadeia decaracteres, um número, um vetor, ou uma lista.

Exemplo 4.1.1 Aqui está uma avaliação de uma expressão aritmética.

(2 * pi) * radius→ (2 * 3.141592653589793) * radius→ 6.283185307179586 * radius→ 6.283185307179586 * 10→ 62.83185307179586

O processo de simplificar gradualmente expressões para valores é chamadoredução.

4.2 Parâmetros

Usando def, pode-se também definir funções com parâmetros. Por exemplo:

4.2 Parâmetros 13

scala> def square(x: Double) = x * xsquare: (Double)Double

scala> square(2)unnamed0: Double = 4.0

scala> square(5 + 3)unnamed1: Double = 64.0

scala> square(square(4))unnamed2: Double = 256.0

scala> def sumOfSquares(x: Double, y: Double) = square(x) + square(y)sumOfSquares: (Double,Double)Double

scala> sumOfSquares(3, 2 + 2)unnamed3: Double = 25.0

Parâmetros de funções seguem o nome da função e sempre são envolvidos porparênteses. Cada parâmetro vem com um tipo que segue o nome do parâmetroe dois pontos. Até aqui, só precisamos de tipos numéricos, tal como o tiposcala.Double dos números de precisão dupla. Scala define tipos aliases para algunstipos básicos, logo podemos escrever tipos numéricos como em Java. Por exemplo,double é um tipo alias de scala.Double e int é um tipo alias para scala.Int.

Funções com parâmetros são avaliadas analogamente a operadores em expressões.Primeiro, os argumentos da função são avaliados (da esquerda para direita). Então,a aplicação da função é substituído pelo lado direito da função, e ao mesmo tempotodos os parâmetros formais são substituídos pelos seus argumentos atuais.

Exemplo 4.2.1

sumOfSquares(3, 2+2)→ sumOfSquares(3, 4)→ square(3) + square(4)→ 3 * 3 + square(4)→ 9 + square(4)→ 9 + 4 * 4→ 9 + 16→ 25

O exemplo mostra que o interpretador reduz argumentos de funções a valores antesde reescrever a aplicação da função. Pode-se ao invés escolher aplicar a função aargumentos não reduzidos. Isto levará a seguinte sequência de redução:

sumOfSquares(3, 2+2)→ square(3) + square(2+2)

14 Expressões e Funções Simples

→ 3 * 3 + square(2+2)→ 9 + square(2+2)→ 9 + (2+2) * (2+2)→ 9 + 4 * (2+2)→ 9 + 4 * 4→ 9 + 16→ 25

A segunda ordem de avaliação é conhecida como chamada por nome, e a primeirapor chamada por valor. Para expressões que se utilizam apenas de funções e queportanto podem ser reduzidas com o modelo de substituição, ambos os esquemaslevam ao mesmo valor final.

Chamada por valor tem a vantagem de evitar avaliações repetidas de argumentos.Chamada por nome tem a vantagem de evitar avaliações de argumentos quando oparâmetro não é usado pela função. Chamada por valor é geralmente mais eficienteque chamada por nome, mas uma avaliação de chamada por valor pode entrar emlaço infinito, enquanto uma avaliação de chamada por nome termina. Considere:

scala> def loop: Int = looploop: Int

scala> def first(x: Int, y: Int) = xfirst: (Int,Int)Int

Então first(1, loop) é reduzido com uma chamada por nome a 1, enquanto omesmo termo, através de uma chamada por valor reduz a si mesmo repetidamente,logo a avaliação não termina.

first(1, loop)→ first(1, loop)→ first(1, loop)→ ...

Scala usa chamada por valor por default, mas muda para avaliação de chamada pornome se o tipo do parâmetro for precedido por =>.

Exemplo 4.2.2

scala> def constOne(x: Int, y: => Int) = 1constOne: (Int,=> Int)Int

scala> constOne(1, loop)unnamed0: Int = 1

scala> constOne(loop, 2) // leva a laco infinito^C // para a execucao com Ctrl-C

4.3 Expressões Condicionais 15

4.3 Expressões Condicionais

O if-else do Scala leva a uma escolha entre duas alternativas. Sua sintaxe éa mesma do if-else do Java. Mas onde o if-else do Java pode ser usadosomente como uma alternativa entre comandos, Scala permite a mesma sintaxepara escolher entre duas expressões. Isso porque o if-else do Scala serve tambémcomo um substituto para a expressão condicional do Java ... ? ... : ....

Exemplo 4.3.1

scala> def abs(x: Double) = if (x >= 0) x else -xabs: (Double)Double

Expressões boleanas em Scala são similares as em Java; são formadas a partir deconstantes. true e false, operadores de comparação, negação boleana ! e osoperadores boleanos && and ||.

4.4 Exemplo: Raiz Quadrada pelo Método de Newton

Agora ilustraremos elementos da linguagem intruduzidos até aqui na construçãode um programa mais interessante. A tarefa é escrever um função

def sqrt(x: Double): Double = ...

que computa a raiz quadrada de x.

Um modo comum de se computar raizes quadradas é pelo método dasaproximações sucessivas de Newton. Inicia-se com um palpite inicial y (digamos:y = 1). Então melhora-se repetidamente o atual palpite y tomando-se a média de y

e x/y. Como um exemplo, as próximas três colunas indicam o palpite y, o quocientex/y, e suas médias para as primeiras aproximações para

p2.

1 2/1 = 2 1.51.5 2/1.5 = 1.3333 1.41671.4167 2/1.4167 = 1.4118 1.41421.4142 ... ...

y x/y (y +x/y)/2

Este algoritmo pode ser implementado em Scala por um conjunto de pequenasfunções, onde cada uma representa um dos elementos do algoritmo.

Primeiro definimos uma função para iterar do palpite para o resultado:

def sqrtIter(guess: Double, x: Double): Double =if (isGoodEnough(guess, x)) guess

16 Expressões e Funções Simples

else sqrtIter(improve(guess, x), x)

Observe que sqrtIter chama a si mesmo recursivamente. Laços em programasimperativos podem sempre ser modelados por recursão em programas funcionais.

Observe também que a definição de sqrtIter contém um tipo retorno, que segueo seção de parâmetros. Tais tipos de retorno são mandatórios para funçõesrecursivas. Para uma função não recursiva, o tipo de retorno é opcional; se estiverfaltando o verificador de tipos o computará a partir do tipo do lado direito dafunção. Entretanto, mesmo para funções não recursivas é sempre boa idéia incluirum tipo de retorno para melhor documentação.

Como segundo passo, definimos as duas funções chamadas por: sqrtIter: umafunção para melhorar (improve) o palpite e um teste de terminação isGoodEnough.Aqui estão suas definições.

def improve(guess: Double, x: Double) =(guess + x / guess) / 2

def isGoodEnough(guess: Double, x: Double) =abs(square(guess) - x) < 0.001

Finalmente, a própria função sqrt é definida como uma aplicação de sqrtIter.

def sqrt(x: Double) = sqrtIter(1.0, x)

Exercício 4.4.1 O teste isGoodEnough não é muito preciso para pequenos númerose podem levar a não terminação para números muito grandes (por que?). Crie umaversão diferente para isGoodEnough que não tenham esses problemas.

Exercício 4.4.2 Simule a execução da expressão sqrt(4).

4.5 Aninhamento de Funções

A programação funcional encoraja a construção de muitas pequenas funçõesauxiliares. Nos últimos exemplos, a implementação de sqrt faz uso da funçõesauxiliares sqrtIter, improve e isGoodEnough. Os nomes destas funções sãorelevantes somente para a implementação de sqrt. Normalmente não queremosque os usuários de sqrt acessem estas funções diretamente.

Nós podemos reforçar isto (e evitar poluição de nomes) incluindo funções auxiliaresdentro das próprias funções:

def sqrt(x: Double) = {def sqrtIter(guess: Double, x: Double): Double =if (isGoodEnough(guess, x)) guess

4.5 Aninhamento de Funções 17

else sqrtIter(improve(guess, x), x)def improve(guess: Double, x: Double) =(guess + x / guess) / 2

def isGoodEnough(guess: Double, x: Double) =abs(square(guess) - x) < 0.001

sqrtIter(1.0, x)}

Neste programa, as chaves { ... } envolvem um bloco. Blocos em Scala são elesmesmos expressões. Cada bloco termina com um expressão resultado o qual defineseu valor. A expressão resultado pode ser precedida por definições auxiliares, asquais são visíveis somente no próprio bloco.

Cada definição no bloco pode ser seguida para um ponto e vírgula, o qual separaesta definição das definições subsequentes ou a expressão resultado. Entretanto,um ponto e vírgula é inserido implicitamente ao final de cada linha, a não ser queuma das condições a seguir seja verdadeira.

1. Ou a linha em questão termina com uma palavra tal que um ponto ou umoperador infixo não são legais ao final da expressão.

2. Ou a próxima linha inicia com uma palavra que não pode iniciar umaexpressão.

3. Ou estamos dentro de parênteses (...) ou chaves , porque estes não podemconter multiplos comandos de qualquer modo.

Entretanto, os seguintes são legais:

def f(x: Int) = x + 1;f(1) + f(2)

def g1(x: Int) = x + 1g(1) + g(2)

def g2(x: Int) = {x + 1}; /* ‘;’ mandatorio */ g2(1) + g2(2)

def h1(x) =x +y

h1(1) * h1(2)

def h2(x: Int) = (x // parenteses mandatorio, senao um ponto e virgula+ y // sera inserido apos o ‘x’.

)h2(1) / h2(2)

18 Expressões e Funções Simples

Scala usa as regras usuais de escopo de bloco estruturado. Um nome definido emalgum outro bloco é também visível em algum bloco interno, desde que não tenhasido redefinido lá. Esta regra nos permite simplificar nosso exemplo sqrt. Nãoprecisamos passar x como parâmetro adicional de funções aninhadas, dado queestá sempre visível nelas como um parâmetro da função externa sqrt. Aqui está ocódigo simplificado:

def sqrt(x: Double) = {def sqrtIter(guess: Double): Double =if (isGoodEnough(guess)) guesselse sqrtIter(improve(guess))

def improve(guess: Double) =(guess + x / guess) / 2

def isGoodEnough(guess: Double) =abs(square(guess) - x) < 0.001

sqrtIter(1.0)}

4.6 Recursão de Cauda

Considere a seguinte função para calcular o maior divisor comum entre doisnúmeros dados.

def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)

Usando nosso modelo de substituição da função de avaliação, gcd(14, 21)

evaluates as follows:

gcd(14, 21)→ if (21 == 0) 14 else gcd(21, 14 % 21)→ if (false) 14 else gcd(21, 14 % 21)→ gcd(21, 14 % 21)→ gcd(21, 14)→ if (14 == 0) 21 else gcd(14, 21 % 14)→ → gcd(14, 21 % 14)→ gcd(14, 7)→ if (7 == 0) 14 else gcd(7, 14 % 7)→ → gcd(7, 14 % 7)→ gcd(7, 0)→ if (0 == 0) 7 else gcd(0, 7 % 0)→ → 7

Contraste isto com a avaliação de uma outra função recursiva, factorial:

def factorial(n: Int): Int = if (n == 0) 1 else n * factorial(n - 1)

4.6 Recursão de Cauda 19

A aplicação de factorial(5) é reescrita como segue:

factorial(5)→ if (5 == 0) 1 else 5 * factorial(5 - 1)→ 5 * factorial(5 - 1)→ 5 * factorial(4)→ . . . → 5 * (4 * factorial(3))→ . . . → 5 * (4 * (3 * factorial(2)))→ . . . → 5 * (4 * (3 * (2 * factorial(1))))→ . . . → 5 * (4 * (3 * (2 * (1 * factorial(0))))→ . . . → 5 * (4 * (3 * (2 * (1 * 1))))→ . . . → 120

Há uma importante diferença entre as duas sentenças reescritas: Os termos nasequência reescrita de gcd tem repetidamente a mesma forma. Conforme aavaliação prossegue, seu tamanho é limitado por uma constante. Diferentemente,na avaliação do fatorial obtemos cadeias cada vez mais longas de operandos quesão então multiplicados na última parte da sequência de avaliação.

Ainda que as atuais implementações de Scala não trabalhem reescrevendo termos,elas contudo devem ter o mesmo comportamento de espaço que nas sequênciasreescritas. Na implementação de gcd, nota-se que a chamada recursiva para gcd éa última ação realizada na avaliação do seu corpo. Pode-se também dizer que gcd éuma recursão de cauda (tail-recursive). A última chamada numa função recursivade cauda pode ser implementada por um salto ao início da função. Os argumentosdessa chamada pode sobrescrever os parâmetros da atual instanciação de gcd,portanto nenhum espaço na pilha é necessário. Consequentemente, funçõesrecursivas de cauda são processos iterativos, que podem ser executados em espaçoconstante.

Em contraste, a chamada recursiva em factorial é seguida por uma multiplicação.consequentemente, um novo espaço na pilha é alocado para a instância recursivade fatorial, e é desalocada após o término daquela instância. A formulação dadapara a função fatorial não é recursiva de cauda; precisa de espaço proporcional aosseus parâmetros de entrada para sua execução.

Genericamente, se a última ação da função é uma chamada a outra função(possivelmente a mesma), apenas um único espaço na pilha é requerido paraambas funções. Tais chamadas são denominadas “tail call”. Em princípio, tail callssempre podem reutilizar o espaço da pilha da função de chamada. Entretanto,alguns ambientes de execução (tais como Java VM) carecem das primitivas parafazer um espaço de pilha reutilizável para tail calls eficientes. A produção de umaimplementação Scala de qualidade é, portanto, requerida somente para reutilizaro espaço de pilha de uma função recursiva de cauda cuja última ação é chamara si mesma. Outras chamadas de cauda podem ser otimizadas também, mas nãodeve-se confiar nisso ao longo das implementações.

20 Expressões e Funções Simples

Exercício 4.6.1 Escreva uma implementação recursiva de cauda de factorial.

Capítulo 5

Funções de Primeira Classe

Uma função em Scala é um valor de primeira classe. Como qualquer outro valor,pode ser passado como um parâmetro ou retornado como um resultado. Funçõesque recebem outras funções como parâmetros ou as retornam como resultadossão denominadas funções de alta ordem. Este capítulo introduz funções de altaordem e mostra como elas fornecem um mecanismo flexível para a composição deprogramas.

Como um exemplo de motivação, considere as três tarefas relacionadas a seguir:

1. Escreva uma função que some todos os inteiros entre dois dados números, ae b:

def sumInts(a: Int, b: Int): Int =if (a > b) 0 else a + sumInts(a + 1, b)

2. Escreva uma função para somar os quadrados de todos os inteiros entre doisdados números, a e b:

def square(x: Int): Int = x * xdef sumSquares(a: Int, b: Int): Int =if (a > b) 0 else square(a) + sumSquares(a + 1, b)

3. Escreva uma função para somar as potências 2n de todos os n inteiros entredois dados números a e b:

def powerOfTwo(x: Int): Int = if (x == 0) 1 else 2 * powerOfTwo(x - 1)def sumPowersOfTwo(a: Int, b: Int): Int =if (a > b) 0 else powerOfTwo(a) + sumPowersOfTwo(a + 1, b)

Estas funções são todas instâncias de∑b

a f (n) para diferentes valores de x. Podemosobter o padrão comum definindo uma função sum:

22 Funções de Primeira Classe

def sum(f: Int => Int, a: Int, b: Int): Int =if (a > b) 0 else f(a) + sum(f, a + 1, b)

O tipo Int => Int é o tipo de funções que recebem argumentos do tipo Int eretornam resultados do tipo Int. Então sum é uma função que recebe uma outrafunção como parâmetro. Em outras palavras, sum é uma função de alta ordem.

Usando sum, podemos formular as três funções de soma como segue:

def sumInts(a: Int, b: Int): Int = sum(id, a, b)def sumSquares(a: Int, b: Int): Int = sum(square, a, b)def sumPowersOfTwo(a: Int, b: Int): Int = sum(powerOfTwo, a, b)

onde

def id(x: Int): Int = xdef square(x: Int): Int = x * xdef powerOfTwo(x: Int): Int = if (x == 0) 1 else 2 * powerOfTwo(x - 1)

5.1 Funções Anônimas

Parametrização por funções tende a criar várias pequenas funções. No exemploanterior, definimos id, square e power como funções separadas, de tal modo quepudessem ser passadas como argumentos para sum.

Ao invés de usarmos definições de funções nomeadas para estas pequenas funçõesargumentos, podemos formulá-las de um modo abreviado como funções anônimas.Uma função anônima é uma expressão que é avaliada para uma função; a funçãoé definida sem receber um nome. Como exemplo considere a função anônimaquadrado:

(x: Int) => x * x

A parte anterior a flecha ‘=>’ são os parâmetros da função, ao passo que a parteseguinte a ‘=>’ é o seu corpo. Por exemplo, aqui está uma função anônima quemultiplica seus dois argumentos.

(x: Int, y: Int) => x * y

Usando funções anônimas, podemos reformular as duas funções soma sem funçõesauxiliares nomeadas:

def sumInts(a: Int, b: Int): Int = sum((x: Int) => x, a, b)def sumSquares(a: Int, b: Int): Int = sum((x: Int) => x * x, a, b)

Frequentemente, o compilador Scala pode deduzir o tipo do parâmetro a partirdo contexto da função anônima nos casos em que foram omitidos. Por exemplo,

5.2 Currying 23

no caso de sumInts ou sumSquares, sabe-se a partir do tipo de sum que o primeiroparâmetro deve ser uma função de tipo Int => Int. Consequentemente, o tipo doparâmetro Int é redundante e pode ser omitido. Se houver um único parâmetrosem um tipo, também podemos omitir os parâmetros a sua volta.

def sumInts(a: Int, b: Int): Int = sum(x => x, a, b)def sumSquares(a: Int, b: Int): Int = sum(x => x * x, a, b)

Em geral, o termo em Scala (x1: T1, ..., xn: Tn) => E define uma função quemapeia seus parâmetros x1, ..., xn ao resultado da expressão E (onde E podereferir-se a x1, ..., xn). Funções anônimas não são elementos essenciais dalinguagem Scala, dado que sempre podem ser expressos em termos de funçõesnomeadas. De fato, a função anônima

(x1: T1, ..., xn: Tn) => E

é equivalente ao bloco

{ def f (x1: T1, ..., xn: Tn) = E ; f _ }

onde f é um novo nome que não é utilizado em qualquer outro lugar do programa.Também dizemos que funções anônimas são “syntactic sugar”.

5.2 Currying

A última formulação das funções de soma já são bem compactas. Mas podemosfazer ainda melhor. Observe que a e b aparecem como parâmetros e argumentosde cada função, mas não parecem tomar parte de combinações interessantes. Háalgum modo de nos livrarmos delas?

Vamos tentar reescrever sum de tal modo que não receba os limites a e b comoparâmetros:

def sum(f: Int => Int): (Int, Int) => Int = {def sumF(a: Int, b: Int): Int =if (a > b) 0 else f(a) + sumF(a + 1, b)

sumF}

Nessa formulação, sum é uma função que retorna uma outra função,especificamente a função especializada soma sumF. Esta última função realizatodo o trabalho; recebe os limites a e b como parâmetros, aplica a função f,parâmetro de sum, a todos os inteiros entre eles, e soma os resultados.

Usando esta nova formulação de sum, podemos agora definir:

def sumInts = sum(x => x)

24 Funções de Primeira Classe

def sumSquares = sum(x => x * x)def sumPowersOfTwo = sum(powerOfTwo)

Ou, equivalentemente, com definições de valores:

val sumInts = sum(x => x)val sumSquares = sum(x => x * x)val sumPowersOfTwo = sum(powerOfTwo)

sumInts, sumSquares, e sumPowersOfTwo podem ser aplicados como qualquer outrafunção. Por exemplo,

scala> sumSquares(1, 10) + sumPowersOfTwo(10, 20)unnamed0: Int = 2096513

Como funções que retornam funções são aplicadas? Como exemplo, na expressão

sum(x => x * x)(1, 10) ,

a função sum é aplicada a função quadrada (x => x * x). O função resultante éentão aplicada ao segunda lista de argumentos, (1, 10).

Essa notação é possível porque aplicações de funções são associativas à esquerda.Ou seja, se args1 e args2 são listas de argumentos, então

f (args1)(args2) é equivalente a ( f (args1))(args2)

Em nosso exemplo, sum(x => x * x)(1, 10) é equivalente a seguinte expressão:(sum(x => x * x))(1, 10).

O estilo “funções que retornam funções” é tão útil que Scala tem sintaxe especialpara ele. Por exemplo, a próxima definição de sum é equivalente a anterior, porémmais curta:

def sum(f: Int => Int)(a: Int, b: Int): Int =if (a > b) 0 else f(a) + sum(f)(a + 1, b)

Genericamente, uma definição de função currificada

def f (args1) ... (argsn) = E

onde n > 1 expande para

def f (args1) ... (argsn−1) = { def g (argsn) = E ; g }

onde g é um novo identificador. Ou, menor, usando uma função anônima:

def f (args1) ... (argsn−1) = ( argsn ) => E .

Realizando este passo n vezes dá aquela

5.3 Exemplo: Encontrando Pontos Fixos de Funções 25

def f (args1) ... (argsn) = E

é equivalente a

def f = (args1) => ... => (argsn) => E .

Ou, equivalentemente, usando uma definição valor:

val f = (args1) => ... => (argsn) => E .

Este estilo de definição de função e aplicação é chamado currificado depois que seudivulgador, Haskell B. Curry, um lógico do século 20, mesmo que a idéia nos leve devolta a Moses Schönfinkel e Gottlob Frege.

O tipo de uma função que retorna função é expresso de modo análogo a sua lista deparâmetros. Tomando a última formulação de sum como exemplo, o tipo para sum é(Int => Int) => (Int, Int) => Int. Isso é possível porque tipos de funções sãoassociativos à direita. Ou seja,

T1 => T2 => T3 is equivalent to T1 => (T2 => T3)

Exercício 5.2.1 1. A função sum usa uma recursiva linear. Você poderia alterá-lapara uma função recursiva de cauda, preenchendo as lacunas marcadas com ’??’abaixo?

def sum(f: Int => Int)(a: Int, b: Int): Int = {def iter(a: Int, result: Int): Int = {if (??) ??else iter(??, ??)

}iter(??, ??)

}

Exercício 5.2.2 Escreva uma função produto que computa o produto de valores defunções em pontos sobre um dado intervalo.

Exercício 5.2.3 Escreva fatorial em termos de produto.

Exercício 5.2.4 Escreva uma função ainda mais genérica que generaliza ambos sume produto.

5.3 Exemplo: Encontrando Pontos Fixos de Funções

Um número x é chamado um ponto fixo de uma função f se

26 Funções de Primeira Classe

f(x) = x .

Para algumas funções f podemos encontrar os pontos fixos iniciando com umpalpite inicial e então aplicando f repetidamente, até que o valor não mude mais(ou a mudança seja tolerável). Isso é possível na sequência

x, f(x), f(f(x)), f(f(f(x))), ...

converge para pontos fixos de f . Essa idéia é captada na “função achadora depontos fixos” a seguir:

val tolerance = 0.0001def isCloseEnough(x: Double, y: Double) = abs((x - y) / x) < tolerancedef fixedPoint(f: Double => Double)(firstGuess: Double) = {def iterate(guess: Double): Double = {val next = f(guess)if (isCloseEnough(guess, next)) nextelse iterate(next)

}iterate(firstGuess)

}

Agora aplicaremos esta idéia na reformulação da função raiz quadrada. Vamoscomeçar pela especificação de sqrt:

sqrt(x) = the y such that y * y = x= the y such that y = x / y

Consequentemente, sqrt(x) é um ponto fixo da função y => x / y. Isso sugereque sqrt(x) pode ser computado por uma iteração de ponto fixo:

def sqrt(x: double) = fixedPoint(y => x / y)(1.0)

Mas se tentarmos isso, percebemos que aquela computação não converge. Vamosagregar à função de ponto fixo um comando print que mostra o valor corrente deguess:

def fixedPoint(f: Double => Double)(firstGuess: Double) = {def iterate(guess: Double): Double = {val next = f(guess)println(next)if (isCloseEnough(guess, next)) nextelse iterate(next)

}iterate(firstGuess)

}

5.3 Exemplo: Encontrando Pontos Fixos de Funções 27

Então, sqrt(2) dá:

2.01.02.01.02.0...

Um modo de controlar tais oscilações é prevenir guess de mudar muito. Issopode ser obtido pela média (função averaging) de sucessivos valores da sequênciaoriginal:

scala> def sqrt(x: Double) = fixedPoint(y => (y + x/y) / 2)(1.0)sqrt: (Double)Double

scala> sqrt(2.0)1.51.41666666666666651.41421568627450971.41421356237468991.4142135623746899

De fato, expandindo a função fixedPoint dá exatamente nossa prévia definição deponto fixo da Seção 4.4.

Os exemplos anteriores mostraram que o poder de expressividade de umalinguagem é consideravelmente aumentado se funções puderem ser passadascomo argumentos. O próximo exemplo mostra que funções que retornam funçõestambém podem ser muito úteis.

Considere novamente iterações de ponto fixo. Começamos observando quep

(x) éum ponto fixo da função y => x / y. Então fazemos a iteração convergir tirando amédia de sucessivos valores. Essa técnica de amortecimento da média é tão genéricaque pode ser denotada em outra função.

def averageDamp(f: Double => Double)(x: Double) = (x + f(x)) / 2

Usando averageDamp, podemos reformular a função da raiz quadrada como segue.

def sqrt(x: Double) = fixedPoint(averageDamp(y => x/y))(1.0)

Isto expressa os elementos do algoritmo tão claramente quanto possível.

Exercício 5.3.1 Escreva uma função para a raiz cúbica usando fixedPoint eaverageDamp.

28 Funções de Primeira Classe

5.4 Sumário

Vimos no capítulo anterior que funções são abstrações essenciais porquenos permitem introduzir métodos genéricos de computação como explícitos,elementos nomeados em nossa linguagem de programação. O presente capítulomostrou que tais abstrações podem ser combinados por funções de alta ordem paracriar mais abstrações. Como programadores, devemos procurar por oportunidadesde reuso e abstração. O mais alto nível possível de abstração nem sempre é omelhor, mas é importante saber técnicas de abstração, de modo que possamosusá-las quando apropriado.

5.5 Elementos da Linguagem Vistos Até Aqui

Os capítulos 4 e 5 trataram elementos da linguagem Scala para expressar tipos eexpressões relacionados a tipos primitivos e funções. A sintaxe livre de contextodesses elementos da linguagem são dados abaixo em formato Backus-Naurestendido, onde ‘|’ denotam alternativas, [...] denotam opção (0 ou 1 ocorrência),e {...} denotam (0 ou mais ocorrências).

Caracteres

Programas em Scala são sequência de caracteres (Unicode). Distinguimos osseguintes conjuntos de caracteres.

• espaço em branco, tal como ‘’, tabulação, ou caracter de mudança de linha(newline),

• letras ‘a’ a ‘z’, ‘A’ a ‘Z’,

• digitos ‘0’ a ‘9’,

• caracteres delimitadores

. , ; ( ) { } [ ] \ " ’

• caracteres operadores, tal como ‘#’, ‘+’ e ‘:’. Essencialmente, há caracteresimprimíveis que não estão em nenhum dos conjuntos de caracteres acima.

Lexemas:

ident = letter {letter | digit}| operator { operator }| ident ’_’ ident

literal = “como em Java”

5.5 Elementos da Linguagem Vistos Até Aqui 29

Literais são como em Java. Eles definem números, caracteres, cadeias decaracteres, ou valores boleanos. Exemplos de literais tais como 0, 1.0e10, ’x’,"ele disse "oi!"", ou true.

Identificadores podem ter duas formas. Eles ou iniciam por uma letra, que é seguidapor uma sequência (possivelmente vazia) de letras ou símbolos, ou podem iniciarcom um caracter operador, seguido por uma sequência (possivelmente vazia) decaracteres operadores. Ambas as formas podem conter caracteres “underscore” ‘_’.Além disso, um caracter underscore pode ser seguido por quaisquer identificadores.Consequentemente, todos a seguir são identificadores legais:

x Room10a + -- foldl_: +_vector

Segue desta regra que identificadores operadores subsequentes precisam serseparados por espaço em branco. Por exemplo, a entrada x+-y é entendida comoa sequência de três “tokens” x, +- e y. Se desejamos expressar a soma de x com ovalor negativo de y, precisamos acrescentar no mínimo um espaço, ou seja, x+ -y.

O caracter $ é reservado para identificadores gerados pelo compilador; não devemser usados em programas fontes.

Os seguintes são palavras reservadas, e não devem ser usadas como identificadores:

abstract case catch class defdo else extends false finalfinally for if implicit importmatch new null object overridepackage private protected requires returnsealed super this throw traittry true type val varwhile with yield_ : = => <- <: <% >: # @

Types:

Type = SimpleType | FunctionTypeFunctionType = SimpleType ’=>’ Type | ’(’ [Types] ’)’ ’=>’ TypeSimpleType = Byte | Short | Char | Int | Long | Float | Double |

Boolean | Unit | StringTypes = Type {‘,’ Type}

Tipos podem ser:

• tipos numéricos Byte, Short, Char, Int, Long, Float and Double (com emJava),

• o tipo Boolean com valores true e false,

• o tipo Unit com o único valor (),

30 Funções de Primeira Classe

• o tipo String,

• tipos função, tal como (Int, Int) => Int ou String => Int => String.

Expressões:

Expr = InfixExpr | FunctionExpr | if ’(’ Expr ’)’ Expr else ExprInfixExpr = PrefixExpr | InfixExpr Operator InfixExprOperator = identPrefixExpr = [’+’ | ’-’ | ’!’ | ’~’ ] SimpleExprSimpleExpr = ident | literal | SimpleExpr ’.’ ident | BlockFunctionExpr = (Bindings | Id) ’=>’ ExprBindings = ‘(’ Binding {‘,’ Binding} ‘)’Binding = ident [’:’ Type]Block = ’{’ {Def ’;’} Expr ’}’

Expressões podem ser:

• identificadores tais como x, isGoodEnough, *, ou +-,

• literais, tais como 0, 1.0, ou "abc",

• Seleções de campo e método, tal como System.out.println,

• aplicações de função, tal como sqrt(x),

• aplicações de operador, tais como -x ou y + x,

• condicionais, tal como if (x < 0) -x else x,

• blocos, tal como { val x = abs(y) ; x * 2 },

• funções anônimas, tais como x => x + 1 ou (x: Int, y: Int) => x + y.

Definições:

Def = FunDef | ValDefFunDef = ’def’ ident {’(’ [Parameters] ’)’} [’:’ Type] ’=’ ExprValDef = ’val’ ident [’:’ Type] ’=’ ExprParameters = Parameter {’,’ Parameter}Parameter = ident ’:’ [’=>’] Type

Definições podem ser:

• definições de função tal como def square(x: Int): Int = x * x,

• definições de valor tal como val y = square(2).

Capítulo 6

Classes e Objetos

Scala não tem um tipo primitivo para números racionais, mas é fácil definir um,usando uma classe. Aqui está uma possível implementação.

class Rational(n: Int, d: Int) {private def gcd(x: Int, y: Int): Int = {if (x == 0) yelse if (x < 0) gcd(-x, y)else if (y < 0) -gcd(x, -y)else gcd(y % x, x)

}private val g = gcd(n, d)

val numer: Int = n/gval denom: Int = d/gdef +(that: Rational) =new Rational(numer * that.denom + that.numer * denom,

denom * that.denom)def -(that: Rational) =new Rational(numer * that.denom - that.numer * denom,

denom * that.denom)def *(that: Rational) =new Rational(numer * that.numer, denom * that.denom)

def /(that: Rational) =new Rational(numer * that.denom, denom * that.numer)

}

Isso define Rational como uma classe que recebe dois argumentos construtores ne d, contendo as partes numéricas do numerador e denominador. A classe provêcampos que retornam estas partes, bem como métodos para a aritmética sobrenúmeros racionais. Cada método aritmético recebe como parâmetro o operandodireito da operação. O operando esquerdo da operação sempre é o número racional

32 Classes e Objetos

do método cujo é membro.

Membros Privados. A implementação dos números racionais define um métodoprivado gcd que computa o maior denominador comum de dois inteiros, bemcomo um campo privado g que contém o gcd dos argumentos construtores.Esses membros são inacessíveis de fora da classe Rational. São usadosna implementação da classe para eliminar fatores comuns nos argumentosconstrutores para garantir que o numerador e o denominador estejam sempre emforma normalizada.

Criando e Acessando Objetos. Como um exemplo de como os números racionaispodem ser usados, aqui está um programa que imprime a soma de todos osnúmeros 1/i onde i está no intervalo de 1 a 10.

var i = 1var x = new Rational(0, 1)while (i <= 10) {x += new Rational(1, i)i += 1

}println("" + x.numer + "/" + x.denom)

O + recebe como operando esquerdo uma cadeia de caracteres e como operandodireito um valor de tipo arbitrário. Retorna o resultado de converter seu operandodireito em uma cadeia de caracteres e concatená-la a seu operando esquerdo.

Herança e Sobrecarga. Cada classe em Scala tem uma superclasse que elaestende. Se uma classe não menciona sua superclasse em sua definição, o tipobásico scala.AnyRef é implicitamente assumido (para implementações Java, estetipo é um apelido (alias) para java.lang.Object. Por exemplo, a classe Rational

pode ser equivalentemente definida como

class Rational(n: Int, d: Int) extends AnyRef {... // como antes

}

Uma classe herda todos os membros de sua superclasse. Pode tambémredefinir (ou: sobrescrever) alguns membros herdados. Por exemplo, a classejava.lang.Object define um método toString que retorna uma representação doobjeto como cadeia de caracteres (string):

class Object {...def toString: String = ...

}

33

A implementação de toString em Object forma uma string que consiste do nomeda classe do objeto e um número. Faz sentido redefinir este método para objetosque são números racionais:

class Rational(n: Int, d: Int) extends AnyRef {... // como antesoverride def toString = "" + numer + "/" + denom

}

Observe que, diferentemente de Java, definições que redefinem precisam serprecedidas por um modificador override.

Se a classe A estende a classe B , então objetos do tipo A podem ser usados sempreque objetos do tipo B são esperados. Nesse caso dizemos que o tipo A está emconformidade com o tipo B . Por exemplo, Rational está em conformidade comAnyRef, logo é legal atribuir um valor Rational à variável do tipo AnyRef:

var x: AnyRef = new Rational(1, 2)

val r = new Rational(1,2)System.out.println(r.toString); // imprime ‘‘1/2’’

Distintamente de Java, métodos em Scala não necessariamente precisam receberuma lista de parâmetros. Um exemplo é o método abaixo square. Este método éinvocado simplesmente mencionando seu nome.

class Rational(n: Int, d: Int) extends AnyRef {... // como antesdef square = new Rational(numer*numer, denom*denom)

}val r = new Rational(3, 4)println(r.square) // imprime ‘‘9/16’’*

Ou seja, métodos sem parâmetros são acessados como campos valor, tais comonumer são. A diferença entre valores e métodos sem parâmetros reside em suasdefinições. O lado direito de um valor é avaliado quando o objeto é criado, e o valornão muda depois. Um lado direito de um método sem parâmetros, por outro lado,é avaliado cada vez que o método é chamado. O acesso uniforme dos campos demétodos sem parâmetros resulta em crescente flexibilidade para o implementadorde uma classe. Frequentemente, um campo em uma versão da classe torna-se umvalor computado na próxima versão. O acesso uniforme garante que clientes nãotenham de ser reescritos por conta desta mudança.

34 Classes e Objetos

Classes Abstratas. Considere a tarefa de escrever uma classe para conjuntos denúmeros inteiros com duas operações, incl e contains. (s incl x) deve retornarum novo conjunto que contém o elemento x juntamente com todos os elementosdo conjunto s. (s contains x) deve retornar true se o conjunto s contiver oelemento x, e deve retornar false, caso contrário. A interface para tais conjuntos édada por:

abstract class IntSet {def incl(x: Int): IntSetdef contains(x: Int): Boolean

}

IntSet é tido como uma classe abstrata. Isso tem duas consequências. Primeiro,classes abstratas podem ter membros protelados (deferred) que são declarados,mas que não tem uma implementação. No nosso caso, ambos incl e contains

são tais membros. Segundo, porque uma classe abstrata pode ter membros nãoimplementados, nenhum objeto daquela classe pode ser criado usando new. Emcontraste, uma classe abstrata pode ser usada como uma classe base de algumaoutra classe, que implementa os membros que foram postergados.

Traits. Ao invés de classe abstrata frequentemente usa-se a palavra chavetrait em Scala. Traits são classes abstratas que desejamos adicionar a algumaoutra classe. Isso pode ser porque um trait adiciona alguns métodos ou campospara uma classe pai desconhecida. Por exemplo, um trait Bordered pode ser usadopara adicionar uma borda a vários componentes gráficos. Um outro cenário deuso ocorre quando o trait coleta assinaturas de alguma funcionalidade provida pordiferentes classes, do mesmo modo que uma interface Java faria.

Como IntSet pertence a esta categoria, pode-se alternativamente definí-la comoum trait:

trait IntSet {def incl(x: Int): IntSetdef contains(x: Int): Boolean

}

Implementando Classes Abstratas. Vamos dizer que planejamos implementarconjuntos como árvores binárias. Há duas possíveis formas de árvores. Uma árvorepara o conjunto vazio, e uma árvore consistindo de um inteiro e duas subárvores.Aqui estão suas implementações:

class EmptySet extends IntSet {def contains(x: Int): Boolean = falsedef incl(x: Int): IntSet = new NonEmptySet(x, new EmptySet, new EmptySet)

}

35

class NonEmptySet(elem: Int, left: IntSet, right: IntSet) extends IntSet {def contains(x: Int): Boolean =if (x < elem) left contains xelse if (x > elem) right contains xelse true

def incl(x: Int): IntSet =if (x < elem) new NonEmptySet(elem, left incl x, right)else if (x > elem) new NonEmptySet(elem, left, right incl x)else this

}

Ambos EmptySet e NonEmptySet estendem a classe IntSet. Isso implica que tiposEmptySet e NonEmptySet estão em conformidade com o tipo IntSet – um valor dotipo EmptySet ou NonEmptySet pode ser usado sempre que um valor do tipo IntSet

é requerido.

Exercício 6.0.1 Escreva os métodos uniao e intersecao para formar a união einterseção entre dois conjuntos.

Exercício 6.0.2 Adicione um método

def excl(x: Int)

para retornar um dado conjunto sem o elemento x. Para isso, é interessantetambém implementar um método teste

def isEmpty: Boolean

para conjuntos.

Ligação Dinâmica. Linguagens orientadas a objetos (inclusive Scala) usamdespacho dinâmico para invocações de métodos. Ou seja, o código invocado poruma chamada de método depende do tipo em tempo de execução do objeto quecontém o método. Por exemplo, considere a expressão s contains 7 onde s éum valor do tipo declarado s: IntSet. Qual código para contains é executadodepende do tipo do valor de s em tempo de execução. Se for um valor EmptySet, éa implementação de contains na classe EmptySet que é executada, e analogamentepara valores NonEmptySet. Este comportamento é consequência direta do nossomodelo de substituição da avaliação. Por exemplo,

(new EmptySet).contains(7)

-> (substituindo contains pelo seu corpo na classe EmptySet)

36 Classes e Objetos

false

Ou,

new NonEmptySet(7, new EmptySet, new EmptySet).contains(1)

-> (substituindo contains pelo seu corpo na classe NonEmptySet)

if (1 < 7) new EmptySet contains 1else if (1 > 7) new EmptySet contains 1else true

-> (reescrevendo o condicional)

new EmptySet contains 1

-> (substituindo contains pelo seu corpo na classe EmptySet)

false .

Envio dinâmico de métodos é análogo a chamadas para funções de alta ordem. Emambos os casos, a identidade do código a ser executado é conhecida somente emtempo de execução. Essa similaridade não é apenas superficial. Na verdade, Scalarepresenta cada valor de função como um objeto (veja Seção 8.6).

Objetos. Na implementação prévia de conjuntos de inteiros, conjuntos vazioseram expressos com new EmptySet; então um novo objeto era criado a cada vez queum valor conjunto vazio era requerido. Podemos evitar criações desnecessárias deobjetos definindo um valor empty uma vez e então, usando este valor ao invés decada ocorrência de new EmptySet. Por exemplo:

val EmptySetVal = new EmptySet

Um problema com esse tratamento é que uma definição de valor tal como a anteriornão é uma definição top-level legal em Scala; tem de ser parte de uma outra classeou objeto. Ainda, a definição de classe EmptySet agora parece um pouco exagerada– por que definir uma classe de objetos, se somente estamos interessados em umúnico objeto dessa classe? Um tratamento mais direto é usar uma definição deobjeto. Aqui está uma alternativa mais adequada para a definição de conjunto vazio:

object EmptySet extends IntSet {def contains(x: Int): Boolean = falsedef incl(x: Int): IntSet = new NonEmptySet(x, EmptySet, EmptySet)

}

37

A sintaxe de uma definição de objeto segue a sintaxe da definição de uma classe;tem uma cláusula opcional extends, bem como um corpo opcional. Assim comoem classes, a cláusula extends define membros herdados do objeto considerandoque o corpo define novos membros ou sobrecarga. Entretanto, uma definição deobjeto denota um único objeto somente se não for possível criar outros objetos coma mesma estrutura usando new. Então, definições de objeto também carecem deparâmetros construtores, que podem estar presentes nas definições das classes.

Definições de objetos podem aparecer em qualquer lugar em um programa Scala;incluindo o top-level. Como não há ordem fixa de execução de entidades top-levelem Scala, pode-se questionar quando exatamente o objeto definido pela definiçãode objeto é criado e inicializado. A resposta é que o objeto é criado assim que seusmembros são acessados. Esta estratégia é chamada avaliação preguiçosa.

Classes Padrão. Scala é uma linguagem orientada a objetos pura. Isso significaque cada valor em Scala pode ser visto como um objeto. De fato, mesmo tiposprimitivos tais como int ou boolean não são tratados de modo especial. Sãodefinidos como apelidos de tipos de classes Scala no módulo Predef.

type boolean = scala.Booleantype int = scala.Inttype long = scala.Long...

Por eficiência, o compilador geralmente representa valor de tipo scala.Int porinteiros de 32 bits, valores de tipo scala.Boolean por boleanos Java etc. Masconverte essas representações especializadas para objetos quando requeridos,por exemplo, quando um valor primitivo Int é passada a uma função comum parâmetro de tipo AnyRef. Consequentemente, a representação de valoresprimitivos é apenas uma otimização, não muda o significado de um programa.

Aqui está a especificação da classe Boolean.

package scalaabstract class Boolean {def && (x: => Boolean): Booleandef || (x: => Boolean): Booleandef ! : Boolean

def == (x: Boolean) : Booleandef != (x: Boolean) : Booleandef < (x: Boolean) : Booleandef > (x: Boolean) : Booleandef <= (x: Boolean) : Booleandef >= (x: Boolean) : Boolean

}

38 Classes e Objetos

Boleanos podem ser definidos usando somente classes e objetos, sem referência aotipos básicos para boleanos ou números. Uma possível implementação da claroBoolean é dada abaixo. Esta não é a implementação atual na biblioteca padrãoScala. Por razões de eficiência a implementação padrão usa boleanos primitivos.

package scalaabstract class Boolean {def ifThenElse(thenpart: => Boolean, elsepart: => Boolean)

def && (x: => Boolean): Boolean = ifThenElse(x, false)def || (x: => Boolean): Boolean = ifThenElse(true, x)def ! : Boolean = ifThenElse(false, true)

def == (x: Boolean) : Boolean = ifThenElse(x, x.!)def != (x: Boolean) : Boolean = ifThenElse(x.!, x)def < (x: Boolean) : Boolean = ifThenElse(false, x)def > (x: Boolean) : Boolean = ifThenElse(x.!, false)def <= (x: Boolean) : Boolean = ifThenElse(x, true)def >= (x: Boolean) : Boolean = ifThenElse(true, x.!)

}case object True extends Boolean {def ifThenElse(t: => Boolean, e: => Boolean) = t

}case object False extends Boolean {def ifThenElse(t: => Boolean, e: => Boolean) = e

}

Aqui está uma especificação parcial da classe Int.

package scalaabstract class Int extends AnyVal {def toLong: Longdef toFloat: Floatdef toDouble: Double

def + (that: Double): Doubledef + (that: Float): Floatdef + (that: Long): Longdef + (that: Int): Int // analogo para -, *, /, %

def << (cnt: Int): Int // analogo para >>, >>>

def & (that: Long): Longdef & (that: Int): Int // analogo para |, ^

def == (that: Double): Booleandef == (that: Float): Boolean

39

def == (that: Long): Boolean // analogo para !=, <, >, <=, >=}

A classe Int pode em princípio também ser implementada usando apenas objetose classes, sem referência a um tipo construído para inteiros. Para ver como,vamos considerar um problema um pouco mais simples, especificamente comoimplementar um tipo Nat de números naturais, ou seja, inteiros não negativo. Aquiestá uma definição para uma classe abstrata Nat:

abstract class Nat {def isZero: Booleandef predecessor: Natdef successor: Natdef + (that: Nat): Natdef - (that: Nat): Nat

}

Para implementar as operações da classe Nat, nós definimos um sub-objeto Zero

e uma subclasse Succ (para sucessor). Cada número N é representado como N

aplicações do construtor Succ até zero:

new Succ( ... new Succ︸ ︷︷ ︸N times

(Zero) ... )

A implementação do objeto Zero é direta:

object Zero extends Nat {def isZero: Boolean = truedef predecessor: Nat = error("negative number")def successor: Nat = new Succ(Zero)def + (that: Nat): Nat = thatdef - (that: Nat): Nat = if (that.isZero) Zero

else error("negative number")}

A implementação das funções predecessor e subtração sobre Zero leva a um Erro,que aborta o programa com uma mensagem de erro.

Aqui está uma implementação da classe sucessor:

class Succ(x: Nat) extends Nat {def isZero: Boolean = falsedef predecessor: Nat = xdef successor: Nat = new Succ(this)def + (that: Nat): Nat = x + that.successordef - (that: Nat): Nat = if (that.isZero) this

else x - that.predecessor}

40 Classes e Objetos

Observe a implementação do método successor. Para criar o sucessor de umnúmero, precisamos passar o próprio objeto como um argumento para o construtorSucc. O próprio objeto é referenciado por uma palavra reservada this.

As implementações de + e -, cada uma contém uma chamada recursiva com oargumento do costrutor como destinatário. A recursão terminará assim que odestinatário for o objeto Zero (o que é garantido de acontecer eventualmente dadoo modo com que os números são formados).

Exercício 6.0.3 Escreva uma implementação Integer dos números inteiros. Aimplementação deve suportar todas as operações da classe Nat e mais doismétodos.

def isPositive: Booleandef negate: Integer

O primeiro método deve retornar true se o número for positivo. O segundo métododeve negativar o número. Não utilize qualquer classe numérica padrão Scala emsua implementação. (Dica: Há duas formas para implementar Integer. Uma podeou fazer uso da implementação existente de Nat, representando um inteiro comoum número natural e um sinal. Ou pode-se generalizar a implementação dadade Nat para Integer, usando as três subclasses Zero para 0, Succ para númerospositivos e Pred para números negativos.

Elementos da Linguagem Introduzidos Neste Capítulo

Types:

Type = ... | ident

Tipos podem agora ser identificadores arbitrários que representam classes.

Expressões:

Expr = ... | Expr ’.’ ident | ’new’ Expr | ’this’

Uma expressão pode agora ser uma criação de objeto, ou uma seleção E.m de ummembro m a partir de uma expressão valorada objeto E, ou pode ser a palavrareservada this.

Definições e Declarações

Def = FunDef | ValDef | ClassDef | TraitDef | ObjectDefClassDef = [’abstract’] ’class’ ident [’(’ [Parameters] ’)’]

[’extends’ Expr] [‘{’ {TemplateDef} ‘}’]TraitDef = ’trait’ ident [’extends’ Expr] [’{’ {TemplateDef} ’}’]ObjectDef = ’object’ ident [’extends’ Expr] [’{’ {ObjectDef} ’}’]

41

TemplateDef = [Modifier] (Def | Dcl)ObjectDef = [Modifier] DefModifier = ’private’ | ’override’Dcl = FunDcl | ValDclFunDcl = ’def’ ident {’(’ [Parameters] ’)’} ’:’ TypeValDcl = ’val’ ident ’:’ Type

Uma definição pode agora ser uma definição de classe, trait ou objeto, tal como

class C(params) extends B { defs }trait T extends B { defs }object O extends B { defs }

As definições defs na classe, trait ou objeto podem ser precedidas pelosmodificadores private ou override.

Classes abstratas e traits podem também conter declarações. Isso introduz funçõesdeferred (postergadas) ou valores com seus tipos, mas não dão uma implementação.Membros deferred devem ser implementados nas subclasses antes que objetos deuma classe abstrata ou trait sejam criados.

Capítulo 7

Classes Case e Casamento dePadrões

Digamos, queremos escrever um interpretador para expressões aritméticas. Paradeixar as coisas simples inicialmente, vamos nos restringir somente a números eoperações +. Tais expressões podem ser representadas como hierarquia de classes,com uma classe abstrata base Expr como raiz, e duas subclasses Number e Sum.Então, uma expressão 1 + (3 + 7) é representada como

new Sum(new Number(1), new Sum(new Number(3), new Number(7)))

Agora, um avaliador de uma expressão como esta precisa saber de qual forma elaé (ou Sum ou Number) e também precisa acessar os componentes da expressão. Aimplementação a seguir provê todos os métodos necessários.

abstract class Expr {def isNumber: Booleandef isSum: Booleandef numValue: Intdef leftOp: Exprdef rightOp: Expr

}class Number(n: Int) extends Expr {def isNumber: Boolean = truedef isSum: Boolean = falsedef numValue: Int = ndef leftOp: Expr = error("Number.leftOp")def rightOp: Expr = error("Number.rightOp")

}class Sum(e1: Expr, e2: Expr) extends Expr {def isNumber: Boolean = false

44 Classes Case e Casamento de Padrões

def isSum: Boolean = truedef numValue: Int = error("Sum.numValue")def leftOp: Expr = e1def rightOp: Expr = e2

}

Com esta classificação e métodos de acesso, escrever uma função avaliador ésimples:

def eval(e: Expr): Int = {if (e.isNumber) e.numValueelse if (e.isSum) eval(e.leftOp) + eval(e.rightOp)else error("unrecognized expression kind")

}

Entretanto, definir todos esses métodos dentro das classes Sum e Number é um tantoquanto tedioso. Além do mais, o problema fica ainda pior se desejarmos adicionarnovas formas de expressões. Por exemplo, considere adicionar uma nova formade expressão Prod para produtos. Não apenas teremos de implementar uma novaclasse Prod, com todos os métodos de acesso e classificação prévios; tambémteremos de introduzir um novo método abstrato isProduct dentro da classe Expr

e implementar aquele método na subclasse Number, Sum, e Prod. Ter de modificarcódigo existente quando um sistema cresce é sempre problemático, pois introduzproblemas de versão e manutenção.

A promessa da programação orientada a objetos é que tais modificações sãodesnecessárias, dado que podem ser evitadas pela reutilização de código existentee não modificado, através da herança. De fato, uma decomposição mais orientadaa objetos para nosso problema resolve a questão. A idéia é tornar a operação de altonível eval um método para cada classe expressão, ao invés de implementá-lo comouma função fora da hierarquia de classes expressões, como fizemos anteriormente.Como eval é agora um membro de todos os nós de expressão, quaisquer métodosde classificação e acesso tornam-se supérfluos, e a implementação é simplificadaconsideravelmente:

abstract class Expr {def eval: Int

}class Number(n: Int) extends Expr {def eval: Int = n

}class Sum(e1: Expr, e2: Expr) extends Expr {def eval: Int = e1.eval + e2.eval

}

45

Além disso, adicionar uma nova classe Prod não leva a qualquer mudança ao códigoexistente:

class Prod(e1: Expr, e2: Expr) extends Expr {def eval: Int = e1.eval * e2.eval

}

A conclusão que tiramos deste exemplo é que a decomposição orientada a objetosé a técnica de escolha para a construção de sistemas que devem ser estensíveiscom novos tipos de dados. Mas há também um outro possível modo paraestender a expressão exemplo. Podemos querer adicionar novas operações sobreexpressões. Por exemplo, podemos querer adicionar uma operação que imprimeuma árvore-expressão para a saída padrão.

Se definimos todos os métodos de classificação e acesso, tal operação pode serfacilmente escrita como uma função externa. Aqui está um exemplo:

def print(e: Expr) {if (e.isNumber) Console.print(e.numValue)else if (e.isSum) {Console.print("(")print(e.leftOp)Console.print("+")print(e.rightOp)Console.print(")")

} else error("unrecognized expression kind")}

Entretanto, se optamos por uma decomposição orientada a objetos das expressões,precisaremos adicionar um novo procedimento print para cada classe:

abstract class Expr {def eval: Intdef print

}class Number(n: Int) extends Expr {def eval: Int = ndef print { Console.print(n) }

}class Sum(e1: Expr, e2: Expr) extends Expr {def eval: Int = e1.eval + e2.evaldef print {Console.print("(")print(e1)Console.print("+")print(e2)Console.print(")")

46 Classes Case e Casamento de Padrões

}}

Consequentemente, decomposição orientada a objetos clássica requer modificaçãode todas as classes existentes quando um sistema for estendido com novasoperações.

Ainda como uma outra forma, podemos querer estender o interpretador. Considerea simplificação de expressões. Por exemplo, podemos querer criar uma função quereescreve expressões da forma a * b + a * c para a * (b + c). Esta operaçãorequer inspeção de mais de um nó para a árvore de expressões ao mesmo tempo.Consequentemente, não pode ser implementada por um método dentro de cadatipo de expressão, a não ser que tal método também possa inspecionar outros nós.Portanto somos forçados a ter métodos de acesso e classificação neste caso. Istoparece nos levar para a casa inicial, com todos os problemas de estensibilidade eexpressividade.

Olhando mais de perto, observa-se que o único propósito das funções de acessoe classificação é reverter o processo de construção de dados. Elas nos permitemdeterminar, primeiro, qual subclasse de uma classe base abstrata foi usada e,segundo, quais foram os argumentos construtores. Como essa situação é bemcomum, Scala tem um modo de automatizá-lo para classes case.

7.1 Classes Case e Objetos Case

Classes Case e objetos case são definidos como classes e objetos normais, exceto quea definição é prefixada com um modificador case. Por exemplo, as definições:

abstract class Exprcase class Number(n: Int) extends Exprcase class Sum(e1: Expr, e2: Expr) extends Expr

introduzem Number e Sum como classes case. O modificador case em frente a umadefinição de classe ou objeto tem os seguintes efeitos.

1. Classes case implicitamente vem com uma função construtora, com o mesmonome da classe. No nosso exemplo, as duas funções

def Number(n: Int) = new Number(n)def Sum(e1: Expr, e2: Expr) = new Sum(e1, e2)

serão adicionadas. Consequentemente, pode-se agora construirárvores-expressões de modo um pouco mais conciso, como em

Sum(Sum(Number(1), Number(2)), Number(3))

7.1 Classes Case e Objetos Case 47

2. Classes case e objetos case implicitamente vem com implementações dosmétodos toString, equals e hashCode, que sobrescrevem os métodos como mesmo nome na classe AnyRef. A implementação desses métodos levaem consideração em cada caso a estrutura de um membro de uma classecase. O método toString representa uma árvore expressão do modo comofoi construída. Então,

Sum(Sum(Number(1), Number(2)), Number(3))

será convertida exatamente naquela cadeia de caracteres, onde aimplementação default dentro da classe AnyRef retornará uma cadeiade caracteres consistindo construtor Sum mais externo e um número. Ométodo equals trata dois membros case da classe case do mesmo modo, casoeles tenham sido construídos com o mesmo construtor e com argumentosque são par a par iguais. Isso também afeta a implementação de == e !=, quesão implementados em termos de equals em Scala. Então,

Sum(Number(1), Number(2)) == Sum(Number(1), Number(2))

dará true. Se Sum ou Number não fossem classes case, a mesma expressãoseria false, pois a implementação padrão de equals na classe AnyRef sempretrata objetos criados por diferentes chamadas de construtores como sendodiferentes. O método hashCode segue a mesmo princípio dos outros doismétodos. Computa um código hash a partir do nome do construtor da classecase e os códigos hash dos argumentos do construtor, ao invés de o fazera partir do endereço do objeto, que é o que a implementação default dehashCode faz.

3. Classes case implicitamente vem com métodos de acesso nulos querecuperam os argumentos do construtor. Em nosso exemplo, Number obteriaum método de acesso

def n: Int

que retorna o parâmetro n do construtor, onde Sum obterá dois métodos deacesso

def e1: Expr, e2: Expr

Consequentemente, para um valor s de tipo Sum, digamos, podemos escrevers.e1 para acessar o operando esquerdo. Entretanto, para um valor e de tipoExpr, o termo e.e1 será ilegal, pois e1 é definido em Sum; não é um membroda classe base Expr. Então, como determinar o construtor e os argumentosdo construtor de acesso para valores cujo tipo estático é a classe base Expr?

48 Classes Case e Casamento de Padrões

Isso é resolvido pela quarta e última particularidade das classes case.

4. Classes case permitem construções de padrões que se referem ao construtorda classe case.

7.2 Casamento de Padrões

O casamento de padrões é uma generalização do comando switchdo C ou Java parahierarquias de classes. Ao invés de um comando switch, há um método padrãomatch, que é definido na classe raíz Scala Any, e portanto está disponível para todosos objetos. O método match recebe como argumento um número de cases. Porexemplo, aqui está uma implementação de eval usando casamento de padrões.

def eval(e: Expr): Int = e match {case Number(n) => ncase Sum(l, r) => eval(l) + eval(r)

}

Neste exemplo, há dois cases. Cada case associa um padrão a uma expressão.Padrões são casados contra o valor do seletor e. O primeiro padrão do nossoexemplo, Number(n), casa todos os valores da forma Number(v), onde v é um valorarbitrário. Naquele caso, a variável padrão n é ligada ao valor v. Similarmente, opadrão Sum(l, r) casa com todos os valores do seletor da forma Sum(v1, v2) e ligaas variáveis padrão l e r a v1 e v2, respectivamente.

Em geral, padrões são construídos a partir

• Construtores de classes case, por exemplo Number, Sum, cujos argumentos são,novamente, padrões,

• variáveis padrão, por exemplo n, e1, e2,

• o padrão “coringa” _,

• literais, tal como 1, true, "abc",

• identificadores constantes, tais como MAXINT, EmptySet.

Variáveis padrão sempre iniciam com uma letra minúscula, para que possamosdistinguí-las de identificadores constantes, que iniciam com uma letra maiúscula.Cada nome de variável pode ocorrer somente uma vez em um padrão. Por exemplo,Sum(x, x) seria ilegal como padrão, pois a variável padrão x ocorre duas vezesdentro dele.

Significado do Casamento de Padrões. Uma expressão de casamento de padrões

e match { case p1 => e1 ... case pn => en }

7.2 Casamento de Padrões 49

casa os padrões p1, . . . , pn na ordem em que eles são escritos contra o valor seletore.

• Um construtor padrão C (p1, . . . , pn) casa com todos os valores que são do tipoC (ou um subtipo dele) e que foram construídos com argumentos C casandocom padrões p1, . . . , pn .

• Uma variável padrão x casa com qualquer valor e liga o nome da variávelàquele valor.

• O caracter padrão ‘_’ casa com qualquer valor, mas não liga um nome àquelevalor.

• Um padrão constante C casa um valor que é igual (em termos de ==) para C.

A expressão de casamento de padrões reescreve no lado direito do primeirocase cujo padrão casa com o valor seletor. Referências as variáveis padrão sãosubstituídas pelos correspondentes argumentos construtores. Se nenhum dospadrões casar, a expressão de casamento de padrões é abortada com um erroMatchError.

Exemplo 7.2.1 Nosso modelo de substituição de avaliação de programa estendede modo natural o casamento de padrões. Por exemplo, aqui temos como eval

aplicado a uma única expressão é reescrito:

eval(Sum(Number(1), Number(2)))

-> (através da reescrita da aplicação)

Sum(Number(1), Number(2)) match {case Number(n) => ncase Sum(e1, e2) => eval(e1) + eval(e2)

}

-> (através da reescrita do casamento de padrões)

eval(Number(1)) + eval(Number(2))

-> (através da reescrita da primeira aplicação)

Number(1) match {case Number(n) => ncase Sum(e1, e2) => eval(e1) + eval(e2)

} + eval(Number(2))

-> (através da reescrita do casamento de padrões)

50 Classes Case e Casamento de Padrões

1 + eval(Number(2))

->∗ 1 + 2 -> 3

Casamento de Padrões e Métodos. No exemplo anterior, usamos casamentode padrões numa função que era definida fora da hierarquia de classes da qualpertencia. De fato, é também possível definir uma função de casamento de padrõesna sua própria hierarquia de classes. Por exemplo, podíamos ter definido eval

como um método da classe base Expr, e ainda assim usar o casamento de padrõesna sua implementação:

abstract class Expr {def eval: Int = this match {case Number(n) => ncase Sum(e1, e2) => e1.eval + e2.eval

}}

Exercício 7.2.2 Considere as seguintes definições representando árvores deinteiros. Estas definições podem ser vistas como uma representação alternativapara IntSet:

abstract class IntTreecase object EmptyTree extends IntTreecase class Node(elem: Int, left: IntTree, right: IntTree) extends IntTree

Complete as implementações a seguir da função contains e insert para IntTree.

def contains(t: IntTree, v: Int): Boolean = t match { ......

}def insert(t: IntTree, v: Int): IntTree = t match { ......

}

Funções Anônimas e Casamento de Padrões. Até aqui, expressões case sempreapareceram conjuntamente com uma operação match. Mas é também possível usarexpressões case por elas mesmas. Um bloco de expressões case tal como

{ case P1 => E1 ... case Pn => En }

é visto como uma função que casa seus argumentos contra os padrões P1, . . . , Pn ,e produz o resultado de um dos E1, . . . , En . (Se nenhum padrão casar, a função

7.2 Casamento de Padrões 51

produzirá uma exceção MatchError. Em outras palavras, a expressão acima é vistacomo um atalho para a função anônima

(x => x match { case P1 => E1 ... case Pn => En })

onde x é uma variável nova que não é usada a não ser dentro da expressão.

Capítulo 8

Tipos Genéricos e Métodos

Classes em Scala podem ter tipos como parâmetros. Demostraremos o uso de tiposparâmetros com pilhas funcionais como exemplo. Digamos, desejamos escreverum tipo de dados para pilhas de inteiros, com métodos push, top, pop, e isEmpty.Isso é conseguido pela seguinte hierarquia de classes:

abstract class IntStack {def push(x: Int): IntStack = new IntNonEmptyStack(x, this)def isEmpty: Booleandef top: Intdef pop: IntStack

}class IntEmptyStack extends IntStack {def isEmpty = truedef top = error("EmptyStack.top")def pop = error("EmptyStack.pop")

}class IntNonEmptyStack(elem: Int, rest: IntStack) extends IntStack {def isEmpty = falsedef top = elemdef pop = rest

}

De fato, faz sentido definir uma abstração para uma pilha de Strings. Para isso,pode-se tomar a abstração existente para IntStack, renomeá-la para StringStack

e ao mesmo tempo renomear todas as ocorrências do tipo Int para String.

Um modo melhor, que não leva a duplicação de código, é parametrizar as definiçõesda pilha com o tipo do elemento. Parametrizações nos levam a generalizar apartir de uma instância específica de um problema para uma mais genérica. Atéaqui, usamos parametrização somente para valores, mas também está disponívelpara tipos. Para obtermos uma versão genérica de Stack, a equiparemos com um

54 Tipos Genéricos e Métodos

parâmetro tipo.

abstract class Stack[A] {def push(x: A): Stack[A] = new NonEmptyStack[A](x, this)def isEmpty: Booleandef top: Adef pop: Stack[A]

}class EmptyStack[A] extends Stack[A] {def isEmpty = truedef top = error("EmptyStack.top")def pop = error("EmptyStack.pop")

}class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] {def isEmpty = falsedef top = elemdef pop = rest

}

Nas definições acima, ‘A’ é um parâmetro tipo da classe Stack e suas subclasses.Parâmetros tipo são nomes arbitrários; eles são envolvidos por chaves ao invés deparênteses, portanto podem facilmente distinguidos de parâmetros valor. Aqui estáum exemplo de como classes genéricas são usadas:

val x = new EmptyStack[Int]val y = x.push(1).push(2)println(y.pop.top)

A primeira linha cria uma nova pilha vazia de Int. Observe o tipo argumento [Int]

que substitui o tipo parâmetro formal A.

Também é possível parametrizar métodos com tipos. Como um exemplo, aqui estáum método genérico que determina se uma pilha é um prefixo de outra.

def isPrefix[A](p: Stack[A], s: Stack[A]): Boolean = {p.isEmpty ||p.top == s.top && isPrefix[A](p.pop, s.pop)

}

Os parâmetros do método são chamados polimórficos. Métodos genéricos sãotambém chamados polimórficos. O termo tem origem no Grego, onde significa“que tem muitas formas”. Para aplicar um método polimórfico tal como isPrefix,passamos parâmetros tipo, bem como parâmetros valor para ele. Por exemplo,

val s1 = new EmptyStack[String].push("abc")val s2 = new EmptyStack[String].push("abx").push(s1.top)println(isPrefix[String](s1, s2))

8.1 Parâmetros Tipo Ligados 55

Inferência de Tipos Local. Passar parâmetros de tipo tais como [Int] ou[String] o tempo todo pode tornar-se enfadonho em aplicações onde funçõesgenéricas são muito utilizadas. Frequentemente, a informação dentro de umparâmetro de tipo é redundante, porque o parâmetro tipo correto pode tambémser determinado pela inspeção dos parâmetros valores da função ou do tipoesperado do resultado. Tomando a expressão isPrefix[String](s1, s2) como umexemplo, sabemos que seus parâmetros valor são ambos do tipo Stack[String],portanto podemos deduzir que o parâmetro tipo deve ser String. Scala tem umpoderoso mecanismo de inferência que nos permite omitir parâmetros tipo parafunções polimórficas e construtores em situações como esta. No exemplo acima,poderíamos ter escrito isPrefix(s1, s2) e o tipo do argumento omitido [String]

seria inserido pelo mecanismo de inferência de tipos.

8.1 Parâmetros Tipo Ligados

Agora que sabemos como criar classes genéricas é natural generalizarmos algumasdas classes escritas anteriormente. Por exemplo, a classe IntSet poderia sergeneralizada para conjuntos com tipos arbitrários de elementos. Vamos tentar. Aclasse abstrata para conjuntos genéricos é facilmente escrita.

abstract class Set[A] {def incl(x: A): Set[A]def contains(x: A): Boolean

}

Entretanto, se ainda quisermos implementar conjuntos como árvores bináriasde busca, encontraremos um problema. Os métodos contains e incl, amboscomparam elementos usando métodos < e >. Para IntSet isto está OK, pois otipo Int tem estes dois métodos. Mas para um tipo arbitrário de parâmetro a,não podemos garantir isso. Logo, a implementação anterior de, digamos, containslevará a um erro de compilação.

def contains(x: Int): Boolean =if (x < elem) left contains x

^ < não é membro do tipo A.

Um modo de resolver o problema é restringir os tipos legais que podem sersubstituídos pelo tipo A àqueles que contenham os métodos < e > do tipos corretos.Na biblioteca de classes padrão do Scala há o trait Ordered[A] que representavalores que podem ser comparados (via < e >) a valores do tipo A. Esse trait édefinido como segue:

/** Uma classe com todos os dados ordenados. */trait Ordered[A] {

56 Tipos Genéricos e Métodos

/** Resultado da comparacao de ‘this’ com o operando ‘that’.

* returna ‘x’ onde

* x < 0 iff this < that

* x == 0 iff this == that

* x > 0 iff this > that

*/def compare(that: A): Int

def < (that: A): Boolean = (this compare that) < 0def > (that: A): Boolean = (this compare that) > 0def <= (that: A): Boolean = (this compare that) <= 0def >= (that: A): Boolean = (this compare that) >= 0def compareTo(that: A): Int = compare(that)

}

Podemos forçar a compatibilidade de um tipo demandando que esse tipo sejasubtipo de Ordered. Isto é feito dando um limite superior ao parâmetro tipo deSet:

trait Set[A <: Ordered[A]] {def incl(x: A): Set[A]def contains(x: A): Boolean

}

A declaração de parâmetro A <: Ordered[A] introduz A como um parâmetrotipo que deve ser um subtipo de Ordered[A], ou seja, seus valores devem sercomparáveis a valores de mesmo tipo.

Com esta restrição, podemos agora implementar o restante da abstração genéricade conjunto como fizemos anteriormente no caso de IntSet.

class EmptySet[A <: Ordered[A]] extends Set[A] {def contains(x: A): Boolean = falsedef incl(x: A): Set[A] = new NonEmptySet(x, new EmptySet[A], new EmptySet[A])

}

class NonEmptySet[A <: Ordered[A]](elem: A, left: Set[A], right: Set[A]) extends Set[A] {

def contains(x: A): Boolean =if (x < elem) left contains xelse if (x > elem) right contains xelse true

def incl(x: A): Set[A] =if (x < elem) new NonEmptySet(elem, left incl x, right)else if (x > elem) new NonEmptySet(elem, left, right incl x)else this

}

8.1 Parâmetros Tipo Ligados 57

Observe que deixamos de fora o tipo argumento na criações dos objetosnew NonEmptySet(...). Do mesmo modo que para métodos polimórficos, tipos deargumentos faltantes nas chamadas de contrutores são inferidos a partir do valordos argumentos e/ou o tipo esperado do resultado.

Aqui está um exemplo que usa a abstração genérica de conjunto. Vamos primeirocriar uma subclasse de Ordered, como esta:

case class Num(value: Double) extends Ordered[Num] {def compare(that: Num): Int =if (this.value < that.value) -1else if (this.value > that.value) 1else 0

}

Então:

val s = new EmptySet[Num].incl(Num(1.0)).incl(Num(2.0))s.contains(Num(1.5))

Isto está OK, pois o tipo Num implementa o trait Ordered[Num]. Entretanto, oexemplo seguinte está errado.

val s = new EmptySet[java.io.File]^ java.io.File não implementa o tipoOrdered[java.io.File] definido no tipo do parâmetro.

Um problema com ligações para parâmetros tipo é que elas requerem antecipação:se não declaramos Num uma subclasse deOrdered, não estaremos aptos a usarelementos Num dentro dos conjuntos. A partir do mesmo token, tipos herdadosdo Java, tais como Int, Double, ou String não são subclasses de Ordered, portantovalores destes tipos não podem ser usados como elementos de conjuntos.

Um desenho mais flexível, que admite elementos destes tipos, usam ligações devisão ao invés de ligações plenas a tipos como temos visto. A única mudança queisto no leva no exemplo abaixo está nos parâmetros tipo:

trait Set[A <% Ordered[A]] ...class EmptySet[A <% Ordered[A]] ...class NonEmptySet[A <% Ordered[A]] ...

Ligações visão <% são mais fracas que ligações plenas <:: Uma ligação visão dacláusula do tipo parâmetro [A <% T] somente especifica que o tipo ligado A deveser convertido ao tipo ligado T, usando uma conversão implícita.

A biblioteca Scala predefine conversões implícitas para vários tipos, incluindo ostipos primitivos e String. Entretanto, o redesenho da abstração conjunto pode ser,do mesmo modo, instanciada com estes tipos. Mais explicações sobre conversões

58 Tipos Genéricos e Métodos

implicitas e ligações visão são dadas na Seção 15.

8.2 Anotações de Variância

A combinação de tipos parâmetros e subtipos levantam algumas questõesinteressantes. Por exemplo, Stack[String] deve ser um subtipo de Stack[AnyRef]?Intuitivamente, isto parece OK, pois uma pilha de Strings é um caso especialde uma pilha de AnyRefs. Mais genericamente, se T é um subtipo do tipo S,então, Stack[T] deve ser um subtipo de Stack[S]. Essa propriedade é chamadasubtipificação co-variante.

Em Scala, tipos genéricos tem por padrão subtipificação não variante. Ou seja, comStack definido conforme acima, pilhas com tipos de elementos diferentes nuncaestarão numa relação de subtipo. Entretanto, podemos forçar a subtipificaçãoco-variante das pilhas mudando a primeira linha da definição da classe Stack comosegue.

class Stack[+A] {

Prefixando um parâmetro tipo formal com um + indica que aquela subtipificaçãoé covariante naquele parâmetro. Além do +, há também um prefixo - que indicasubtipificação contravariante. Se Stack foi definida class Stack[-A] ..., então T,um subtipo do tipo S, poderia implicar que Stack[S] é um subtipo de Stack[T] (oque no caso de pilhas seria um tanto quanto surpreendente!).

Em um mundo puramente funcional, todos os tipos podem ser covariantes.Entretanto, a situação muda quando introduzimos dados mutantes. Considere ocaso de vetores em Java ou .NET. Tais vetores são representados em Scala por umaclasse genérica Array. Aqui está uma definição parcial desta classe.

class Array[A] {def apply(index: Int): Adef update(index: Int, elem: A)

}

A classe acima define o modo que vetores em Scala são vistos a partir deprogramas Scala do usuário. O compilador Scala mapeará esta abstração aosvetores subjacentes do sistema hospedeiro sempre que possível.

Em Java, vetores são, de fato, covariantes; ou seja, para os tipos referenciados T e S,se T é um subtipo de S, então Array[T] é um subtipo de Array[S]. Isso pode parecernatural, mas leva a problemas de segurança que requerem checagem especial emtempo de execução. Aqui está um exemplo:

val x = new Array[String](1)val y: Array[Any] = xy(0) = new Rational(1, 2) // isto é syntactic sugar para

8.2 Anotações de Variância 59

// y.update(0, new Rational(1, 2))

Na primeira linha, um novo vetor de strings é criado. Na segunda linha, este vetor éligado a uma variável y, de tipo Array[Any]. Assumindo vetores como covariantes,isto está OK, pois Array[String] é um subtipo de Array[Any]. Finalmente, naúltima linha, um número racional é guardado no vetor. Isso também está OK, poiso tipo Rational é um subtipo do tipo do elemento Any do vetor y. Acabamos porguardar um número racional em um vetor de strings, o que claramente viola aintegridade do tipo.

Java resolve este problema introduzindo checagem em tempo de execução naterceira linha que testa se o elemento guardado é compatível com o tipo deelemento para o qual o vetor foi criado. Vimos no exemplo que este tipo deelemento não é necessariamente o tipo estático de elemento do vetor que estásendo atualizado. Se o teste falhar, é dado um ArrayStoreException.

Ao invés disto, Scala resolve este problema estáticamente, rejeitando a segundalinha em tempo de compilação, porque vetores em Scala tem subtipificação nãovariante. Isso nos leva a questão de como o compilador Scala verifica que anotaçõesde variância são corretas. Se simplesmente declararmos vetores como covariantes,como detectar este potencial problema?

Scala usa uma aproximação conservadora para verificar a integridade de anotaçõesde variância. Um parâmetro tipo covariante de uma classe pode somente aparecerem posições covariantes dentro da classe. Apesar de posições covariantes seremtipos de valores na classe, o tipo resultante dos métodos na classe, e tiposargumentos para outros tipos covariantes. Não covariantes são tipos de parâmetrosformais de métodos. Logo, a seguinte definição de classe seria rejeitada

class Array[+A] {def apply(index: Int): Adef update(index: Int, elem: A)

^ o tipo parâmetro covariante Aaparece na posição contravariante.

}

Até aqui tudo bem. Intuitivamente, o compilador estava correto rejeitando oprocedimento update na classe covariante, porque update potencialmente mudaestado, e portanto mina a integridade da subtipificação covariante.

Entretanto, há também métodos que não mudam estado, mas onde um parâmetrotipo ainda aparece contravariantemente. Como exemplo temos o push no tipoStack. Novamente o compilador Scala rejeitará a definição deste método parapilhas covariantes.

class Stack[+A] {def push(x: A): Stack[A] =

^ o tipo parâmetro covariante A

60 Tipos Genéricos e Métodos

aparece na posição contravariante.

Isto é uma pena, porque diferente de vetores, pilhas são estruturas de dadospuramente funcionais e portanto devem habilitar a subtipificação covariante.Entretanto, há um modo de resolver o problema usando um método polimórficocom uma baixa ligação para tipos de parâmetros.

8.3 Lower Bounds

Nós temos visto ligações fortes para tipos de parâmetros. Em uma declaração detipo parâmetro tal como T <: U, o tipo parâmetro T é restrito ao intervalo somentesobre subtipos do tipo U. Simetrico a isso estão as ligações fracas em Scala. Em umadeclaração de tipo parâmetro T >: S, o tipo parâmetro T está restrito ao intervalosomente sobre supertipos do tipo S. (Pode-se também combinar ligações fracas efortes, como em T >: S <: U.)

Usando ligações fracas, podemos generalizar o método push dentro de Stack comosegue.

class Stack[+A] {def push[B >: A](x: B): Stack[B] = new NonEmptyStack(x, this)

Tecnicamente isso resolve nosso problema de variância, pois agora o tipoparâmetro A não mais aparece como um tipo parâmetro do método push. Ao invés,aparece como ligação fraca para um outro tipo parâmetro de um método, que éclassificado como uma posição covariante. Logo, o compilador Scala aceita a novadefinição de push.

De fato, não apenas resolvemos o problema técnico da variância, mas tambémgeneralizamos a definição de push. Antes, só podíamos efetuar push em elementoscom tipos que estivessem em conformidade com o tipo do elemento declarado dapilha. Agora, também podemos efetuar push sobre elementos de um supertipodeste tipo, mas o tipo da pilha retornada será alterado de acordo. Por exemplo,podemos agora efetuar push de AnyRef sobre uma pilha de Strings, mas a pilharesultante será uma pilha de AnyRefs ao invés de uma pilha de Strings!

Em resumo, não devemos hesitar em adicionar anotações de variância às estruturasde dados, pois isso enriquece naturalmente relacionamentos de subtipificação.O compilador detectará problemas de integridade potenciais. Mesmo se aaproximação do compilador for muito conservadora, frequentemente sugerirá umageneralização útil do método contestado.

8.4 Tipos Minimais 61

8.4 Tipos Minimais

Scala não nos permite parametrizar objetos com tipos. Este é o motivo pelo qualoriginalmente definimos uma classe genérica EmptyStack[A], ainda que um únicovalor denotando pilhas vazias de tipos arbitrários o fizesse. Para pilhas covariantes,entretanto, pode-se usar o seguinte idioma:

object EmptyStack extends Stack[Nothing] { ... }

O tipo base Nothing não contém valor, portanto o tipo Stack[Nothing] expressao fato que uma EmptyStack não contém elementos. Além disso, Nothing é umsubtipo de todos os outros tipos. Consequentemente, para pilhas covariantes,Stack[Nothing] é um subtipo de Stack[T], para qualquer outro tipo T. Isso tornapossível usar um único objeto pilha vazia no código do usuário. Por exemplo:

val s = EmptyStack.push("abc").push(new AnyRef())

Vamos analisar a atribuição de tipo para esta expressão em detalhes. O objetoEmptyStack é do tipo Stack[Nothing], o qual tem um método

push[B >: Nothing](elem: B): Stack[B] .

A inferência local de tipos determinará que o tipo parâmetro B deve ser instanciadopara String na aplicação EmptyStack.push("abc"). O tipo resultado destaaplicação é, consequentemente, Stack[String], que por sua vez tem um método

push[B >: String](elem: B): Stack[B] .

A parte final da definição do valor acima é a aplicação deste método anew AnyRef(). A inferência local de tipos determinará que o tipo parâmetro b

deve desta vez ser instanciado para AnyRef, com tipo resultado Stack[AnyRef].Consequentemente, o tipo atribuído ao valor s é Stack[AnyRef].

Além de Nothing, que é um subtipo para cada outro tipo, há também o tipo Null,que é um subtipo de scala.AnyRef, e de cada classe derivada dele. O literal Null emScala é o único valor deste tipo. Isto torna null compatível com cada tipo referência,mas não com um valor de tipo tal como Int.

Concluímos esta seção com a definição completa melhorada de pilhas. Pilhas temagora subtipificação covariante, o método push foi generalizado, e a pilha vazia édenotada por um único objeto.

abstract class Stack[+A] {def push[B >: A](x: B): Stack[B] = new NonEmptyStack(x, this)def isEmpty: Booleandef top: Adef pop: Stack[A]

}

62 Tipos Genéricos e Métodos

object EmptyStack extends Stack[Nothing] {def isEmpty = truedef top = error("EmptyStack.top")def pop = error("EmptyStack.pop")

}class NonEmptyStack[+A](elem: A, rest: Stack[A]) extends Stack[A] {def isEmpty = falsedef top = elemdef pop = rest

}

Muitas classes na biblioteca Scala são genéricas. Agora apresentaremos duascomumente usadas famílias de classes genéricas, tuplas e funções. A discussão deuma outra classe bem comum, listas, é postergada para o próximo capítulo.

8.5 Tuplas

Vez por outra, uma função precisa retornar mais de um resultado. Por exemplo,suponha a função divmod que retorna o quociente inteiro e o resto de doisargumentos inteiros dados. De fato, pode-se definir uma classe para pegar os doisresultados de divmod, como em:

case class TwoInts(first: Int, second: Int)def divmod(x: Int, y: Int): TwoInts = new TwoInts(x / y, x % y)

Entretanto, ter que definir uma nova classe para cada possível par de tipos deresultados é bastante tedioso. Em Scala pode-se usar, ao invés, uma classe genéricaTuple2, que é definida como segue:

package scalacase class Tuple2[A, B](_1: A, _2: B)

Com Tuple2, o método divmod pode ser escrito como segue.

def divmod(x: Int, y: Int) = new Tuple2[Int, Int](x / y, x % y)

Como sempre, parâmetros tipos para construtores podem ser omitidos se foremdedutíveis a partir dos valores dos argumentos. Há também classes tuplas para cadaoutro número de elementos (a implementação Scala atual limita isto a tuplas dealgum número razoável de elementos).

Como os elementos de tuplas são acessados? Como tuplas são classes case, háduas possibilidades. Pode-se ou acessar os campos da tupla usando os nomes dosparâmetros dos construtores _i , como no seguinte exemplo:

val xy = divmod(x, y)

8.6 Funções 63

println("quotient: " + xy._1 + ", rest: " + xy._2)

Ou usa-se casamento de padrões sobre tuplas, como no seguinte exemplo:

divmod(x, y) match {case Tuple2(n, d) =>println("quotient: " + n + ", rest: " + d)

}

Observe que tipos parâmetros nunca são usados nos padrões; seria ilegal escrevercase Tuple2[Int, Int](n, d).

Tuplas são tão convenientes que Scala define uma sintaxe especial para elas. Paraformar uma tupla com n elementos x1, . . . , xn pode-se escrever (x1, . . . , xn). Isto éequivalente a Tuplen(x1, . . . , xn). A sintaxe (...) funciona de modo equivalente paratipos e para padrões. Com esta sintaxe para tuplas, o exemplo divmod é escrito comosegue:

def divmod(x: Int, y: Int): (Int, Int) = (x / y, x % y)divmod(x, y) match {case (n, d) => println("quotient: " + n + ", rest: " + d)

}

8.6 Funções

Scala é uma linguagem funcional na qual funções são valores de primeira classe.Scala é também uma linguagem orientada a objetos na qual cada valor é umobjeto. Segue daí que funções são objetos em Scala. Por exemplo, uma funçãodo tipo String para o tipo Int é representada como uma instância do traitFunciton1[String, Int]. O trait Function1 é definido como segue.

package scalatrait Function1[-A, +B] {def apply(x: A): B

}

Ao lado de Funciton1, há também definições para funções de todas as outrasaridades (a implementação corrente implementa isto somente até um limiterazoável). Ou seja, há uma definição para cada possível número de parâmetros defunções. Sintaxe para tipos de funções em Scala (T1, . . . , Tn) => S é apenas umaabreviatura para o tipo parametrizado Functionn[T1, . . . , Tn ,S] .

Scala usa a mesma sintaxe f (x) para aplicações de funções, não importa se f éum método ou um objeto função. Isto é possível pela seguinte convenção: Umaaplicação de função f (x) onde f é um objeto (em contraste com um método) étomado para ser um atalho para f .apply(x). Consequentemente, o método apply

64 Tipos Genéricos e Métodos

de um tipo de função é inserido automaticamente onde isso é necessário.

Isso justifica o porquê definimos subscritos de vetores na Seção 8.2 através de ummétodo apply. Para cada vetor a, a operação subscritora a(i) é tomada como umatalho para a.apply(i).

Funções são exemplos em que uma declaração de um parâmetro tipocontravariante é útil. Por exemplo, considere o seguinte código:

val f: (AnyRef => Int) = x => x.hashCode()val g: (String => Int) = fg("abc")

É correto ligar o valor g de tipo String => Int a f, que é do tipo AnyRef => Int. Defato, tudo o que se pode fazer com uma função do tipo String => Int é passar-lheuma string para se obter um inteiro. Isso demonstra que subtipificar funções écontravariante nos tipos dos argumentos, enquanto é covariante no tipo do seuresultado. Em resumo, S ⇒ T é um subtipo de S′ ⇒ T ′, desde que S′ seja um subtipode S e T seja um subtipo de T ′.

Exemplo 8.6.1 Considere o código Scala

val plus1: (Int => Int) = (x: Int) => x + 1plus1(2)

Isso é expandido no seguinte código objeto.

val plus1: Function1[Int, Int] = new Function1[Int, Int] {def apply(x: Int): Int = x + 1

}plus1.apply(2)

Aqui, a criação do objeto new Function1[Int, Int]{ ... } representa umainstância de uma classe anônima. Combina a criação de um novo objeto Function1

com uma implementação do método apply (que é abstrato dentro de Function1).Equivalentemente, mas mais prolixo, podería-se usar uma classe local:

val plus1: Function1[Int, Int] = {class Local extends Function1[Int, Int] {def apply(x: Int): Int = x + 1

}new Local: Function1[Int, Int]

}plus1.apply(2)

Capítulo 9

Listas

Listas são uma importante estrutura de dados em muitos programas Scala. Umalista contendo os elementos x1, . . . , xn é escrita List(x1, ..., xn). Algunsexemplos:

val fruit = List("apples", "oranges", "pears")val nums = List(1, 2, 3, 4)val diag3 = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1))val empty = List()

Listas são similares a vetores em linguagens tais como C ou Java, mas há tambémtrês importantes diferenças. Primeiro, listas são imutáveis. Ou seja, elementos deuma lista não podem ser mudados por meio de atribuição. Segundo, listas temuma estrutura recursiva, enquanto vetores são triviais. Terceiro, em geral, listassuportam um conjunto muito mais rico de operações que vetores.

9.1 Usando Listas

O tipo lista. Do mesmo modo que com vetores, listas são homogêneas. Ou seja, oselementos de uma lista têm todos o mesmo tipo. O tipo de uma lista com elementosde tipo T é escrito List[T] (compare com T[] em Java).

val fruit: List[String] = List("apples", "oranges", "pears")val nums : List[Int] = List(1, 2, 3, 4)val diag3: List[List[Int]] = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1))val empty: List[Int] = List()

Construtores de listas. Todas as listas são construídas a partir de doisconstrutores fundamentais, Nil e :: (lê-se “cons”). Nil representa uma lista vazia.

66 Listas

O operador infixo :: expressa a extensão da lista. Ou seja, x :: xs denota umalista cujo primeiro elemento é x, e que é seguido pela (os elementos da) lista xs.Consequentemente, os valores da lista acima podem também ter sido definidoscomo segue (de fato essa definição prévia é apenas um facilitador sintático paraas definições abaixo).

val fruit = "apples" :: ("oranges" :: ("pears" :: Nil))val nums = 1 :: (2 :: (3 :: (4 :: Nil)))val diag3 = (1 :: (0 :: (0 :: Nil))) ::

(0 :: (1 :: (0 :: Nil))) ::(0 :: (0 :: (1 :: Nil))) :: Nil

val empty = Nil

A operação ‘::’ é associativa à direita: A :: B :: C é interpretada comoA :: (B :: C). Por essa razão, podemos retirar os parênteses das definições acima.Por exemplo, podemos escrever de modo resumido

val nums = 1 :: 2 :: 3 :: 4 :: Nil

Operações básicas sobre listas. Todas as operações sobre listas podem serexpressas em termos das três a seguir:

head retorna o primeiro elemento de uma lista,tail retorna a lista que consiste de todos os elementos exceto o

primeiro elemento,isEmpty retorna true se e só se a lista for vazia.

Estas operações são definidas como métodos de objetos listas. Portanto asinvocamos escolhendo da lista aqueles que sofrerão a operação. Exemplos:

empty.isEmpty = truefruit.isEmpty = falsefruit.head = "apples"fruit.tail.head = "oranges"diag3.head = List(1, 0, 0)

Os métodos head e tail são definidos somente para listas não vazias. Quandoselecionados para uma lista vazia, eles lançam uma exceção.

Como um exemplo de como listas podem ser processadas, considere ordenar oselementos de uma lista de números em ordem crescente. Um modo simples defazer isso é usar o insertion sort, que trabalha da seguinte maneira: Para ordenaruma lista não vazia com primeiro elemento x e resto xs, ordene o restante xs e insirao elemento x na posição correta do resultado. Ordenar uma lista vazia dará umalista vazia. Em Scala temos o código:

def isort(xs: List[Int]): List[Int] =

9.2 Definição da classe List I: Métodos de Primeira Ordem 67

if (xs.isEmpty) Nilelse insert(xs.head, isort(xs.tail))

Exercício 9.1.1 Escreva a função faltante insert.

Listas e padrões. De fato, :: é definido como uma classe case na bibliotecapadrão Scala. Consequentemente, é possível decompor listas através de casamentode padrões, usando padrões compostos a partir dos construtores Nil e ::. Porexemplo, isort pode ser escrito aternativamente como segue.

def isort(xs: List[Int]): List[Int] = xs match {case List() => List()case x :: xs1 => insert(x, isort(xs1))

}

onde

def insert(x: Int, xs: List[Int]): List[Int] = xs match {case List() => List(x)case y :: ys => if (x <= y) x :: xs else y :: insert(x, ys)

}

9.2 Definição da classe List I: Métodos de Primeira Ordem

Listas não são construídas em Scala; elas são definidas por uma classe abstrataList, que vem com duas subclasses para :: e Nil. A seguir apresentaremos umtour através da classe List.

package scalaabstract class List[+A] {

List é uma classe abstrata, logo não pode-se definir elementos chamandoo construtor de List vazia (ou seja, através de new List). A classe temuma tipo parâmetro a. É covariante nesse parâmetro, o que significa queList[S] <: List[T] para todos os tipos S e T tal que S <: T. A classe está nopacote scala. Este pacote contém as mais importantes classes Scala. List defineum número de métodos, que são explicados a seguir.

Decompondo listas. Primeiro, há os três métodos básicos isEmpty, head, tail.Suas implementações em termos de casamento de padrões são diretas:

def isEmpty: Boolean = this match {case Nil => true

68 Listas

case x :: xs => false}def head: A = this match {case Nil => error("Nil.head")case x :: xs => x

}def tail: List[A] = this match {case Nil => error("Nil.tail")case x :: xs => xs

}

A próxima função computa o tamanho de uma lista.

def length: Int = this match {case Nil => 0case x :: xs => 1 + xs.length

}

Exercício 9.2.1 Escreva uma versão recursiva de cauda de length.

As próximas duas funções são o complemento para head e tail.

def last: Adef init: List[A]

xs.last retorna o último elemento da lista xs, enquanto xs.init retorna todosos elementos de xs exceto o último. Ambas as funções tem de atravessar toda alista, e são, portanto, menos eficientes que seus análogos head e tail. Aqui está aimplementação de last.

def last: A = this match {case Nil => error("Nil.last")case x :: Nil => xcase x :: xs => xs.last

}

A implementação de init é análoga.

As próximas três funções retornam um prefixo da lista, ou um sufixo, ou ambos.

def take(n: Int): List[A] =if (n == 0 || isEmpty) Nil else head :: tail.take(n-1)

def drop(n: Int): List[A] =if (n == 0 || isEmpty) this else tail.drop(n-1)

def split(n: Int): (List[A], List[A]) = (take(n), drop(n))

9.2 Definição da classe List I: Métodos de Primeira Ordem 69

(xs take n) retorna os primeiros n elementos da lista xs, ou a lista inteira, caso seutamanho seja menor que n. (xs drop n) retorna todos os elementos de xs excetoos n primeiros. Finalmente, (xs split n) retorna um par consistindo das listasresultantes de xs take n e xs drop n.

A próxima função retorna um elemento de uma dada posição na lista. É, portanto,análoga ao subscrito de vetor. Índices começam em 0.

def apply(n: Int): A = drop(n).head

O método apply tem um significado especial em Scala. Um objeto com um métodoapply pode ser aplicado a argumentos como se fosse uma função. Por exemplo,para pegar o terceiro elemento de uma lista xs, pode-se escrever ou xs.apply(3)

ou xs(3)—a última expressão expande na primeira.

Com take e drop, podemos extrair sublistas consistindo de elementos consecutivosda lista original. Para extrair a sublista xsm , . . . , xsn−1 da lista xs, use:

xs.drop(m).take(n - m)

Zipando listas. A próxima função combina duas listas em uma lista de pares.Dadas duas listas

xs = List(x1, ..., xn) , eys = List(y1, ..., yn) ,

xs zip ys constrói a lista List((x1, y1), ..., (xn, yn)). Se as duas listastiverem tamanhos diferentes, a maior das duas é truncada. Aqui está a definiçãode zip—observe que trata-se de um método polimórfico.

def zip[B](that: List[B]): List[(a,b)] =if (this.isEmpty || that.isEmpty) Nilelse (this.head, that.head) :: (this.tail zip that.tail)

Consing listas.. Como qualquer outro operador infixo, :: também éimplementado como um método de um objeto. Neste caso, o objeto é a listaque é estendida. Isto é possível porque operadores terminados com um caracter ‘:’são tratados de modo especial em Scala. Todos esses operadores são tratados comométodos de seus operandos direitos. Ou seja,

x :: y = y.::(x) enquanto que x + y = x.+(y)

Observe, entretanto, que operandos de uma operação binária são em cada casoavaliados da esquerda para a direita. Logo, se D e E são expressões com possíveisefeitos colaterais, D :: E é traduzido para {val x = D; E.::(x)} de modo amanter a ordem esquerda-para-direita da avaliação dos operandos.

70 Listas

Outra diferença entre operadores terminando com um ‘:’ e outros operadores éconcernente à associatividade. Operadores terminados com ‘:’ são associativos àdireita, enquanto outros operadores são associativos à esquerda. Isto é,

x :: y :: z = x :: (y :: z) enquanto que x + y + z = (x + y) + z

A definição de :: como um método na classe List é a seguinte:

def ::[B >: A](x: B): List[B] = new scala.::(x, this)

Observe que :: é definido para todos os elementos x de tipo B e listas do tipoList[A] tais como o tipo B de x é um supertipo dos elementos da lista de tipo A.O resultado neste caso é uma lista de Bs. Isto é expresso pelo tipo parâmetro B comligação fraca A na assinatura de ::.

Concatenando listas. Uma operação similar a :: é a concatenação de listas,escrita ‘:::’. O resultado de (xs ::: ys) é uma lista consistindo de todos oselementos de xs, seguidos para todos os elementos de ys. Como termina em doispontos, ::: é associativo à direita e é considerado método de seu operando direito.Por isso,

xs ::: ys ::: zs = xs ::: (ys ::: zs)= zs.:::(ys).:::(xs)

Aqui está a implementação do método ::::

def :::[B >: A](prefix: List[B]): List[B] = prefix match {case Nil => thiscase p :: ps => this.:::(ps).::(p)

}

Invertendo listas. Outra operação útil é a inversão de lista. Há um métodoreverse em List que tem esse efeito. Vamos tentar dar a implementação:

def reverse[A](xs: List[A]): List[A] = xs match {case Nil => Nilcase x :: xs => reverse(xs) ::: List(x)

}

Esta implementação tem a vantagem de ser mais simples, mas não é muitoeficiente. Na verdade, uma concatenação é feita para cada elemento da lista.Concatenação de listas leva tempo proporcional ao tamanho do seu primeirooperando. Consequentemente, a complexidade de reverse(xs) é

n + (n −1)+ ...+1 = n(n +1)/2

9.3 Exemplo: Merge sort 71

onde n é o tamanho de xs. Pode-se implementar reverse mais eficientemente?Veremos mais tarde que há uma outra implementação que tem complexidadelinear.

9.3 Exemplo: Merge sort

O insertion sort apresentado anteriormente neste capítulo é simples de formular,mas também não é muito eficiente. Sua complexidade média é proporcional aoquadrado do tamanho de sua lista de entrata. Agora escreveremos um programapara ordenar os elementos de uma lista que é mais eficiente que o insertion sort.Um bom algoritmo para isso é merge sort, que trabalha do seguinte modo.

Primeiro, se a lista tem zero ou um elementos, já está ordenada, logo retornamosa lista sem modificações. Listas mais longas são divididas em duas sublistas, cadauma contendo por volta de metade dos elementos da lista original. Cada sublista éordenada através de uma chamada recursiva para a função de ordenação, e as duaslistas ordenadas resultantes são então combinadas em uma operação merge.

Para uma implementação geral do merge sort, ainda temos que especificar otipo dos elementos da lista a ser ordenada, bem como a função a ser usadana comparação dos elementos. Obtemos uma função de generalidade maximalpassando estes dois itens como parâmetros. Isto leva a seguinte implementação.

def msort[A](less: (A, A) => Boolean)(xs: List[A]): List[A] = {def merge(xs1: List[A], xs2: List[A]): List[A] =if (xs1.isEmpty) xs2else if (xs2.isEmpty) xs1else if (less(xs1.head, xs2.head)) xs1.head :: merge(xs1.tail, xs2)else xs2.head :: merge(xs1, xs2.tail)

val n = xs.length/2if (n == 0) xselse merge(msort(less)(xs take n), msort(less)(xs drop n))

}

A complexidade do msort é O(N l og (N )), onde N é o tamanho da lista de entrada.Para ver porque, observe que dividir uma lista em duas e intercalar as duas listasordenadas leva tempo proporcional ao tamanho das listas argumentos. Cadachamada recursiva de msort reduz a metade o número de elementos na sua entrada,logo há O(l og (N )) chamadas recursivas consecutivas, até que o caso base daslistas de tamanho 1 seja alcançado. Entretanto, para listas mais longas, cadachamada gera duas outras chamadas. Somando tudo acima obtemos que a cadanível O(log (N )) de chamada, cada elemento da lista original toma parte em umaoperação de divisão e em uma operação merge. Consequentemente, cada nívelde chamada tem um custo proporcional total de O(N ). Como há O(log (N )) níveisde chamada, obtemos um custo total de O(N log (N )). Este custo não depende da

72 Listas

distribuição inicial dos elementos na lista, portanto o pior caso tem o mesmo custoque o caso médio. Isto torna o merge sort um algoritmo atraente para ordenação delistas.

Aqui está um exemplo de como msort é usado.

msort((x: Int, y: Int) => x < y)(List(5, 7, 1, 3))

A definição de msort está currificada para tornar sua especialização com funções decomparação. Por exemplo,

val intSort = msort((x: Int, y: Int) => x < y)val reverseSort = msort((x: Int, y: Int) => x > y)

9.4 Definição da classe List II: Métodos de Alta Ordem

Os exemplos encontrados até aqui mostram que funções sobre listasfrequentemente tem estruturas similares. Podemos identificar vários padrõesde computação sobre listas, tais como:

• transformar cada elemento de uma lista de algum modo.

• extrair de uma lista todos os elementos que satisfaçam uma critério.

• combinar os elementos de uma lista usando algum operador.

Linguagens de programação funcional habilitam programadores a escrever funçõesgenéricas que implementam padrões como estes por meio de funções de altaordem. Agora discutiremos um conjunto comumente usado em funções de altaordem, que são implementados como métodos dentro da classe List.

Mapping sobre listas. Um operação comum é transformar cada elemento de umalista e então retornar a lista de resultados. Por exemplo, para multiplicar cadaelemento de uma lista por um dado fator.

def scaleList(xs: List[Double], factor: Double): List[Double] = xs match {case Nil => xscase x :: xs1 => x * factor :: scaleList(xs1, factor)

}

Este padrão pode ser generalizado para o método map da classe List:

abstract class List[A] { ...def map[B](f: A => B): List[B] = this match {case Nil => thiscase x :: xs => f(x) :: xs.map(f)

}

9.4 Definição da classe List II: Métodos de Alta Ordem 73

Usando map, scaleList pode ser mais concisamente escrito como segue.

def scaleList(xs: List[Double], factor: Double) =xs map (x => x * factor)

Como outro exemplo, considere o problema de retornar uma dada coluna de umamatriz que é representada como uma lista de linhas, onde cada linha é novamenteuma lista. Isto é feito através da seguinte função column.

def column[A](xs: List[List[A]], index: Int): List[A] =xs map (row => row(index))

Um método similar a map é o método foreach que aplica uma dada função a todosos elementos de uma lista, mas não constrói uma lista de resultados. A função éassim aplicada somente por seu efeito colateral. foreach é definido como segue.

def foreach(f: A => Unit) {this match {case Nil => ()case x :: xs => f(x); xs.foreach(f)

}}

Esta função pode ser usada para imprimir todos os elementos de uma lista, porexemplo:

xs foreach (x => println(x))

Exercício 9.4.1 Considere uma função que eleva ao quadrado todos os elementosde uma lista e retorna uma lista com os resultados. Complete as duas definições aseguir de squareList.

def squareList(xs: List[Int]): List[Int] = xs match {case List() => ??case y :: ys => ??

}def squareList(xs: List[Int]): List[Int] =xs map ??

Filtrando Listas. Outra operação comum seleciona de uma lista todos oselemento que satisfazem um dado critério. Por exemplo, para retornar uma listade todos os elementos positivos de algumas listas dadas de inteiros:

def posElems(xs: List[Int]): List[Int] = xs match {case Nil => xscase x :: xs1 => if (x > 0) x :: posElems(xs1) else posElems(xs1)

74 Listas

}

Este padrão é generalizado para o método filter da classe List:

def filter(p: A => Boolean): List[A] = this match {case Nil => thiscase x :: xs => if (p(x)) x :: xs.filter(p) else xs.filter(p)

}

Usando filter, posElems pode ser mais concisamente escrito como segue.

def posElems(xs: List[Int]): List[Int] =xs filter (x => x > 0)

Uma operação relacionada a filtering é testar se todos os elementos de uma listasatisfazem uma dada condição. Dualmente, pode-se também estar interessado naquestão se há um elemento em uma lista que satisfaz uma dada condição. Estasoperações são incorporadas nas funções de alta ordem forall e exists da classeList.

def forall(p: A => Boolean): Boolean =isEmpty || (p(head) && (tail forall p))

def exists(p: A => Boolean): Boolean =!isEmpty && (p(head) || (tail exists p))

Para ilustrar o uso de forall, considere a questão se um número é primo. Lembreque um número n é primo se puder ser dividido, sem resto, somente por um e porele mesmo. A tradução mais direta desta definição testará se n dividido por todos osnúmeros de 2 até, mas excluindo, ele mesmo dá resto diferente de zero. Esta lista denúmeros pode ser gerada usando uma função List.range que é definida no objetoList como segue.

package scalaobject List { ...def range(from: Int, end: Int): List[Int] =if (from >= end) Nil else from :: range(from + 1, end)

Por exemplo, List.range(2, n) gera a lista de todos os inteiros de 2 até, excluindo,n. A função isPrime pode agora ser defida como segue.

def isPrime(n: Int) =List.range(2, n) forall (x => n % x != 0)

Vemos que a definição matemática de primalidade pode ser traduzida diretamenteem código Scala.

Exercício: Defina forall e exists em termos de filter.

9.4 Definição da classe List II: Métodos de Alta Ordem 75

Desdobrando (folding) e Reduzindo Listas. Uma outra operação comum écombinar os elementos de uma lista com algum operador. Por exemplo:

sum(List(x1, ..., xn)) = 0 + x1 + ... + xn

product(List(x1, ..., xn)) = 1 * x1 * ... * xn

De fato, podemos implementar ambas as funções por meio de um esquemarecursivo:

def sum(xs: List[Int]): Int = xs match {case Nil => 0case y :: ys => y + sum(ys)

}def product(xs: List[Int]): Int = xs match {case Nil => 1case y :: ys => y * product(ys)

}

Mas também podemos usar a generalização deste esquema de programaincorporado no método reduceLeft da classe List. Este método insere um dadooperador binário entre elementos adjacentes de uma dada lista. Ou seja,

List(x1, ..., xn).reduceLeft(op) = (...(x1 op x2) op ... ) op xn

Usando reduceLeft, podemos tornar o padrão comum emas sum e product

aparente:

def sum(xs: List[Int]) = (0 :: xs) reduceLeft {(x, y) => x + y}def product(xs: List[Int]) = (1 :: xs) reduceLeft {(x, y) => x * y}

Aqui está a implementação de reduceLeft.

def reduceLeft(op: (A, A) => A): A = this match {case Nil => error("Nil.reduceLeft")case x :: xs => (xs foldLeft x)(op)

}def foldLeft[B](z: B)(op: (B, A) => B): B = this match {case Nil => zcase x :: xs => (xs foldLeft op(z, x))(op)

}}

Vemos que o método reduceLeft é definido em termos de um outro método,geralmente útil, foldLeft. O último pega como parâmetro adicional umacumulador z, que é retornado quando foldLeft é aplicado sobre uma lista vazia.Ou seja,

(List(x1, ..., xn) foldLeft z)(op) = (...(z op x1) op ... ) op xn

76 Listas

Os métodos sum e product podem ser defidos alternativamente usando foldLeft:

def sum(xs: List[Int]) = (xs foldLeft 0) {(x, y) => x + y}def product(xs: List[Int]) = (xs foldLeft 1) {(x, y) => x * y}

FoldRight e ReduceRight. Aplicações de foldLeft e reduceLeft expandem paraárvores inclinadas à esquerda. . Eles têm duals foldRight e reduceRight queproduzem árvores inclinadas à direita.

List(x1, ..., xn).reduceRight(op) = x1 op ( ... (xn−1 op xn)...)(List(x1, ..., xn) foldRight acc)(op) = x1 op ( ... (xn op acc)...)

Estes são definidos como segue.

def reduceRight(op: (A, A) => A): A = this match {case Nil => error("Nil.reduceRight")case x :: Nil => xcase x :: xs => op(x, xs.reduceRight(op))

}def foldRight[B](z: B)(op: (A, B) => B): B = this match {case Nil => zcase x :: xs => op(x, (xs foldRight z)(op))

}

A classe List define também duas abreviaturas simbólicas para foldLeft efoldRight:

def /:[B](z: B)(f: (B, A) => B): B = foldLeft(z)(f)def :\[B](z: B)(f: (A, B) => B): B = foldRight(z)(f)

Os nomes dos métodos ilustram a inclinação à esquerda/direita das árvores dasoperações fold através de barra simples ou invertida. O : aponta em cada caso paraa lista de argumentos enquanto o fim da barra aponta para o acumulador (ou: zero)argumento z. Ou seja,

(z /: List(x1, ..., xn))(op) = (...(z op x1) op ... ) op xn

(List(x1, ..., xn) :\ z)(op) = x1 op ( ... (xn op z)...)

Através de operadores associativos e comutativos, /: e :\ são equivalentes (mesmosabendo que podem ser diferentes em eficiência).

Exercício 9.4.2 Considere o problema de escrever uma função flatten, querecebe uma lista de listas como argumentos. O resultado de flatten deve ser aconcatenação de todos os elementos listas em uma única lista. Aqui está umaimplementação deste método em termos de :\.

def flatten[A](xs: List[List[A]]): List[A] =

9.4 Definição da classe List II: Métodos de Alta Ordem 77

(xs :\ (Nil: List[A])) {(x, xs) => x ::: xs}

Considere substituir o corpo de flatten por

((Nil: List[A]) /: xs) ((xs, x) => xs ::: x)

Qual a diferença na complexidade assintótica entre as duas versões de flatten?

De fato flatten é predefinido junto com um conjunto de outras funções úteisno objeto chamado List na biblioteca padrão Scala. Pode ser acessado de umprograma usuário chamando List.flatten. Observe que flatten não é ummétodo da classe List—não faz sentido, pois aplica-se somente a listas de listas,não a todas as listas em geral.

Novamente Inversão de Lista. Vimos na Seção 9.2 uma implementação dométodo reverse cujo tempo de execução era quadrático para o tamanho da listaa ser invertida. Agora desenvolveremos uma nova implementação de reverse, cujocusto é linear. A idéia é usar uma operação foldLeft baseada no seguinte programascheme.

class List[+A] { ...def reverse: List[A] = (z? /: this)(op?)

Agora falta preencher z? e op?. Vamos tentar deduzir a partir dos exemplos.

Nil= Nil.reverse // pela especifica\c{c}\~{a}o= (z /: Nil)(op) // pelo template para reverse= (Nil foldLeft z)(op) // pela defini\c{c}\~{a}o de /:= z // pela defini\c{c}\~{a}o de foldLeft

Consequentemente, z? deve ser Nil. Para deduzir o segundo operando, vamosestudar o inverso de uma lista de tamanho um.

List(x)= List(x).reverse // pela especifica\c{c}\~{a}o= (Nil /: List(x))(op) // pelo template para reverse, com z = Nil= (List(x) foldLeft Nil)(op) // pela defini\c{c}\~{a}o de /:= op(Nil, x) // pela defini\c{c}\~{a}o de foldLeft

Consequentemente, op(Nil, x) é igual a List(x), o que é o mesmo quex :: Nil. Isto sugere pegar como op o operador :: com seus operandos trocados.Consequentemente, chegamos a seguinte implementação para reverse, que temcomplexidade linear.

def reverse: List[A] =((Nil: List[A]) /: this) {(xs, x) => x :: xs}

78 Listas

(Obs: O tipo de anotação para Nil é necessário para que a inferência de tiposfuncione.)

Exercício 9.4.3 Preencha as expressões faltantes para completar as seguintesdefinições de algumas operações básicas de manipulação de listas como operaçõesfold.

def mapFun[A, B](xs: List[A], f: A => B): List[B] =(xs :\ List[B]()){ ?? }

def lengthFun[A](xs: List[A]): int =(0 /: xs){ ?? }

Mapeamentos Aninhados. Podemos empregar funções de processamento delistas de alta ordem para expressar muita computação que, normalmente é expressaatravés de aninhamento de laços nas linguagens imperativas.

Como exemplo, considere o seguinte problema: Dado um inteiro positivo n,encontre todos os pares de inteiros positivos i e j , onde 1 ≤ j < i < n tal que i + jseja primo. Por exemplo, se n = 7, os pares são

i 2 3 4 4 5 6 6j 1 2 1 3 2 1 5

i + j 3 5 5 7 7 7 11

Um modo natural de resolver este problema consiste em dois passos. Num primeiropasso, gera-se a sequência de todos os pares (i , j ) de inteiros tal que 1 ≤ j < i < n.Num segundo passo filtra-se, a partir desta sequência, todos os pares (i , j ) tal quei + j é primo.

Examinando o primeiro passo em detalhe, um modo natural de gerar a sequênciade pares consiste de três sub-passos. Primeiro, gera-se todos os inteiros entre 1 e npara i .

Segundo, para cada inteiro i entre 1 e n, gera-se a lista de pares (i ,1) up to (i , i −1).Isso pode ser conseguido através de uma combinação de range e map:

List.range(1, i) map (x => (i, x))

Finalmente, combina-se todas as sublistas usando foldRight com :::. Juntandotudo dá a seguinte expressão:

List.range(1, n).map(i => List.range(1, i).map(x => (i, x))).foldRight(List[(Int, Int)]()) {(xs, ys) => xs ::: ys}.filter(pair => isPrime(pair._1 + pair._2))

9.5 Sumário 79

Flattening Maps. A combinação entre mapeamento e a concatenação dassublistas resultantes do mapeamento é tão comum que há um método especial paraisto na classe List:

abstract class List[+A] { ...def flatMap[B](f: A => List[B]): List[B] = this match {case Nil => Nilcase x :: xs => f(x) ::: (xs flatMap f)

}}

Com flatMap, os expressão dos “pares cuja soma dá um primo” pode ser escrita demodo mais sucinto como segue.

List.range(1, n).flatMap(i => List.range(1, i).map(x => (i, x))).filter(pair => isPrime(pair._1 + pair._2))

9.5 Sumário

Este capítulo introduziu listas como uma estrutura de dados fundamental naprogramação. Como listas são imutáveis, elas são um tipo de dado comumna programação em linguagens funcionais. Elas têm importância comparávela vetores nas linguagens imperativas. Entretanto, o padrão de acesso é bemdiferente entre os dois. Enquanto o acesso em vetores é sempre feito através deindexação, isto é muito incomum em listas. Nós vimos que scala.List define ummétodo chamado apply para a indexação, entretanto, esta operação é muito maiscustosa que no caso dos vetores (linear em comparação a tempo constante). Aoinvés da indexação, listas são geralmente percorridas recursivamente, onde passosrecursivos são em geral baseados em casamento de padrões sobre a lista percorrida.Há também um rico conjunto de combinadores de alta ordem que permitema instanciação de um conjunto de padrões pré-definidos de computações sobrelistas.

Capítulo 10

For-Comprehensions

O capítulo anterior mostrou que funções de alta ordem, tais como map, flatMap,filter provém poderosas construções para lidar com listas. Mas algumas vezeso nível de abstração requerido por estas funções tornam um programa difícil deentender.

Para ajudar a inteligibilidade, Scala tem uma notação especial que simplificapadrões comuns de aplicações de funções de alta ordem. Esta notação cria umaponte entre abrangência de conjuntos (set-comprehensions) na matemática e laçosfor em linguagens imperativas tais como C ou Java. Também lembram de perto anotação de pesquisa em bases de dados relacionais.

Como um primeiro exemplo, digamos que nos é dada uma lista persons de pessoascom campos name e age (idade). Para imprimir os nomes de todas as pessoas nasequência para idades acima de 20, pode-se escrever:

for (p <- persons if p.age > 20) yield p.name

Isto é equivalente à seguinte expressão, que usa as funções de alta ordem filter emap:

persons filter (p => p.age > 20) map (p => p.name)

A abrangência for (for-comprehension) parece um pouco com um laço for naslinguagens imperativas, exceto que constrói uma lista de resultados de todasiterações.

Geralmente, um for-comprehension tem a forma

for ( s ) yield e

Aqui, s é uma sequência de geradores, definições e filtros. Um gerador tem a formaval x <- e, onde e é uma expressão avaliada como lista. Liga x a sucessivos valoresna lista. Uma definição tem a forma val x = e. Introduz x como um nome para o

82 For-Comprehensions

valor de e no restante da abrangência. Um filtro é uma expressão f do tipo Boolean.Omite da consideração todas as ligações para as quais f é falso. A sequência scomeça em caad case com um gerador. Se houver vários geradores na sequência,os geradores subsequentes variarão mais rapidamente que os anteriores.

A sequência s pode também ser envolvida em chaves ao invés de parênteses, e osdois pontos entre geradores, definições e filtros podem ser omitidos.

Aqui estão dois exemplos que mostram como for-comprehensions são usados.Primeiro, vamos refazer um exemplo do capítulo anterior: Dado um inteiro positivon, encontre todos os pares de inteiros positivos i e j , onde 1 ≤ j < i < n such thati + j é primo. Com um for-comprehension este problema é resolvido como segue:

for { i <- List.range(1, n)j <- List.range(1, i)if isPrime(i+j) } yield {i, j}

Isto é discutivelmente mais claro que a solução usando map, flatMap e filter quedesenvolvemos previamente.

Como segundo exemplo, considere calcular o produto escalar de dois vetores xs eys. Usando um for-comprehension, isto pode ser escrito como segue.

sum(for ((x, y) <- xs zip ys) yield x * y)

10.1 O Problema das N-Rainhas

For-comprehensions são especialmente úteis para resolver puzzles combinatórios.Um exemplo é o problema das 8-rainhas: Dado um tabuleiro de xadrez padrão,coloque 8 rainhas, tal que nenhuma rainha esteja atacada por nenhuma outra(uma rainha pode atacar qualquer outra peça se estiver na mesma coluna,linha ou diagonal que a mesma). Desenvolveremos agora uma solução paraeste problema, generalizando para tabuleiros de xadrez de tamanho arbitrário.Consequentemente, o problema é colocar n rainhas em um tabuleiro de xadrez detamanho n ×n.

Para resolver este problema, observe que precisamos colocar uma rainha em cadalinha. Logo podemos colocar rainhas em linhas sucessivas, cada vez checando queuma rainha recentemente colocada não esteja atacada por qualquer outra rainhaque já se encontra no tabuleiro. No curso desta busca, pode acontecer que umarainha a ser colocada na linha k esteja atacada em todas as casas desta linha porrainhas rainhas das linhas 1 até k−1. Neste caso, precisamos interromper esta parteda busca e continuar com uma configuração diferente de rainhas nas colunas 1 aték −1.

Isso sugere um algoritmo recursivo. Assuma que já geramos todas as soluções paracolocar k −1 rainhas em um tabuleiro de tamanho n ×n. Podemos representar tal

10.2 Pesquisando com For-Comprehensions 83

solução por uma lista de tamanho k − 1 de números das colunas (no intervalo de1 a n). Tratamos estas listas de soluções parciais como pilhas, onde o número dacoluna da rainha na linha k −1 vem primeiro dentro da lista, seguido pelo númeroda coluna da rainha na linha k −2 etc. O fundo da pilha é o número da coluna darainha colocada na primeira linha do tabuleiro. Todas as soluções juntas são entãorepresentadas como uma lista de listas, com um elemento para cada solução.

Agora, para colocar a k-ésima rainha, geramos todas as possíveis extensões paracada solução prévia com uma rainha a mais. Isto leva a uma outra lista de listassoluções, desta vez de tamanho k. Continuamos o processo até atingirmos soluçõesdo tamanho n do tabuleiro de xadrez. Esta idéia algorítmica é incorporada nafunção placeQueens abaixo:

def queens(n: Int): List[List[Int]] = {def placeQueens(k: Int): List[List[Int]] =if (k == 0) List(List())else for { queens <- placeQueens(k - 1)

column <- List.range(1, n + 1)if isSafe(column, queens, 1) } yield column :: queens

placeQueens(n)}

Exercício 10.1.1 Escreva a função

def isSafe(col: Int, queens: List[Int], delta: Int): Boolean

que testa se uma rainha numa dada coluna col está segura com respeito às queensjá colocadas. Aqui, delta é a diferença entre a linha da rainha a ser colocada e alinha da primeira rainha da lista.

10.2 Pesquisando com For-Comprehensions

A notação for é essencialmente equivalente a operações comuns de linguagens depesquisa de bases de dados. Por exemplo, digamos que temos uma base de dadosbooks, representada como uma lista de livros, onde Book é definido como segue.

case class Book(title: String, authors: List[String])

Aqui está um pequeno exemplo de base de dados:

val books: List[Book] = List(Book("Structure and Interpretation of Computer Programs",

List("Abelson, Harold", "Sussman, Gerald J.")),Book("Principles of Compiler Design",

List("Aho, Alfred", "Ullman, Jeffrey")),Book("Programming in Modula-2",

84 For-Comprehensions

List("Wirth, Niklaus")),Book("Introduction to Functional Programming"),

List("Bird, Richard")),Book("The Java Language Specification",

List("Gosling, James", "Joy, Bill", "Steele, Guy", "Bracha, Gilad")))

Então, para encontrar os títulos de todos os livros cujos autores tenham sobrenome“Ullman”:

for (b <- books; a <- b.authors if a startsWith "Ullman")yield b.title

(Aqui, startsWith é um método dentro em java.lang.String). Ou, para encontraros títulos de todos os livros que tenham a cadeia de caracteres “Program” em seutítulo:

for (b <- books if (b.title indexOf "Program") >= 0)yield b.title

Ou, para encontrar os nomes de todos os autores que escreveram pelo menos doislivros, na base de dados.

for (b1 <- books; b2 <- books if b1 != b2;a1 <- b1.authors; a2 <- b2.authors if a1 == a2)

yield a1

A última solução ainda não é perfeita, porque autores aparecerão diversas vezesna lista de resultados. Ainda precisamos remover autores duplicados das listasresultantes. Isto pode ser obtido através da seguinte função.

def removeDuplicates[A](xs: List[A]): List[A] =if (xs.isEmpty) xselse xs.head :: removeDuplicates(xs.tail filter (x => x != xs.head))

Observe que a última expressão no método removeDuplicates pode serequivalentemente expresso usando um for-comprehension.

xs.head :: removeDuplicates(for (x <- xs.tail if x != xs.head) yield x)

10.3 Tradução de For-Comprehensions

Cada for-comprehension pode ser expresso em termos de três funções dealta-ordem: map, flatMap e filter. Aqui está o esquema de tradução, que tambémé usado pelo compilador Scala.

• Um for-comprehension simples

10.3 Tradução de For-Comprehensions 85

for (x <- e) yield e’

é traduzido para

e.map(x => e’)

• Um for-comprehension

for (x <- e if f; s) yield e’

onde f é um filtro e s é uma (possivelmente vazia) sequência de geradores oufiltros, é traduzida para

for (x <- e.filter(x => f); s) yield e’

e então, a tradução continua com a última expressão.

• Um for-comprehension

for (x <- e; y <- e’; s) yield e’’

onde s é uma (possivelmente vazia) sequência de geradores ou filtros étraduzida para

e.flatMap(x => for (y <- e’; s) yield e’’)

e então, a tradução continua com a última expressão.

Por exemplo, tomando nosso exemplo “pares de inteiros cuja soma é um primo”:

for { i <- range(1, n)j <- range(1, i)if isPrime(i+j)

} yield {i, j}

Aqui está o que obtemos quando traduzimos esta expressão:

range(1, n).flatMap(i =>range(1, i).filter(j => isPrime(i+j)).map(j => (i, j)))

De modo inverso, também seria possível expressar as funções map, flatMap e filterusando for-comprehensions. Aqui estão as três funções novamente, desta vezimplementadas usando for-comprehensions.

object Demo {def map[A, B](xs: List[A], f: A => B): List[B] =

86 For-Comprehensions

for (x <- xs) yield f(x)

def flatMap[A, B](xs: List[A], f: A => List[B]): List[B] =for (x <- xs; y <- f(x)) yield y

def filter[A](xs: List[A], p: A => Boolean): List[A] =for (x <- xs if p(x)) yield x

}

Não surpreendentemente, a tradução do for-comprehension no corpo de Demo.map

produzirá uma chamada para map na classe List. Similarmente, Demo.flatMap eDemo.filter são traduzidos para flatMap e filter na classe List.

Exercício 10.3.1 Defina a seguinte função em termos de for.

def flatten[A](xss: List[List[A]]): List[A] =(xss :\ (Nil: List[A])) ((xs, ys) => xs ::: ys)

Exercício 10.3.2 Traduza

for (b <- books; a <- b.authors if a startsWith "Bird") yield b.titlefor (b <- books if (b.title indexOf "Program") >= 0) yield b.title

para funções de alta ordem.

10.4 Laços For

For-comprehensions lembram os laços for das linguagens imperativas, exceto queeles produzem uma lista de resultados. Algumas vezes, uma lista de resultadosnão é necessária, mas ainda gostaríamos da flexibilidade dos geradores e filtrosnas iterações sobre listas. Isso é possível por uma variante da sintaxe dosfor-comprehensions, os quais expressam laços for:

for ( s ) e

Esta construção é a mesma da sintaxe padrão do for-comprehension, exceto quea palavra chave yield está faltando. O laço for é executado pela execução daexpressão e para cada elemento gerado da sequência de geradores e filtros s.

Como um exemplo, a seguinte expressão imprime todos os elementos de umamatriz representada como uma lista de listas:

for (xs <- xss) {for (x <- xs) print(x + "\t")println()

}

10.5 Generalizando For 87

A tradução de laços for para métodos de alta ordem da classe List é similar àtradução de for-comprehensions, mas mais simples. Onde for-comprehensions sãotraduzidos para map e flatMap, laços for traduzem em cada caso para foreach.

10.5 Generalizando For

Temos visto que a tradução de for-comprehensions somente ocorre na presençados métodos map, flatMap, e filter. Portanto é possível aplicar a mesma notaçãopara geradores que produzam outros objetos além de listas; tais objetos somentetem de atender as três funções-chave map, flatMap, e filter.

A biblioteca padrão Scala tem diveras outras abstrações que suportam estestrês métodos e com eles suportam for-comprehensions. Encontraremos algunsdeles nos capítulos seguintes. Como programador você também pode usar esteprincípio para habilitar for-comprehensions para tipos que você definiu—estestipos somente precisam suportar os métodos map, flatMap, e filter.

Há muitos exemplos onde isto é útil: interfaces de base de dados, árvores XML, ouvalores opcionais. Uma advertência: não há garantia automática que a traduçãoresultante de um for-comprehension seja bem tipada. Para garantir isso, os tiposde map, flatMap, e filter tem de ser essencialmente similares aos tipos dessesmétodos na classe List. Para tornar isto preciso, assuma que você tenha uma classeparametrizada C[A] para a qual você deseja habilitar for-comprehensions. Então C

deve definir map, flatMap, e filter com os seguintes tipos:

def map[B](f: A => B): C[B]def flatMap[B](f: A => C[B]): C[B]def filter(p: A => Boolean): C[A]

Seria atrativo forçar estes tipos estáticamente dentro do compilador Scala,por exemplo, requerendo que qualquer tipo que suporte for-comprehensionsimplemente um trait padrão com estes métodos 1. O problema é que tal trait padrãotem de abstrair sobre a identidade da classe C, por exemplo tomando C como umtipo parâmetro. Observe que este parâmetro deve ser um tipo construtor, queé aplicado a diversos diferentes tipos nas assinaturas dos métodos map e flatMap.Infelizmente, o sistema de tipos Scala é muito fraco para expressar este construtor,desde que pode lidar somente com tipos parâmetros que são tipos totalmenteaplicados.

1Na linguagem de programação Haskell, que tem construtores similares, esta abstração échamada um “monada com zero”

Capítulo 11

Estados Mutáveis

A maioria dos programas apresentados até o momento não possui efeitos colaterais.1. Portanto, a noção de tempo não importou.

Para um programa que termina, qualquer sequência de ações levará ao mesmoresultado! Isso também é refletido pelo modelo de computação de substituição,onde um passo de reescrita pode ser aplicado em qualquer lugar de um termo, etodas as reescritas que terminam levam a mesma solução. De fato, esta propriedadede confluência é um profundo resultado do cálculo λ, a teoria que embasa aprogramação funcional.

Neste capítulo, introduziremos funções com efeitos colaterais e estudaremos seucomportamento. Veremos que como consequência temos fundamentalmente quemodificar o modelo de substituição de computação empregado até aqui.

11.1 Objetos Mutáveis

Normalmente vemos o mundo como um conjunto de objetos, alguns dos quaiscom estado que muda através do tempo. Normalmente, o estado é associado comum conjunto de variáveis que podem ser alteradas no curso de uma computação.Há também uma noção mais abstrata de estado, que não se refere a construçõesparticulares de uma linguagem de programação: um objeto tem estado (ou: émutável) se seu comportamento é influenciado por sua história.

Por exemplo, uma objeto conta de banco tem estado, porque a questão “Eu possosacar R$ 100?” pode ter diferentes respostas durante o tempo de vida da conta.

Em Scala, todos os estados mutáveis são em última análise criados a partir dasvariáveis. Uma definição de variável é escrita como uma definição de valor, mas

1Ignoramos o fato de que algums dos programas imprimem na tela, o que, tecnicamente, é umefeito colateral.

90 Estados Mutáveis

começa com var ao invés de val. Por exemplo, os duas definições seguintesintroduzem e inicializam duas variáveis x e count.

var x: String = "abc"var count = 111

Como uma definição de valor, uma definição de variável associa um nome comum valor. Mas no caso da definição de variável, esta associação pode ser alteradaposteriormente através de uma atribuição. Tais atribuições são escritas em C ouJava. Exemplos:

x = "hello"count = count + 1

Em Scala, cada variável definida tem de ser inicializada no ponto de sua definição.Por exemplo, a declaração var x: Int; não é tida como uma definição de variável,porque o inicializador está ausente2. Se não se sabe, ou não é importante, oinicializador apropriado, pode-se usar um caracter coringa no lugar. Ou seja,

val x: T = _

inicializará x para algum valor default (null para tipos referência, false paraboleanos, e a versão apropriada de 0 para valores de tipos numéricos).

Objetos do mundo real com estado são representados em Scala através de objetosque tem variáveis como membros. Por exemplo, aqui está uma classe querepresenta contas bancárias.

class BankAccount {private var balance = 0def deposit(amount: Int) {if (amount > 0) balance += amount

}

def withdraw(amount: Int): Int =if (0 < amount && amount <= balance) {balance -= amountbalance

} else error("insufficient funds")}

A classe define uma variável balance que contém o balanço corrente de uma conta.Métodos deposit e whithdraw mudam o valor desta variável através de atribuições.Observe que balance e private na classe BankAccount – consequentemente não

2Se uma declaração como esta aparece numa classe, é tida como uma declaração de variável,que introduz métodos de acesso abstratos para a variável, mas não associa estes métodos com umpedaço do estado.

11.1 Objetos Mutáveis 91

pode ser acessado diretamente fora da classe. Para criar contas bancárias, usamosa notação usual para criação de objeto:

val myAccount = new BankAccount

Exemplo 11.1.1 Aqui está uma sessão scalaint que lida com contas bancárias.

scala> :l bankaccount.scalaLoading bankaccount.scala...defined class BankAccountscala> val account = new BankAccountaccount: BankAccount = BankAccount$class@1797795scala> account deposit 50unnamed0: Unit = ()scala> account withdraw 20unnamed1: Int = 30scala> account withdraw 20unnamed2: Int = 10scala> account withdraw 15java.lang.Error: insufficient funds

at scala.Predef$error(Predef.scala:74)at BankAccount$class.withdraw(<console>:14)at <init>(<console>:5)

scala>

O exemplo mostra que aplicando a mesma operação (withdraw 20) duas vezespara uma conta gera resultados diferentes. Então, claramente, contas são objetosdinâmicos.

Constância e Alteração. Atribuições colocam novos problemas na decisão dequando duas expressões são “a mesma”. Se atribuições são excluídas, e escrevemos

val x = E; val y = E

onde E é alguma expressão arbitrária, então x e y podem ser assumidosrazoavelmente como sendo o mesmo. Ou seja, poderia-se equivalentementeescrever

val x = E; val y = x

(Esta propriedade é geralmente chamada transparência referencial). Mas umavez que admitimos atribuições, as duas sequências de definições são diferentes.Considere:

val x = new BankAccount; val y = new BankAccount

92 Estados Mutáveis

Para responder a questão se x e y são o mesmo, precisamos ser mais precisos sobreo que significa constância. Este significado é capturado na noção de equivalênciaoperacional, a qual, de modo um tanto quanto informal, é descrita como segue.

Suponha que temos duas definições para x e y. Para testar se x e y definem o mesmovalor, proceda como segue.

• Execute as definições seguidas para uma sequência arbitrária S de operaçõesque envolvam x e y. Observe o resultado (se houver).

• Então, execute as definições com uma outra sequência S’ resultante de S pelarenomeação de todas as ocorrências de y em S para x.

• Se os resultados da execução de S’ forem diferentes, então certamente x e y

são diferentes.

• Por outro lado, se todos os possíveis pares de sequências {S, S’} dão osmesmos resultados, então x e y são o mesmo.

Em outras palavras, equivalência operacional levam duas definições x e y comodefinindo o mesmo valor, se nenhum experimento possível puder distinguir entrex e y. Um experimento neste contexto são duas versões de um programa arbitrárioque usa ou x ou y.

Dada esta definição, vamos testar se

val x = new BankAccount; val y = new BankAccount

definem valores x e y que são o mesmo. Aqui estão as definições novamente,seguidas por uma sequência de testes:

> val x = new BankAccount> val y = new BankAccount> x deposit 3030> y withdraw 20java.lang.RuntimeException: insufficient funds

Agora, renomeie todas as ocorrências de y naquela sequência para x. Obtemos:

> val x = new BankAccount> val y = new BankAccount> x deposit 3030> x withdraw 2010

Como os resultados finais são diferentes, estabelecemos que x e y não são o mesmo.Por outro lado, se definimos

11.2 Estruturas Imperativas de Controle 93

val x = new BankAccount; val y = x

então nenhuma sequência de operações pode distiguir entre x e y, portanto x e y

são o mesmo neste caso.

Atribuição e o Modelo de Substituição. Estes exemplos mostram que nossomodelo de substituição computacional não mais pode ser usado. Afinal de contas,sob este modelo nem sempre podemos substituir um valor de nome por suaexpressão definida. Por exemplo em

val x = new BankAccount; val y = x

o x na definição de y pode ser substituído por new BankAccount. Mas vimos queesta alteração leva a programas distintos. Então o modelo de substituição deve serinválido, uma vez que adicionemos atribuições.

11.2 Estruturas Imperativas de Controle

Scala tem construções de laços while e do-while derivadas das linguagens deprogramação C e Java. Também há uma ramificação única if que deixa defora a parte “else”, bem como uma declaração return que aborta uma funçãoprematuramente. Isso torna possível programar no estilo imperativo convencional.Por exemplo, a seguinte função, que computa a enésima potência de um dadoparâmetro x, é implementada usando while e um if simples.

def power(x: Double, n: Int): Double = {var r = 1.0var i = nvar j = 0while (j < 32) {r = r * rif (i < 0)r *= x

i = i << 1j += 1

}r

}

Estas construções de controle imperativo estão na linguagem por conveniência.Poderiam ser deixadas de fora, pois as mesmas construções podem serimplementadas usando apenas funções. Como exemplo, vamos desenvolver umaimplementação funcional para o laço while. whileLoop deve ser uma função querecebe dois parâmetros: uma condição, de tipo Boolean, e um comando, de tipo

94 Estados Mutáveis

Unit. Ambos, condição e comando, devem ser passados por nome, dado que serãoavaliados repetidamente para cada iteração do laço. Isso leva a seguinte definiçãopara whileLoop.

def whileLoop(condition: => Boolean)(command: => Unit) {if (condition) {command; whileLoop(condition)(command)

} else ()}

Observe que WhileLoop é uma recursão de cauda, logo opera em um espaço de pilhaconstante.

Exercício 11.2.1 Escreva uma função repeatLoop, que deve ser aplicada comosegue:

repeatLoop { command } ( condition )

Há também um modo de se obter uma sintaxe de laço como a seguinte?

repeatLoop { command } until ( condition )

Algumas outras construções de controle conhecidas do C e do Java estão ausentesem Scala: não há break e continue. Também não há laços “for” similares ao Java –estes foram substituídos por uma construção de laço “for” mais geral, discutida naSeção 10.4.

11.3 Exemplo Estendido: Simulação de Eventos Discretos

Agora discutiremos um exemplo que demonstra como atribuições e funções dealta ordem podem ser combinados de maneiras interessantes. Construiremos umsimulador para circuitos digitais.

O exemplo é emprestado do livro do Abelson e Sussman [ASS96]. Nós expandimos ocódigo básico (Scheme-) através de uma estrutura orientada a objetos que permitereuso de código por meio de herança. O exemplo também mostra como programasde simulação de eventos discretos em geral são estruturados e construídos.

Começamos com uma pequena linguagem para descrever circuitos digitais. Umcircuito digital é criado a partir de fios e caixas função. Fios carregam sinais quesão transformados por caixas função. Representaremos sinais pelos boleanos truee false.

Caixas função básicas (ou: portas) são:

• Um inversor, o qual nega seu sinal.

11.3 Exemplo Estendido: Simulação de Eventos Discretos 95

• Uma porta E, o qual determina sua saída com base na conjunção da suaentrada.

• Uma porta OU, o qual determina sua saída com base na disjunção da suaentrada.

Outras portas lógicas pode ser construídas pela combinação das portas básicas.

Portas tem delays, portanto a saída de uma porta somente mudará algum tempoapós a mudança da sua entrada.

Uma Linguagem para Circuitos Digitais. Descrevemos os elementos de umcircuito digital através do seguinte conjunto de classes e funções Scala.

Primeiro, há uma classe Wire para fios. Podemos construir fios como segue.

val a = new Wireval b = new Wireval c = new Wire

Segundo, há procedimentos

def inverter(input: Wire, output: Wire)def andGate(a1: Wire, a2: Wire, output: Wire)def orGate(o1: Wire, o2: Wire, output: Wire)

que “criam” as portas lógicas básicas que precisamos (como efeitos colaterais).Portas mais complicadas pode agora ser construídas a partir destas. Por exemplo,para construir um circuito meia soma, podemos definir:

def halfAdder(a: Wire, b: Wire, s: Wire, c: Wire) {val d = new Wireval e = new WireorGate(a, b, d)andGate(a, b, c)inverter(c, e)andGate(d, e, s)

}

Esta abstração pode ela mesma ser usada, por exemplo, na definição de um circuitosoma completo:

def fullAdder(a: Wire, b: Wire, cin: Wire, sum: Wire, cout: Wire) {val s = new Wireval c1 = new Wireval c2 = new WirehalfAdder(a, cin, s, c1)halfAdder(b, s, sum, c2)

96 Estados Mutáveis

orGate(c1, c2, cout)}

A classe Wire e as funções inverter, andGate, e orGate representam, portanto, umapequena linguagem na qual usuários podem definir circuitos digitais. Agora damosimplementações destas classes e funções, que nos permite simalar circuitos. Essaimplementações são baseadas numa API simples e geral para simulação de eventosdiscretos.

A API Simulação. A simulação de eventos discretos realiza ações definidas pelousuário em tempos específicos. Uma ação é representada como uma função quenão recebe parâmetros e retorna um resultado Unit:

type Action = () => Unit

O tempo é simulado; não é o horário atual do “relógio de parede”.

Uma simulação concreta será efetuada dentro do objeto que herda a classe abstrataSimulation. Esta classe tem a seguinte assinatura:

abstract class Simulation {def currentTime: Intdef afterDelay(delay: Int, action: => Action)def run()

}

Aqui, currentTime retorna o tempo corrente como um número inteiro, afterDelayagenda uma ação para ser realizada com um atraso específico após currentTime, erun executa a simulação até que não haja mais ações a serem realizadas.

A Classe Fio (Wire). Um fio precisa atender a três ações básicas.

getSignal: Boolean retorna o sinal corrente sobre o fio.

setSignal(sig: Boolean) atualiza o sinal do fio para sig.

addAction(p: Action) junta o procedimento especificado p nas ações do fio.Todos os procedimentos juntados às ações serão executados toda vez que osinal do fio mudar.

Aqui está uma implementação da classe Wire (fio):

class Wire {private var sigVal = falseprivate var actions: List[Action] = List()def getSignal = sigValdef setSignal(s: Boolean) =

11.3 Exemplo Estendido: Simulação de Eventos Discretos 97

if (s != sigVal) {sigVal = sactions.foreach(action => action())

}def addAction(a: Action) {actions = a :: actions; a()

}}

Duas variáveis privadas compõem o estado de um fio. A variável sigVal denota osinal corrente, e a variável actions denota os procedimentos de ação atualmentejuntados ao fio.

A Classe Inversor. Implementamos um inversor instalando uma ação ao seu fiode entrada, ou seja, a ação que coloca a entrada negada sobre o sinal de saída.A ação precisa ter efeito nas unidades de tempo simulado InverterDelay após aentrada mudar. Isso sugere a seguinte implementação:

def inverter(input: Wire, output: Wire) {def invertAction() {val inputSig = input.getSignalafterDelay(InverterDelay) { output setSignal !inputSig }

}input addAction invertAction

}

A Classe And-Gate (Porta E). Portas E são implementadas analogamente ainversores. A ação de uma andGate é criar na saída a conjunção dos seus sinaisde entrada. Isso deve ocorrer nas unidades de tempo simulado AndGateDelay apósqualquer de suas duas entradas mudar. Consequentemente, temos a seguinteimplementação:

def andGate(a1: Wire, a2: Wire, output: Wire) {def andAction() {val a1Sig = a1.getSignalval a2Sig = a2.getSignalafterDelay(AndGateDelay) { output setSignal (a1Sig & a2Sig) }

}a1 addAction andActiona2 addAction andAction

}

Exercício 11.3.1 Escreva a implementação da porta lógica OU (orGate).

98 Estados Mutáveis

Exercício 11.3.2 Um outro modo de se definir uma porta OU é pela combinação deinversores e portas E. Defina uma função orGate em termos de andGate e inverter.Qual o tempo de atraso desta função?

A Classe Simulação. Agora, só falta implementar a classe Simulation. A idéia émanter dentro de um objeto Simulation uma agenda de ações a serem realizadas.A agenda é representada como uma lista de pares de ações e os tempos a seremexecutadas. A lista agenda é ordenada, portanto ações que devem ocorrer mais cêdoprecedem as que devem ocorrer depois.

abstract class Simulation {case class WorkItem(time: Int, action: Action)private type Agenda = List[WorkItem]private var agenda: Agenda = List()

Há também uma variável privada curtime para acompanhar o tempo simuladocorrente.

private var curtime = 0

Uma aplicação do método afterDelay(delay, block) insere o elementoWorkItem(currentTime + delay, () => block) dentro da lista agenda no lugarapropriado.

private def insert(ag: Agenda, item: WorkItem): Agenda =if (ag.isEmpty || item.time < ag.head.time) item :: agelse ag.head :: insert(ag.tail, item)

def afterDelay(delay: Int)(block: => Unit) {val item = WorkItem(currentTime + delay, () => block)agenda = insert(agenda, item)

}

Uma aplicação para o método run remove sucessivos elementos da agenda e realizasuas ações. Continua até que a agenda esteja vazia:

private def next() {agenda match {case WorkItem(time, action) :: rest =>agenda = rest; curtime = time; action()

case List() =>}

}

def run() {afterDelay(0) { println("*** simulation started ***") }

11.4 Sumário 99

while (!agenda.isEmpty) next()}

Executando o Simulador. Para executar o simulador, ainda precisamos de ummodo de inspecionar mudanças de sinais sobre os fios. Para este propósito,escrevemos uma função probe (sonda).

def probe(name: String, wire: Wire) {wire addAction { () =>println(name + " " + currentTime + " new_value = " + wire.getSignal)

}}

Agora, para vermos o simulador em ação, vamos definir quatro fios, e colocar duassondas sobre dois deles:

scala> val input1, input2, sum, carry = new Wire

scala> probe("sum", sum)sum 0 new_value = false

scala> probe("carry", carry)carry 0 new_value = false

Agora vamos definir um circuito meia soma conectando os fios:

scala> halfAdder(input1, input2, sum, carry)

Finalmente, setamos um após o outro os sinais sobre os dois fios de entrada paratrue e rodamos a simulação.

scala> input1 setSignal true; run

*** simulation started ***sum 8 new_value = true

scala> input2 setSignal true; runcarry 11 new_value = truesum 15 new_value = false

11.4 Sumário

Vimos neste capítulo as construções que nos permitiram modelar estados em Scala– que são variáveis, atribuições, e estruturas de controle imperativo. Estados eAtribuições complicam nosso modelo mental de computação. Em particular, a

100 Estados Mutáveis

transparência referencial é perdida. Por outro lado, atribuição nos dá novos meiospara formular programas elegantemente. Como sempre, isso depende de qualfunciona melhor para uma dada situação: programas puramente funcionais ouprogramas com atribuições.

Capítulo 12

Computando com Streams

Os capítulos anteriores apresentaram variáveis, atribuição e objetos com estado.Vimos como objetos no mundo real mudam com o tempo e podem ser modeladosatravés da mudança de estado das suas variáveis durante a computação. Destamaneira, mudanças no tempo no mundo real são modeladas por mudanças notempo durante a execução do programa. Obviamente, usa-se uma escala e estasmudanças no tempo são alargadas ou comprimidas, mas sua ordem relativapermanece a mesma. Isto pode parecer natural, mas existe um preço a se pagar:o modelo de substituição usado na computação funcional não pode ser aplicado aose introduzir variáveis e atribuições.

Será que não existe uma outra maneira? Não seria possível modelar mudanças deestado no mundo real usando somente funções imutáveis? Tomando matemáticacomo um guia, a resposta claramente é sim: um quantidade que muda no tempoé modelada através da função f(t) que tem como parâmetro t para representaro tempo. O mesmo pode ser feito na computação. Ao invés de sobre-escreveruma variável com sucessivos valores, podemos representar estes valores comosucessivos elementos em uma lista. Desta forma, a variável mutável var x: T

pode ser substituída por uma variável imutável val x: List[T]. De certa maneira,troca-se espaço por tempo – os diferentes valores da variável agora existemconcorrentemente como diferentes elementos da lista. Uma das vantagens domodelo baseado em lista é a possibilidade de “viajar no tempo”, ou seja, de versucessivos valores da variável ao mesmo tempo. Uma outra vantagem é quepodemos fazer uso da poderosa biblioteca de funções processamento de listas, quenormalmente simplifica a computação. Por exemplo, considere a forma imperativade computar a soma de todos os primos em um intervalo:

def sumPrimes(start: Int, end: Int): Int = {var i = startvar acc = 0while (i < end) {

102 Computando com Streams

if (isPrime(i)) acc += ii += 1

}acc

}

Note que a variável i “passa por” todos os valores do intervalo [start .. end-1].

Uma forma mais funcional, é representar a lista de valores da variável i diretamentecomo range(start, end). Então a função pode ser re-escrita da seguinte maneira:

def sumPrimes(start: Int, end: Int) =sum(range(start, end) filter isPrime)

Não dúvida que o programa é mais curto e mais claro! Porém, o programa funcionalé também consideravelmente menos eficiente, uma vez que ele constrói a lista detodos os número no intervalo e, posteriormente, constrói uma outra lista com osnúmeros primos. Ainda pior sob o ponto de vista da eficiência é o seguinte exemplo:

Para encontrar o segundo número primio entre 1000 e 10000:

range(1000, 10000) filter isPrime at 1

Aqui, a lista de todos os números entre 1000 e 10000 é construída, mas a maior parteda lista nunca é usada!

No entanto, pode-se obter uma execução eficiente para exemplos como este usandoum truque:

Nunca compute o resto (tail) de uma sequência a não ser que o restoseja realmente necessário para a computação.

Para isso, definimos uma nova classe para sequências, que é chamada Stream.

Streams são criados usando a constante empty e o construtor cons, ambos definidosno módulo scala.Stream. Por exemplo, a seguinte expressão constrói um streamcomo os elementos 1 e 2:

Stream.cons(1, Stream.cons(2, Stream.empty))

Como um outro exemplo, este é o análogo de List.range, mas que retorna umstream ao invés de uma lista:

def range(start: Int, end: Int): Stream[Int] =if (start >= end) Stream.emptyelse Stream.cons(start, range(start + 1, end))

(Esta função também foi definida como mostrado anteriormente no móduloStream). Embora Stream.range e List.range se pareçam, seu comportamento emtempo de execução é completamente diferente:

103

Stream.range retorna imediatamente um objeto do tipo Stream cujo primeiroelemento é start. Todos os outros elementos são computados somente quandoeles são demandados através da chamada do método tail (o que pode nuncaacontecer).

Streams são acessados exatamente como listas. Similarmente às listas, os métodosbásicos de acesso são isEmpty, head e tail. Por exemplo, podemos imprimir todosos elementos de um stream da seguinte maneira.

def print(xs: Stream[A]) {if (!xs.isEmpty) { Console.println(xs.head); print(xs.tail) }

}

Streams também oferecem praticamente todos os outros métodos definidos naslistas (veja a seguir onde os conjuntos de métodos são diferentes). Por exemplo,podemos encontrar o segundo número primo entre 1000 e 10000 através do usodos métodos filter e apply no stream que fornece o intervalo:

Stream.range(1000, 10000) filter isPrime at 1

A diferença entre a implementação anterior que usada lista, é que agora nãoconstruímos nem testamos se é primo nenhum número maior que 1013.

Retorno e concatenação de streams. Há dois métodos na classe List que nãoestão presentes na classe Stream. São eles :: e :::. O motivo é que estes métodossão chamados pelo argumento da direita, o que significa que este argumentoprecisa ser computado antes que o método seja chamado. Por exemplo, no casode x :: xs nas listas, o resto xs precisa ser computado antes de :: ser chamadoe a nova lista ser construída. Isto não funciona para streams, onde queremos queo resto de um stream não seja computado até que seja demandado pela chamadaao método tail. O argumento pelo qual a concatenação de listas ::: não pode seradaptado para streams é análogo.

Ao invés de x :: xs, pode-se usar Stream.cons(x, xs) para construir um streamcom o primeiro elemento x e o resto (não computado). Ao invés de xs ::: ys,pode-se usar xs append ys.

Capítulo 13

Iteradores

Iteradores são uma versão imperativa dos streams. Assim como streams, iteradoresdescrevem listas potencialmente infinitas. No entanto, não existe uma estruturade dados que contenha os elementos de um iterador. Ao invés disso, os iteradorespermitem que se ande somente um passo na sequência de cada vez, usando osmétodos abstratos next e hasNext.

trait Iterator[+A] {def hasNext: Booleandef next: A

O método next retorna sucessivos elementos. O método hasNext indica se existemmais elementos a serem retornados por next. Iteradores também possuem outrosmétodos que serão explicados posteriormente.

Como exemplo, esta é uma aplicação que imprime os quadrados de todos osnúmeros de 1 à 100.

val it: Iterator[Int] = Iterator.range(1, 100)while (it.hasNext) {val x = it.nextprintln(x * x)

}

13.1 Métodos dos Iteradores

Os iteradores possuem um conjunto de métodos além de next e hasNext, quesão descritos a seguir. Muitos destes métodos oferecem uma funcionalidadecorrespondente à do método equivalente em uma lista.

106 Iteradores

Append. O método append constrói uma iterador que começa com o código dadode iteração it depois que o iterador atual tem terminado.

def append[B >: A](that: Iterator[B]): Iterator[B] = new Iterator[B] {def hasNext = Iterator.this.hasNext || that.hasNextdef next = if (Iterator.this.hasNext) Iterator.this.next else that.next

}

Os termos Iterator.this.next e Iterator.this.hasNext na definição de append

chamam os métodos correspondentes definidos na classe Iterator que contém ométodo append. Se o prefixo Iterator não fosse adicionado ao this, hasNext e next

chamariam recursivamente os métodos definidos no resultado de append, o que nãoera o efeito desejado.

Map, FlatMap, Foreach. O método map constrói um iterador que retorna todos oselementos do iterador original transformados por uma dada função f.

def map[B](f: A => B): Iterator[B] = new Iterator[B] {def hasNext = Iterator.this.hasNextdef next = f(Iterator.this.next)

}

O método flatMap é similar ao método map, exceto que a função de transformação f

retorna um iterador ao invés de um elemento. O resultado de flatMap é um iteradorresultante da anexação (append) de todos os iteradores resultantes das sucessivaschamadas de f.

def flatMap[B](f: A => Iterator[B]): Iterator[B] = new Iterator[B] {private var cur: Iterator[B] = Iterator.emptydef hasNext: Boolean =if (cur.hasNext) trueelse if (Iterator.this.hasNext) { cur = f(Iterator.this.next); hasNext }else false

def next: B =if (cur.hasNext) cur.nextelse if (Iterator.this.hasNext) { cur = f(Iterator.this.next); next }else error("next on empty iterator")

}

Também relacionado à map, temos o método foreach que aplica uma dada funçãoa todos elementos de um iterador, mas não constrói uma lista de resultados

def foreach(f: A => Unit): Unit =while (hasNext) { f(next) }

13.1 Métodos dos Iteradores 107

Filter. O método filter constrói um iterador que retorna todos os elementos doiterador original que satisfazem um dado critério p.

def filter(p: A => Boolean) = new BufferedIterator[A] {private val source =Iterator.this.buffered

private def skip ={ while (source.hasNext && !p(source.head)) { source.next } }

def hasNext: Boolean ={ skip; source.hasNext }

def next: A ={ skip; source.next }

def head: A ={ skip; source.head }

}

Na prática, filter retorna uma instância de uma sub-classe dos iteradores quetrabalha com um “buffer”. Um objeto do tipo BufferedIterator é um iterador quetem adicionalmente um método head. Este método retorna um elemento que seriaretornado pelo método next, porém não avança além daquele elemento. Com isso,o elemento retornado por head é retornado novamente pela próxima chamada dehead ou next. Esta é a definição do trait BufferedIterator:

trait BufferedIterator[+A] extends Iterator[A] {def head: A

}

Como map, flatMap, filter e foreach existem para iteradores, como consequênciafor-comprehensions e loops de for também podem ser usados em iteradores. Porexemplo, a aplicação que imprime os quadrados dos números entre 1 e 100 poderiater sido expressada da seguinte forma:

for (i <- Iterator.range(1, 100))println(i * i)

Zip. O método zip recebe um outro iterador e retorna um iterador que consistede pares dos respectivos elementos retornados pelos dois iteradores.

def zip[B](that: Iterator[B]) = new Iterator[(A, B)] {def hasNext = Iterator.this.hasNext && that.hasNextdef next = (Iterator.this.next, that.next)

}}

108 Iteradores

13.2 Construindo Iteradores

As classes concretas de iteradores precisam implementar os dois métodos abstratosnext e hasNext definidos na classe Iterator. O iterador mais simples éIterator.empty que sempre retorna uma sequência vazia:

object Iterator {object empty extends Iterator[Nothing] {def hasNext = falsedef next = error("next on empty iterator")

}

Um iterador um pouco mais interessante enumera todos os elementos de um vetor.Este iterador é construído a partir do método fromArray, que também foi definidono objeto Iterator

def fromArray[A](xs: Array[A]) = new Iterator[A] {private var i = 0def hasNext: Boolean =i < xs.length

def next: A =if (i < xs.length) { val x = xs(i); i += 1; x }else error("next on empty iterator")

}

Um outro iterador enumera um intervalo de inteiros. A função Iterator.range

retorna o iterador que caminha em um intervalo dado de valores inteiros. Suadefinição é a seguinte:

object Iterator {def range(start: Int, end: Int) = new Iterator[Int] {private var current = startdef hasNext = current < enddef next = {val r = currentif (current < end) current += 1else error("end of iterator")r

}}

}

Todos os iteradores mostrados até agora tem um fim. Também é possível definiriteradores que nunca terminam. Por exemplo, o iterador a seguir retorna inteirossucessivos a partir de um valor inicial. 1.

1Como a representação do tipo int é finita, os números acabaram em 231.

13.3 Usando Iteradores 109

def from(start: Int) = new Iterator[Int] {private var last = start - 1def hasNext = truedef next = { last += 1; last }

}

13.3 Usando Iteradores

Existem mais 2 exemplos de como os iteradores são usados. Primeiro, paraimprimir todos os elementos de um vetor xs: Array[Int], uma pessoa podeescrever:

Iterator.fromArray(xs) foreach (x => println(x))

Ou, usando for-comprehension:

for (x <- Iterator.fromArray(xs))println(x)

Como um segundo exemplo, considere o problema de encontrar os índices de todosos elementos de um vetor de doubles maiores que um dado limit. Os índicesdevem ser retornado na forma de um iterador. Isto pode ser obtido pela expressão:

import Iterator._fromArray(xs).zip(from(0)).filter(case (x, i) => x > limit).map(case (x, i) => i)

Ou usando for-comprehension:

import Iterator._for ((x, i) <- fromArray(xs) zip from(0); x > limit)yield i

Capítulo 14

Valores Preguiçosos (Lazy)

Valores preguiçosos oferecem uma maneira de postergar a inicialização de um valoraté o primeiro momento em que seja acessado. Isto pode ser útil quando estiverlidando com valores que podem não ser necessários durante a execução e cujocusto computacional seja significativo. Como primeiro exemplo, vamos considerarum banco de dados de empregados contendo cada empregado e seu gestor e suaequipe.

case class Employee(id: Int,name: String,managerId: Int) {

val manager: Employee = Db.get(managerId)val team: List[Employee] = Db.team(id)

}

A classe Employee dada anteriormente irá tentar inicializar todos os seus campos,carregando todoa a tabela de empregados na memória. Isto certamente não éo ideal e pode ser melhorado como facilidade tornando os campos preguiçosos.Desta forma, atrasamos o acesso ao banco de dados até o momento em que ele sejarealmente necessário, se isto ocorrer.

case class Employee(id: Int,name: String,managerId: Int) {

lazy val manager: Employee = Db.get(managerId)lazy val team: List[Employee] = Db.team(id)

}

Para ver o que realmente está acontecendo, podemos usar este banco de dadosmock que mostra quando os registros são acessados:

object Db {

112 Valores Preguiçosos (Lazy)

val table = Map(1 -> (1, "Haruki Murakami", -1),2 -> (2, "Milan Kundera", 1),3 -> (3, "Jeffrey Eugenides", 1),4 -> (4, "Mario Vargas Llosa", 1),5 -> (5, "Julian Barnes", 2))

def team(id: Int) = {for (rec <- table.values.toList; if rec._3 == id)yield recToEmployee(rec)

}

def get(id: Int) = recToEmployee(table(id))

private def recToEmployee(rec: (Int, String, Int)) = {println("[db] fetching " + rec._1)Employee(rec._1, rec._2, rec._3)

}}

Ao rodar o programa, a saída confirma que ele retorna um empregado e que o bancosomente é acessado quando é feita uma referência ao valor preguiçoso.

Um outro uso dos valores preguiçosos é para resolver a ordem de inicialização deaplicações compostas de muitos módulos. Antes dos valores preguiçosos seremcriados, o mesmo efeito era conquistado usando definições do tipo object. Comoum segundo exemplo considere um compilador composto de diversos módulos.Olhamos primeiro para uma tabela de símbolos que definie uma classe parasímbolos e duas funções pré-definidas.

class Symbols(val compiler: Compiler) {import compiler.types._

val Add = new Symbol("+", FunType(List(IntType, IntType), IntType))val Sub = new Symbol("-", FunType(List(IntType, IntType), IntType))

class Symbol(name: String, tpe: Type) {override def toString = name + ": " + tpe

}}

O módulo Symbols é parametrizado com uma instância de Compiler que permiteo acesso a outros serviços, tais como o módulo de tipos. Em nosso exemplo,há somente duas funções pré-definidas, adição e subtração e suas definiçõesdependem do módulo types.

class Types(val compiler: Compiler) {import compiler.symtab._

113

abstract class Typecase class FunType(args: List[Type], res: Type) extends Typecase class NamedType(sym: Symbol) extends Typecase object IntType extends Type

}

Para conectar os dois componentes, um objeto do tipo compilador é criado epassado com parâmetro para os dois componentes.

class Compiler {val symtab = new Symbols(this)val types = new Types(this)

}

Infelizmente, esta abordagem falha em tempo de execução, pois o módulo symtab

depende do módulo types. De maneira geral, a dependência entre os módulospode ficar complicada e conseguir a ordem correta de inicialização é difícil ou,até mesmo, impossível, quando existem dependências cíclicas. A maneira maissimples de corrigir este erro é tornar estes campos lazy e deixar o compiladordescobrir qual é a ordem correta de inicialização.

class Compiler {lazy val symtab = new Symbols(this)lazy val types = new Types(this)

}

Aogra os dois módulos são inicializados no primeiro acesso e o compilador podeexecutar da forma esperada.

Sintaxe

O modificador lazy é permitido apenas na definição de valores concretos. Todasas regras válidas para definição de valores se aplicam também para valores do tipolazy, com uma restrição a menos: valores locais recursivos são permitidos.

Capítulo 15

Parâmetros Implícitos eConversões

Parâmetros implícitos e conversões são ferramentas poderosas para personalizarbibliotecas existentes e para criar abstrações de alto-nível. Como exemplo, vamoscomeçar com uma classe abstrata SemiGroup que contém uma operação nãoespecificada chamada add.

abstract class SemiGroup[A] {def add(x: A, y: A): A

}

Aqui está a sub-classe abstrata Monoid que herda de SemiGroup e inclui um novoelemento unit.

abstract class Monoid[A] extends SemiGroup[A] {def unit: A

}

Aqui estão duas implementações de Monoid:

object stringMonoid extends Monoid[String] {def add(x: String, y: String): String = x.concat(y)def unit: String = ""

}

object intMonoid extends Monoid[Int] {def add(x: Int, y: Int): Int = x + ydef unit: Int = 0

}

116 Parâmetros Implícitos e Conversões

O método sum, que funciona com monoids arbitrários pode ser escrito em Scala daseguinte forma:

def sum[A](xs: List[A])(m: Monoid[A]): A =if (xs.isEmpty) m.unitelse m.add(xs.head, sum(m)(xs.tail)

O método sum pode ser chamado da seguinte forma:

sum(List("a", "bc", "def"))(stringMonoid)sum(List(1, 2, 3))(intMonoid)

Embora tudo isso funcione, o código não fica muito limpo. O problema é que asimplementações de monoid tem que ser passada para todo o código que as usa. Nósgostaríamos que o sistema conseguisse descobrir os argumentos de forma correta eautomática, semelhante ao que é feito quando o tipo de parâmetros é inferido. Istoé o que os parâmetros implícitos permite.

Noções Básicas sobre Parâmetros Implícitos

Na versão 2 de Scala, há uma nova palavra-chave (implicit) que pode ser usada noinício de uma lista de parâmetros. Sintaxe:

ParamClauses ::= {‘(’ [Param {‘,’ Param}] ’)’}[‘(’ implicit Param {‘,’ Param} ‘)’]

Se esta palavra chave estiver presente, todos os parâmetros da lista serão implícitos.Por exemplo, a versão de sum a seguir tem m como um parâmetro implícito.

def sum[A](xs: List[A])(implicit m: Monoid[A]): A =if (xs.isEmpty) m.unitelse m.add(xs.head, sum(xs.tail))

Como pode ser visto no exemplo, é possível combinar parâmetros normais eimplícitos. No entanto, pode haver apenas uma lista de parâmetros implícitos eela deve vir por último.

implicit também pode ser usado como um modificador de declarações edefinições. Exemplos:

implicit object stringMonoid extends Monoid[String] {def add(x: String, y: String): String = x.concat(y)def unit: String = ""

}implicit object intMonoid extends Monoid[Int] {def add(x: Int, y: Int): Int = x + ydef unit: Int = 0

}

117

A principal ideia por trás de parâmetros implícitos é que argumentos para elespodem ser omitidos em uma chamada de método. Se os argumentos estiveremausentes, eles serão inferidos pelo compilador de Scala.

Os argumento atuais que são elegíveis a serem passados implicitamente porparâmetro são todos os identificadores X que puderem ser acessado no ponto emque o método é chamado sem o uso de prefixo e que denotem uma definiçãoimplícita ou parâmetro.

Se existem mais do que um argumento elegíveis que casam com o tipo doparâmetro, o compilador Scala vai escolher o mais específico usando as regraspadrâo para resolução de sobrecarga. Por exemplo, dada a chamada

sum(List(1, 2, 3))

está em um contexto onde stringMonoid e intMonoid estão visíveis. Nós sabemosque o tipo genérico A do método sum precisa ser instanciado usando int. O únicovalor elegível que casa com o parâmetro implícito Monoid[Int] é o intMonoid e porisso este objeto será passado como parâmetro implícito.

A discussão mostra também que parâmetros implícitos são inferidos depois que otipo dos outros parâmetros são inferidos.

Conversões Implícitas

Digamos que você tenha uma expressão E do tipo T onde é esperado um tipoS. T não é um sub-tipo de S e nem é conversível para S por alguma conversãopré-definida. Nesse caso, o compilador Scala irá tentar como um último recursouma conversão implícita I (E). Onde, I é um identificador que denota a definiçãoimplícita ou parâmetro que seja acessível sem prefixo no ponto da conversão e quecontenha uma função ao qual podem ser usados como argumentos valores do tipoT e cujo resultado seja do tipo S ou sub-tipo do mesmo.

Conversões Implícitas podem também ser usadas na seleção de membros. Dadaa chamada E .x onde x não é um membro do tipo E , o compilador Scala irá tentarinserir uma conversão implícita I (E).x, de maneira que x seja um membro de I (E).

Aqui está um exemplo de uma função de conversão implícita que converte inteirosem instâncias da classe scala.Ordered:

implicit def int2ordered(x: Int): Ordered[Int] = new Ordered[Int] {def compare(y: Int): Int =if (x < y) -1else if (x > y) 1else 0

}

118 Parâmetros Implícitos e Conversões

Parâmetros de Tipos Delimitados

Parâmetros de Tipos Delimitados1 são uma sintaxe simplificada2 e convenientepara parâmetros implícitos. Considere por exemplo, um método de ordenaçãogenérico:

def sort[A <% Ordered[A]](xs: List[A]): List[A] =if (xs.isEmpty || xs.tail.isEmpty) xselse {val {ys, zs} = xs.splitAt(xs.length / 2)merge(ys, zs)

}

O parâmetros de tipo delimitado [A <% Ordered[A]] expressa que sort podeser usado com listas do tipo A onde exista uma conversão implícita de A paraOrdered[A]. A definição é tratada como um atalho para a seguinte assinatura demétodo com parâmetro implícito:

def sort[A](xs: List[A])(implicit c: A => Ordered[A]): List[A] = ...

(Aqui o nome do parâmetro c foi escolhido arbitrariamente, garantindo-se que nãoera um nome já usado no programa.)

Como um exemplo mais detalhado, considere o método merge que vêm com ométodo sort citado anteriormente:

def merge[A <% Ordered[A]](xs: List[A], ys: List[A]): List[A] =if (xs.isEmpty) yselse if (ys.isEmpty) xselse if (xs.head < ys.head) xs.head :: merge(xs.tail, ys)else if ys.head :: merge(xs, ys.tail)

Depois de expandir os parâmetros de tipo delimitado e inserir as conversõesimplícitas a implementação deste método ficaria assim:

def merge[A](xs: List[A], ys: List[A])(implicit c: A => Ordered[A]): List[A] =

if (xs.isEmpty) yselse if (ys.isEmpty) xselse if (c(xs.head) < ys.head) xs.head :: merge(xs.tail, ys)else if ys.head :: merge(xs, ys.tail)(c)

As duas últimas linhas da definição do método ilustram dois diferentes usos doparâmetro implícito c. Ele é usado na conversão da condição na penúltima linha epassado como parãmetro implícito na chamada recursiva de merge na última linha.

1View bounds2Syntactic sugar

Capítulo 16

Inferência de Tipos deHindley/Milner

Este capítulo demonstra os tipos de dados Scala e o casamento de padrôesatravés do desenvolvimento de um sistema de inferência de tipos no estilo deHindley/Milner [Mil78]. A linguagem fonte para a inferência de tipos é o cálculolambda com uma construção chamada Mini-ML. As árvoes de sintaxe abstrata parao Mini-ML são representadas através do tipo de dados Terms.

abstract class Term {}case class Var(x: String) extends Term {override def toString = x

}case class Lam(x: String, e: Term) extends Term {override def toString = "(\\" + x + "." + e + ")"

}case class App(f: Term, e: Term) extends Term {override def toString = "(" + f + " " + e + ")"

}case class Let(x: String, e: Term, f: Term) extends Term {override def toString = "let " + x + " = " + e + " in " + f

}

Há quatro construtores de termos: Var para variáveis, Lam para abstrações lambda,App para aplicação e Let para expressões de atribuição. Cada uma destas classessobrescreve o método toString da classe Any, de forma que os termos podem serimpressos de forma legível.

A seguir, definimos os tipos que serão computados pelo sistema de inferência.

sealed abstract class Type {}case class Tyvar(a: String) extends Type {

120 Inferência de Tipos de Hindley/Milner

override def toString = a}case class Arrow(t1: Type, t2: Type) extends Type {override def toString = "(" + t1 + "->" + t2 + ")"

}case class Tycon(k: String, ts: List[Type]) extends Type {override def toString =k + (if (ts.isEmpty) "" else ts.mkString("[", ",", "]"))

}

Há três construtores de tipos: Tyvar para o tipo variável, Arrow para o tipo função eTyconpara o tipo construtor como, por exemplo, Boolean ou List. O tipo construtortem como componente uma lista de tipos que contem seus parâmetros. Esta lista évaiz para tipos constantes como Boolean. Assim como nos construtores de termos,implementamos o método toString para mostrar os tipos de forma legível.

Note que Type foi declarada com o modificador sealed. Isto significa que nenhumasub-classe ou construtores de dados que extendam Type podem ser declaradosfora da sequência de definições em que Type foi definida. Isto torna Type um tipoalgébrico fechado com exatas três alternativas. Em contraste, o tipo Term is um tipoalgébrico aberto onde mais alternativas poderâo ser definidas.

As principais partes da inferência de tipos estão contidas no objeto typeInfer.Começamos com uma função utilitária que cria novos tipos variáveis:

object typeInfer {private var n: Int = 0def newTyvar(): Type = { n += 1; Tyvar("a" + n) }

Em seguinda, definimos uma classe para substituições. A substituição é umafunção idempotente de tipos variáveis para tipos. Ela mapeia um número finitode tipos variáveis para alguns tipos e não modifica todos os outros tipos. Osignificado de uma substituição é extendido a partir de um mapeamento de tipospara tipos. Também extendemos o significado da substituição para ambientes queserão definidos posteriormente.

abstract class Subst extends Function1[Type,Type] {

def lookup(x: Tyvar): Type

def apply(t: Type): Type = t match {case tv @ Tyvar(a) => val u = lookup(tv); if (t == u) t else apply(u)case Arrow(t1, t2) => Arrow(apply(t1), apply(t2))case Tycon(k, ts) => Tycon(k, ts map apply)

}

def apply(env: Env): Env = env.map({ case (x, TypeScheme(tyvars, tpe)) =>

121

// assume que tyvars nao ocorre nesta substituicao(x, TypeScheme(tyvars, apply(tpe)))

})

def extend(x: Tyvar, t: Type) = new Subst {def lookup(y: Tyvar): Type = if (x == y) t else Subst.this.lookup(y)

}}val emptySubst = new Subst { def lookup(t: Tyvar): Type = t }

Representamos substituições como funções do tipo Type => Type. Isto podeser obtido fazendo com que a classe Subst herde da tipo função unáriaFunction1[Type, Type]1. Para ser uma instância de Subst, uma substituição s

tem que implementar o método apply que recebe como argumento um Type eretorna um outro Type como resultado. A função aplicação s(t) é interpretadacomo s.apply(t).

O método lookup é abstrato na classe Subst. Existem duas formas concretas desubstituição que diferem em como elas implementam este método. Uma forma édefinida pelo valor emptySubst e a outra é definida pelo método extend na classeSubst.

O próximo tipo de dado descreve esquemas de tipos, que consistem de um tipo euma lista de nomes de tipos variáveis que aparecem universalmente quantificadosno esquema de tipos. Por exemplo o esquema de tipos ∀a∀b.a → b seriarepresentado no checador de tipos como:

TypeScheme(List(Tyvar("a"), Tyvar("b")), Arrow(Tyvar("a"), Tyvar("b"))) .

A definição da classe esquema de tipos não contém uma cláusula extends; isso querdizer que um esquema de tipos herda diretamente da classe AnyRef. Embora existaapenas uma única maneira de construir um esquema de tipos, uma representaçãousando classe case foi escolhida, pois oferece formas convenientes de acessar aspartes de uma instância deste tipo.

case class TypeScheme(tyvars: List[Tyvar], tpe: Type) {def newInstance: Type = {(emptySubst /: tyvars) ((s, tv) => s.extend(tv, newTyvar())) (tpe)

}}

Os objetos de esquemas de tipos vem com o método newInstance, que retornao tipo contido no esquema depois de todos os tipos variáveis tiverem sidorenomeados para novas variáveis. A implementação deste método reduz (com /:)

1 A classe herda do tipo função como um mixin, ao invés de uma super-classe direta. Isto ocorreporque na implementação atual de Scala, o tipo Function1 é uma interface Java que não pode seruma super-classe de uma outra classe.

122 Inferência de Tipos de Hindley/Milner

os tipos variáveis do esquema de tipos com uma função que extende uma dadasubstituição s renomeando um dado tipo variável tv em um novo tipo variável. Asubstituição resultante renomeia todos os tipos variáveis do esquema em novos.Esta substituição é então aplicada o tipo do esquema de tipos.

O último tipo que necessitamos no sistema de inferência de tipos é Env, um tipopara os ambientes, que associa nomes de variáveis à esquema de tipos. Eles sãorepresentados pelo tipo Env no módulo typeInfer:

type Env = List[(String, TypeScheme)]

Existem duas operações nos ambientes. A função lookup retorna o esquema detipos associado com um dado nome ou null se o nome não foi registrado noambiente.

def lookup(env: Env, x: String): TypeScheme = env match {case List() => nullcase (y, t) :: env1 => if (x == y) t else lookup(env1, x)

}

A função gen retorna um esquema de tipos dado um tipo, quantificando todos ostipos variáveis que estão livres no tipo, mas não no ambiente.

def gen(env: Env, t: Type): TypeScheme =TypeScheme(tyvars(t) diff tyvars(env), t)

o conjunto de tipos variáveis livres é simplesmente o conjunto de todos os tiposvariáveis que ocorrem no tipo. É representado por uma lista de tipos variáveisconstruído da seguinte maneira.

def tyvars(t: Type): List[Tyvar] = t match {case tv @ Tyvar(a) =>List(tv)

case Arrow(t1, t2) =>tyvars(t1) union tyvars(t2)

case Tycon(k, ts) =>(List[Tyvar]() /: ts) ((tvs, t) => tvs union tyvars(t))

}

Note que a sintaxe tv @ ... no primeiro padrão introduz a variável que está ligadaao padrão seguinte. Note também que o tipo parâmetro [Tyvar] explícito naexpressão da terceita cláusula é necessário para que a inferência de tipos locaisfuncione.

O conjunto de tipos variáveis livres de um esquema de tipos é o conjunto detipos variáveis livre do seu tipo componente, excluíndo-se quaisquer tipos variáveisquantificáveis.

123

def tyvars(ts: TypeScheme): List[Tyvar] =tyvars(ts.tpe) diff ts.tyvars

Finalmente, o conjunto tipos variáveis livres de um ambiente é a união de todos ostipos variáveis livres de todos os esquemas de tipos registrados neste ambiente.

def tyvars(env: Env): List[Tyvar] =(List[Tyvar]() /: env) ((tvs, nt) => tvs union tyvars(nt._2))

A principal operação da checagem de tipos de Hindley/Milner é a unificação, quecomputa a substituição para fazer que dois tipos dados se tornem iguais (estasubsituição é chamada um unificador2). A função mgu computa o unificador maisgeral de dois tipos dados t e u sob um substituição pre-existente s. Isto é, ele retornaa substituição mais geral s′ que herda de s, e que faz com que s′(t ) e s′(u) retornemtipos iguais.

def mgu(t: Type, u: Type, s: Subst): Subst = (s(t), s(u)) match {case (Tyvar(a), Tyvar(b)) if (a == b) =>s

case (st @ Tyvar(a), su) if !(tyvars(su) contains st) =>s.extend(st, su)

case (_, Tyvar(a)) =>mgu(u, t, s)

case (Arrow(t1, t2), Arrow(u1, u2)) =>mgu(t1, u1, mgu(t2, u2, s))

case (Tycon(k1, ts), Tycon(k2, us)) if (k1 == k2) =>(s /: (ts zip us)) ((s, tu) => mgu(tu._1, tu._2, s))

case _ =>throw new TypeError("cannot unify " + s(t) + " with " + s(u))

}

A função mgu lança uma exceção do tipo TypeError se não existir uma substituiçãounificadora. Isto pode ocorrer quando os dois tipos tem diferentes tiposconstrutores em seus lugares correspondentes ou quando um tipo variável éunificado com um tipo que contém um tipo variável dele mesmo. Estas exceçõesforam modeladas como instâncias de classe case que herdam um classe Exception

pré-definida.

case class TypeError(s: String) extends Exception(s) {}

A principal tarefa do checador de tipos é implementada pela função tp. Estafunção recebe como parâmetro um ambiente env , um termo e, um tipo t , e umasubstituição pre-existente s. A função retorna uma substituição s′ que herda de s eque torna s′(env) ` e : s′(t ) num julgamento de tipos derivável de acordo com asregras de derivação do sistema de tipos de Hindley/Milner[Mil78]. Uma exceção do

2unifier

124 Inferência de Tipos de Hindley/Milner

tipo TypeError é lançada se não existe uma substituição com estas características.

def tp(env: Env, e: Term, t: Type, s: Subst): Subst = {current = ee match {case Var(x) =>val u = lookup(env, x)if (u == null) throw new TypeError("undefined: " + x)else mgu(u.newInstance, t, s)

case Lam(x, e1) =>val a, b = newTyvar()val s1 = mgu(t, Arrow(a, b), s)val env1 = {x, TypeScheme(List(), a)} :: envtp(env1, e1, b, s1)

case App(e1, e2) =>val a = newTyvar()val s1 = tp(env, e1, Arrow(a, t), s)tp(env, e2, a, s1)

case Let(x, e1, e2) =>val a = newTyvar()val s1 = tp(env, e1, a, s)tp({x, gen(s1(env), s1(a))} :: env, e2, t, s1)

}}var current: Term = null

Para auxiliar no diagnóstico de erros, a função tp guarda o sub-termo que estásendo analisado na variável current. Com isso, se a checagem de tipos foriterrompido por uma exceção do tipo TypeError, esta variável conterá o sub-termoque causou o problema.

A última função do módulo de inferência de tipos , typeOf, é uma façadesimplificada para tp. Ela computa o tipo de um dado termo e em um dado ambienteenv . Ela o faz através da criação de um novo tipo variável a, e da computação dasubstituição de tipos que faz env ` e : a se tornar um tipo derivado e retorna oresultado da aplicação da subsituição em a.

def typeOf(env: Env, e: Term): Type = {val a = newTyvar()tp(env, e, a, emptySubst)(a)

}}// fim typeInfer

125

Para usar o sistema de inferência de tipos, é conveniente ter um ambientepré-definido que contém as definições das constantes mais comumente usadas.O módulo predefined define um ambiente env que contém as definições dostipos booleanos, números e listas assim como algumas operações primitivas sobreeles. Também define um operador de ponto fixo fix, que pode ser usado pararepresentar uma recursão.

object predefined {val booleanType = Tycon("Boolean", List())val intType = Tycon("Int", List())def listType(t: Type) = Tycon("List", List(t))

private def gen(t: Type): typeInfer.TypeScheme = typeInfer.gen(List(), t)private val a = typeInfer.newTyvar()val env = List({"true", gen(booleanType)},{"false", gen(booleanType)},{"if", gen(Arrow(booleanType, Arrow(a, Arrow(a, a))))},{"zero", gen(intType)},{"succ", gen(Arrow(intType, intType))},{"nil", gen(listType(a))},{"cons", gen(Arrow(a, Arrow(listType(a), listType(a))))},{"isEmpty", gen(Arrow(listType(a), booleanType))},{"head", gen(Arrow(listType(a), a))},{"tail", gen(Arrow(listType(a), listType(a)))},{"fix", gen(Arrow(Arrow(a, a), a))}

)}

Aqui está um exemplo de como o sistema de inferência de tipos pode serusado. Vamos definir a função showType que retorna os tipos de um dado termocomputado em um dado ambiente Predefined.env:

object testInfer {def showType(e: Term): String =try {typeInfer.typeOf(predefined.env, e).toString

} catch {case typeInfer.TypeError(msg) =>"\n cannot type: " + typeInfer.current +"\n reason: " + msg

}

Então a aplicação

> testInfer.showType(Lam("x", App(App(Var("cons"), Var("x")), Var("nil"))))

126 Inferência de Tipos de Hindley/Milner

retornará a resposta

> (a6->List[a6])

Exercício 16.0.1 Extenda o Mini-ML e o sistema de inferência de tipos com umaconstrução letrec que permita a definição recursiva de funções. Sintaxe:

letrec ident "=" term in term .

Os tipos de letrec devem ser como os de let, exceto que os identificadoresdefinidos são visíveis na expressão que está sendo definida. Usando letrec, afunção length para listas seria definida da seguinte maneira.

letrec length = \xs.if (isEmpty xs)zero(succ (length (tail xs)))

in ...

Capítulo 17

Abstracões para Concorrência

Esta seção revisa padrôes comuns de concorrência e mostra como eles podem serimplementados em Scala.

17.1 Sinais e Monitores

Exemplo 17.1.1 Um monitor provê mecanismos básicos para processosmutuamente exclusivos em Scala. Toda instância de AnyRef pode ser usadacomo um monitor através da chamada de um ou mais dos métodos apresentadosa seguir.

def synchronized[A] (e: => A): Adef wait()def wait(msec: Long)def notify()def notifyAll()

O método synchronized computa e em modo mutuamente exclusivo – em umdado momento qualquer, somente uma thread pode executar um argumentosynchronized em um dado monitor.

Threads podem ser paradas dentro de um monitor e esperar por um sinal. Threadspodem chamar o método wait e esperar até que o método notify do mesmo objetoseja chamado por alguma outra thread. Chamadas ao método notify quando nãoexistam threads esperando por um sinal são ignoradas.

Existe também uma forma do método wait baseada em tempo em que a execuçãoé bloqueada enquanto nenhum sinal seja recebido ou um dado espaço de tempo(dado em milisegundos) tenha passado. Além disso, há o método notifyAll

que desbloqueia todas as threads que estejam esperando por um sinal. Estesmétodos, assim como a classe Monitor são primitivos em Scala, ou seja, eles

128 Abstracões para Concorrência

são implementados usando os mecanismos internos do sistema de execução dosprogramas.

Tipicamente, uma thread espera até que um certa condição ocorra. Se estacondição não ocorrer até o tempo definido na chamada do método wait, a threadfica com execução suspensa até que alguma outra thread estabeleça tal condiçãoou que o tempo definido tenha passado. É responsabilidade desta outra threadreiniciar os processos que estavam esperando através da chamada dos métodosnotify ou notifyAll. Note que não existe garantia de que um processo em esperaexecute imediatamente após a chamada do método notify. Pode ocorrer de outrosprocessos que executem antes invalidem novamente esta condição, deixando asthreads suspensas. Portanto, a forma correta de esperar uma condição C usa umlaço do tipo while:

while (!C) wait()

Como um exemplo de como os monitores são usados, aqui está umaimplementação de uma classe de buffer com limites.

class BoundedBuffer[A](N: Int) {var in = 0, out = 0, n = 0val elems = new Array[A](N)

def put(x: A) = synchronized {while (n >= N) wait()elems(in) = x ; in = (in + 1) % N ; n = n + 1if (n == 1) notifyAll()

}

def get: A = synchronized {while (n == 0) wait()val x = elems(out) ; out = (out + 1) % N ; n = n - 1if (n == N - 1) notifyAll()x

}}

E aqui está um programa usando esta classe para comunicar entre processosconsumidores e produtores.

import scala.concurrent.ops._...val buf = new BoundedBuffer[String](10)spawn { while (true) { val s = produceString ; buf.put(s) } }spawn { while (true) { val s = buf.get ; consumeString(s) } }}

17.2 SyncVars 129

O método spawn dispara uma nova thread que executa a expressão passada comoparâmetro. Foi definida no objeto concurrent.ops da seguinte maneira:

def spawn(p: => Unit) {val t = new Thread() { override def run() = p }t.start()

}

17.2 SyncVars

Uma variável sincronizada (ou syncvar) oferece as operações get e put para ler eescrever na variável. As operações get bloqueiam a execução até que o valor davariável tenha sido definido. Um operação unset coloca o valor da variável em umvalor indefinido.

Aqui segue uma implementação padrão de variáveis sincronizadas:

package scala.concurrentclass SyncVar[A] {private var isDefined: Boolean = falseprivate var value: A = _def get = synchronized {while (!isDefined) wait()value

}def set(x: A) = synchronized {value = x; isDefined = true; notifyAll()

}def isSet: Boolean = synchronized {isDefined

}def unset = synchronized {isDefined = false

}}

17.3 Futuros

Um futuro (future) é um valor que será computado em paralelo com alguma outrathread do cliente e que será usado pelo cliente em algum momento no futuro.Futuros são usados para utilizar melhor os recursos de processamento paralelo. Ouso típico é:

import scala.concurrent.ops._

130 Abstracões para Concorrência

...val x = future(someLengthyComputation)anotherLengthyComputationval y = f(x()) + g(x())

O método future é definido no objeto scala.concurrent.ops da seguinte maneira.

def future[A](p: => A): Unit => A = {val result = new SyncVar[A]fork { result.set(p) }(() => result.get)

}

O método future recebe como parâmetro uma computação p que precisa sercalculado. O tipo da computação é arbitrário e é representando pelo tipo genérico A.O método future define um guarda result, que recebe o parâmetro que representao resultado da computação. Ao chegar neste ponto, ele abre uma nova thread quecomputa o resultado e invoca o guarda result quanto o processo terminar. Emparalelo à esta thread, a função retorna uma função anônima do tipo A. Quandochamada, esta função espera que o guarda resultado tenha sido chamada, e,quando isso ocoore, retorna o resultado. Ao mesmo tempo, a função re-invoca oguarda result com o mesmo argumento, de forma que, futuras chamadas à funçãopossam retornar o resultado imediatamente.

17.4 Computação Paralela

O próximo exemplo apresenta a função par que recebe um par de computaçoescomo prarâmetros e retorna o resultado destas computações em um outro par. Asduas computações são executadas paralelamente.

A função estã definida no objeto scala.concurrent.ops da seguinte forma:

def par[A, B](xp: => A, yp: => B): (A, B) = {val y = new SyncVar[B]spawn { y set yp }(xp, y.get)

}

Definida no mesmo objeto está a função replicate que executa um número deréplicas de uma computação em paralelo. Cada instância replicada é passada comoum número inteiro que a identifica.

def replicate(start: Int, end: Int)(p: Int => Unit) {if (start == end)()

else if (start + 1 == end)

17.5 Semáforos 131

p(start)else {val mid = (start + end) / 2spawn { replicate(start, mid)(p) }replicate(mid, end)(p)

}}

A função a seguir usa replicate para realizar uma computação paralela em todosos elementos de um vetor.

def parMap[A,B](f: A => B, xs: Array[A]): Array[B] = {val results = new Array[B](xs.length)replicate(0, xs.length) { i => results(i) = f(xs(i)) }results

}

17.5 Semáforos

Um mecanismo comum para sincronização de processos é o uso de travas (lock) ousemáforo. Uma trava oferece duas operações atômicas: acquire e release. Aqui estáa implementação de uma trava en Scala:

package scala.concurrent

class Lock {var available = truedef acquire = synchronized {while (!available) wait()available = false

}def release = synchronized {available = truenotify()

}}

17.6 Leitores/Escritores

Uma forma mais complexa de sincronização distingue leitores (readers) queacessam um recurso comum sem modificá-lo e escritores (writers) que podemacessar e modificar esse recurso. Para sincronizar leitores e escritores, precisamos

132 Abstracões para Concorrência

implementar as operações startRead, startWrite, endRead, endWrite, de tal formaque:

• podem haver múltiplos leitores concorrentemente;

• só pode haver um único escritor em um dado instante;

• solicitações de escrita pendentes tem prioridade sobre solicitações deleitura pendentes, mas não interrompem operações de leitura que estejamocorrendo.

A implementação de travas para leitores/escritores a seguir é baseada no conceitode caixa postal (mailbox) (ver Seção 17.10).

import scala.concurrent._

class ReadersWriters {val m = new MailBoxprivate case class Writers(n: Int), Readers(n: Int) { m send this }Writers(0); Readers(0)def startRead = m receive {case Writers(n) if n == 0 => m receive {case Readers(n) => Writers(0); Readers(n+1)

}}def startWrite = m receive {case Writers(n) =>Writers(n+1)m receive { case Readers(n) if n == 0 => }

}def endRead = m receive {case Readers(n) => Readers(n-1)

}def endWrite = m receive {case Writers(n) => Writers(n-1); if (n == 0) Readers(0)

}}

17.7 Canais Assíncronos

Um modo fundamental de comunicação entre processos é o canal assíncrono. Suaimplementação faz usa da seguinte classe para listas-ligadas:

class LinkedList[A] {var elem: A = _var next: LinkedList[A] = null

17.8 Canais Síncronos 133

}

Para facilitar a inserção e remoção de elementos nas listas ligadas, cada referênciana lista ligada aponta para o nó que precede o nó que conceitualmente forma otopo da lista. Listas ligadas vazias começam com um nó fantasma, cujo o sucessoré null.

A classe canal usa a lista ligada para armazenar dados que foram enviados, masainda não foram lidos. No lado oposto, threads que necessitam ler de um canalvazio, registram sua presença incrementando o campo nreaders e esperando seremnotificadas.

package scala.concurrent

class Channel[A] {class LinkedList[A] {var elem: A = _var next: LinkedList[A] = null

}private var written = new LinkedList[A]private var lastWritten = writtenprivate var nreaders = 0

def write(x: A) = synchronized {lastWritten.elem = xlastWritten.next = new LinkedList[A]lastWritten = lastWritten.nextif (nreaders > 0) notify()

}

def read: A = synchronized {if (written.next == null) {nreaders = nreaders + 1; wait(); nreaders = nreaders - 1

}val x = written.elemwritten = written.nextx

}}

17.8 Canais Síncronos

Aqui está uma implementação de canais síncronos, onde quem envia umamensagem tem sua execução bloqueada até que esta mensagem seja recebida.Canais síncronos precisam apenas de uma única variável para armazenar as

134 Abstracões para Concorrência

mensagens em trânsito, mas de três sinais para coordenar os processos de leiturae escrita.

package scala.concurrent

class SyncChannel[A] {private var data: A = _private var reading = falseprivate var writing = false

def write(x: A) = synchronized {while (writing) wait()data = xwriting = trueif (reading) notifyAll()else while (!reading) wait()

}

def read: A = synchronized {while (reading) wait()reading = truewhile (!writing) wait()val x = datawriting = falsereading = falsenotifyAll()x

}}

17.9 Trabalhadores

Aqui está uma implementação de um servidor de computação em Scala. O servidorimplementa um método future que computa uma dada expressão paralelamentecom quem chamou o método. Diferenmente da implementação de futuros daseção 17.3 o servidor computa os futuros somente com um número pré-definido dethreads. Uma possível implementação do servidor poderia executar cada thread emum processador separado, e com isso evitar o custo inerente à mudança de contextoentre muitas threads em um único processo.

import scala.concurrent._, scala.concurrent.ops._

class ComputeServer(n: Int) {

private abstract class Job {

17.9 Trabalhadores 135

type Tdef task: Tdef ret(x: T)

}

private val openJobs = new Channel[Job]()

private def processor(i: Int) {while (true) {val job = openJobs.readjob.ret(job.task)

}}

def future[A](p: => A): () => A = {val reply = new SyncVar[A]()openJobs.write{new Job {type T = Adef task = pdef ret(x: A) = reply.set(x)

}}() => reply.get

}

spawn(replicate(0, n) { processor })}

Expressões a serem computadas (ex.: parâmetros da chamada de um future) sãoescritos no canal openJobs . Um job é um objeto em que:

• Um tipo abstrato T descreve o resultado de sua computação.

• Um método sem parâmetros (task) do tipo t representa a expressão a sercomputada.

• Um método ret consome o resultado, quando este tiver sido computado.

O servidor de computação cria n processos no processador como parte de suainicialização. Cada um destes processos repetidamente consome um trabalhoem aberto no canal openJobs, computa o métodotask e passa o resultado para ométodo ret. O método polimórfico future cria um novo job quando o método ret

é implementado por um guarda chamado reply e insere este job no conjunto detrabalhos em aberto. Este espera até que o método guarda reply correspondentefor chamado.

136 Abstracões para Concorrência

O exemplo mostra o uso de tipos abstratos. Um tipo abstrato t mantém controledo tipo do resultado de um job que pode variar entre diferentes jobs. Sem ostipos abstratos seria impossível implementar a mesma classe para o usuário deuma forma que garantisse a segurança do sistemas de tipos estaticamente. Seriamnecessários testes de tipos dinâmicos e o uso de casts.

Aqui um trecho de código que usa o servidor de computação para calcular aexpressão 41 + 1.

object Test with Executable {val server = new ComputeServer(1)val f = server.future(41 + 1)println(f())

}

17.10 Caixas Postais

Caixas postais são construções flexíveis de alto nível para comunicação esincronização de processos. Elas permitem enviar e receber mensagens. Ummensagem neste contexto é um objeto arbitrário. Há uma mensagem especial,chamada TIMEOUT que é usada para sinalizar um time-out.

case object TIMEOUT

Caixas postais implentam os seguintes métodos

class MailBox {def send(msg: Any)def receive[A](f: PartialFunction[Any, A]): Adef receiveWithin[A](msec: Long)(f: PartialFunction[Any, A]): A

}

O estado de uma caixa postal consiste de vários conjuntos de mensagens. Asmensagens são adicionadas à caix postal através do método send. As mensagenssão removidas usando o método receive, que passa um processador de mensagensf como parâmetro. O processador de mensagens é uma função parcial que tenhacomo resultado um tipo arbitrário. Normalmente, esta função é implementadausando uma expressão de casamento de padrôes. O método receive suspendesua execução até que exista uma mensagem na caixa postal para o qual o seuprocessador de mensagens foi definido. A mensagem que casa com um processadoré então removida da caixa postal e a thread suspensa é reiniciada aplicando oprocessador de mensagens à mensagem. Tanto mensagens enviadas e receptoressão ordenados cronologicamente. Um receptor r é aplicado à uma mensagem mse for capaz de recebê-la (tipo) e somente se não existe um outro par {message,receiver} que preceda m,r no ordenamento cronológico parcial destes pares.

17.10 Caixas Postais 137

Como um simples exemplo de como caixas postais são usadas, considere o bufferde uma posição:

class OnePlaceBuffer {private val m = new MailBox // Caixa postal internaprivate case class Empty, Full(x: Int) // Tipo das mensagens que pode tratarm send Empty // Inicializa\c{c}\~{a}odef write(x: Int){ m receive { case Empty => m send Full(x) } }

def read: Int =m receive { case Full(x) => m send Empty; x }

}

Aqui como uma classe caixa postal pode ser implementada:

class MailBox {private abstract class Receiver extends Signal {def isDefined(msg: Any): Booleanvar msg = null

}

Definimos um classe interna para receptores com um método de teste isDefined,que indica se o receptor pode tratar uma dada mensagem. O receptor herda daclasse Signal o método notify que serve para "acordar" a thread para receptora.Quanto a thread receptora é reiniciada, a mensagem que precisa ser aplicada éarmazenada na variável msg do Receiver.

private val sent = new LinkedList[Any]private var lastSent = sentprivate val receivers = new LinkedList[Receiver]private var lastReceiver = receivers

Um classe caixa postal mantém duas lista ligadas, uma para mensagens enviadas enão consumidas e outra para mensagens esperando receptores.

def send(msg: Any) = synchronized {var r = receivers, r1 = r.nextwhile (r1 != null && !r1.elem.isDefined(msg)) {r = r1; r1 = r1.next

}if (r1 != null) {r.next = r1.next; r1.elem.msg = msg; r1.elem.notify

} else {lastSent = insert(lastSent, msg)

}}

138 Abstracões para Concorrência

Inicialmente, o método send verifica se há um receptor que pode ser aplicado àmensagem enviada. Se sim, o receptor é notificado. Se não, a mensagem é anexadaà lista ligada de mensagens enviadas.

def receive[A](f: PartialFunction[Any, A]): A = {val msg: Any = synchronized {var s = sent, s1 = s.nextwhile (s1 != null && !f.isDefinedAt(s1.elem)) {s = s1; s1 = s1.next

}if (s1 != null) {s.next = s1.next; s1.elem

} else {val r = insert(lastReceiver, new Receiver {def isDefined(msg: Any) = f.isDefinedAt(msg)

})lastReceiver = rr.elem.wait()r.elem.msg

}}f(msg)

}

Inicialmente, o método receive verifica se existe uma função processadora demensagens f que pode ser aplicada à mensagem que já foi enviada, mas aindanão foi consumida. Se sim, a thread continua imediatamente aplicando a funçãof à mensagem. Se não, um novo receptor é criado adicionado à lista receivers ea thread esperará por uma notificação neste receptor. Uma vez que a thread sejareiniciada, ela continua aplicando a função f que foi armazenado no receptor àmensagem. O método insert na lista ligada é definido da seguinte maneira:

def insert(l: LinkedList[A], x: A): LinkedList[A] = {l.next = new LinkedList[A]l.next.elem = xl.next.next = l.nextl

}

A classe caixa postal também oferece o método receiveWithin que suspendea execução por um tempo máximo especificado. Se nenhuma mensagem forrecebida no intervalo de tempo especificado (dado em milissegundos), o parâmetrodo processador de mensagens f será desbloqueado com a mensagem especialTIMEOUT. A implementação de receiveWithin é bastante parecida com a dereceive:

17.11 Actors 139

def receiveWithin[A](msec: Long)(f: PartialFunction[Any, A]): A = {val msg: Any = synchronized {var s = sent, s1 = s.nextwhile (s1 != null && !f.isDefinedAt(s1.elem)) {s = s1; s1 = s1.next

}if (s1 != null) {s.next = s1.next; s1.elem

} else {val r = insert(lastReceiver, new Receiver {

def isDefined(msg: Any) = f.isDefinedAt(msg)})lastReceiver = rr.elem.wait(msec)if (r.elem.msg == null) r.elem.msg = TIMEOUTr.elem.msg

}}f(msg)

}} // fim MailBox

A única diferença é a chamada ao método wait que recebe o tempo de espera e alinha de código que vem em seguida, onde a mensagem TIMEOUT é aplicada.

17.11 Actors

O capítulo 3 apresentou um programa como exemplo de implementação deum serviço de leilão eletrônico. Este serviço foi baseado em atores quuerepresentam processos de alto nível e trabalham inspecionando as mensagensem sua caixa-postal usando casamento de padrões. Uma implementação maisrefinada e otimizada de atores pode ser encontrada no pacote scala.actors. Agoramostraremos um rascunho de uma versão simplificada da biblioteca de atores.

O código apresentado a seguir é diferente da implementação presente no pacotescala.actors e, portanto, deve ser vista como um exemplo de como uma versãosimplificada dos atores poderia ser implementada. Ela não descreve como os atoresforam definidos e implementados na biblioteca padrão de Scala. Caso deseje essainformação, por favor consulte a documentação da API de Scala.

Um ator simplificado é apenas uma thread cujas primitivas de comunicação sãoaquelas de uma caixa postal. Tal ator pode ser definido como uma composiçãomixin da extensão da classe Java padrão Thread com a classe MailBox. Nós tambémsobre-escrevemos o método run da classe Thread, de tal forma que ele executeo comportamento de um ator que é definido pelo método act. O método !

140 Abstracões para Concorrência

simplesmente chama o método send da classe MailBox:

abstract class Actor extends Thread with MailBox {def act(): Unitoverride def run(): Unit = act()def !(msg: Any) = send(msg)

}

Bibliografia

[ASS96] Harold Abelson, Gerald Jay Sussman, and Julie Sussman. The Structure andInterpretation of Computer Programs, 2nd edition. MIT Press, Cambridge,Massachusetts, 1996.

[Mil78] Robin Milner. A Theory of Type Polymorphism in Programming. Journalof Computer and System Sciences, 17:348–375, Dec 1978.