Técnico

Streams e Iterators em Scala

Utilizando Stream no lugar de List


Vimos no post anterior como lidar com listas em Scala.
Listas são uma das estruturas mais úteis no dia a dia de desenvolvimento, muitas vezes modelando bem a maioria dos problemas que envolvem algum tipo de estrutura de dados. Mas listas tem uma desvantagem que muitas vezes passa desapercebida em nossos programas: elas estão completamente definidas na memória. Isso significa que mesmo que o programa não necessite de todos os elementos da lista para executar, ainda sim a lista estará definida completamente.

Vamos a um exemplo para que fique mais claro o problema. Imagine que queremos encontrar o segundo número primo entre um determinado intervalo de inteiros. Para isso vamos começar definindo uma função que verifica se o número é primo:

[cc_java]
def isPrime(x: Int): Boolean = {
for (i <- List.range(2, x-1)) if (x%i == 0) return false true } [/cc_java]

Nossa função recebe um inteiro e constrói um range com os inteiros entre 2 e o inteiro passado menos 1. Estamos usando um for-comprehension para iterar no intervalo de inteiros construído, e caso o inteiro passado seja divisível pelo número atual no intervalo devolvemos false sinalizando que o inteiro não é primo.
Apenas uma observação: fomos obrigados a definir um tipo devolvido pela função, pois utilizamos a palavra reservada return. Isso porque como estamos dentro de um loop, precisamos obrigatoriamente usá-la para forçar a saida da execução da função. No caso do true, não é obrigatório pois se trata do último membro da função.

Agora vamos definir uma função que busca pelo segundo número primo:

[cc_java]
def secondPrime(start: Int, end: Int) = {
(List.range(start, end) filter isPrime)(1)
}
[/cc_java]

Sem grandes segredos nessa função. Estamos utilizando nossa função que verifica se o número é primo para criar uma lista apenas com os valores primos do intervalo. Dado a lista criada, devolvemos o segundo elemento, ou seja, o segundo número primo do intervalo. Existem outras maneiras de se implementar essa função, mas aqui estamos usando uma abordagem bem funcional, e tentando deixar o código o mais enxuto possível deixando de lado a ineficiência. Mantenha o cinto apertado, já chegaremos no conceito de Stream =).

Perceba que nossas funções são otimistas e não verificam alguns possíveis casos de erro, estamos seguindo esta abordagem pois o foco é demonstrar o conceito.

Utilizando o scala-ide, crie um Object com uma função main para rodarmos alguns testes:

[cc_java]
object StreamTest {
def main(args: Array[String]) = {
}
}
[/cc_java]

Como podemos utilizar classes Java no Scala, coloque uma verificação pelo tempo gasto com a execução de nossa função, e execute-a em um intervalo bem grande como por exemplo 1000 e 20000:

[cc_java]
var s = System.currentTimeMillis()
print(secondPrime(1000, 20000))
s = System.currentTimeMillis() – s
print(“ntime: ” + s)
[/cc_java]

Repare na saída do terminal, e no tempo em milisegundos gasto por nosso programa:

1013
time: 10327


Ou seja, aproximadamente 10 segundos.
O programa demorou todo esse tempo, pois a lista gerada pelo filtro de números primos foi completamente construída para a execução, mesmo que não utilizemos todos os valores.
O que você pode estar pensando agora é que com uma implementação diferente, a função seria muito mais eficiente. Mas com certeza não seria tão clara como esta. Para manter o paradigma funcional, Scala disponibiliza uma classe que trata os valores de uma lista de maneira mais lazy: Stream.

Streams são muito parecidos com listas, inclusive na API, porém computam valores da listas apenas quando estes são acessados. Portanto vamos mudar apenas a função secondPrime com o seguinte trecho, e verificar o tempo gasto pelo programa:

[cc_java]
def secondPrime(start: Int, end: Int) = {
(Stream.range(start, end) filter isPrime)(1)
}
[/cc_java]

1013
time: 41


Um pouco mais rápido, não? 😉
Com isso mantivemos a claresa do nosso código, sem abandonar a programação funcional.
O que fizemos, foi criar um intervalo de inteiros que ao invés de uma lista, estão definidos em um Stream. Dessa forma, nossa função só será computada de verdade quando acessamos a posição 1 do Stream, computando o valor para a posição 0 também, e deixando de lado o resto da lista, ou nesse caso Stream.

Quase toda a API de listas está disponível em Streams com execeção dos métodos :: e :::. Isso pois estes métodos precisam evaluar o tail da lista para concatenar o novo valor, o que invalidaria o funcionamento de Stream. Portanto, para adicionar elementos ao Stream devemos utilizar o método cons, da mesma maneira que utilizamos nas listas.

Iterators


Iterators em Scala são bem parecidos com os iterators de Java, e podemos também utilizá-los em for-comprehensions como listas.
Iterator são expostos por dois métodos: hasNext e next. O método hasNext devolve true se ainda existe algum elemento a ser percorrido no Iterator, e o método next devolve o próximo elemento. Pensando ainda no espírito lazy, podemos utilizar um Iterator que percorre um intervalo infinito de inteiros:

[cc_java]
val it = Iterator.from(7)
println(it.next);
println(it.next);
println(it.next);
println(it.next);
println(it.next);
[/cc_java]

Repare na saída:

7
8
9
10
11


Note que nosso iterator não tem fim (na verdade é limitado pelo valor máximo de um inteiro de 32 bits, que é aproximadamente quatro bilhões).
Podemos criar iterators para determinados intervalos também, dessa forma devemos checar pelo método hasNext antes de pedir pelo próximo elemento par ter certeza de que não chegamos no fim do intervalo.

Também conseguimos criar um Iterator par um determinado Array, com o método fromArray. O comportamento é exatamente o mesmo do iterator com números, por isso devemos sempre checar pelo método hasNext antes de iterar no Iterator.

Estamos próximos do fim dos tutorias de Scala. Espero que até aqui tenham sido úteis e tenham facilitado o aprendizado de quem está se aventurando nessa linguagem que ainda é nova na comunidade. Em breve estaremos tratando de como Scala trata concorrência e paralelismo, por isso recomendo que até aqui os conceitos sejam bem compreendidos para que possamos discutir mais sobre modelagem e a sintaxe deixe de ser um impeditivo.

Dúvidas, críticas e sugestões, só me procurar =).

Baseado em Scala by Example de Martin Odersky.

Por @Gust4v0_H4xx0r

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Compartilhe isso: