Técnico

Aprendendo Scala – Parte 2

Funções em Scala


Este post é a continuação do primeiro post sobre Scala onde eu explico como instalar e como entender o básico da sintaxe. Caso você não tenha visto ainda, vale a consulta rápida: primeiros passos com Scala.

Funções em Scala assim como valores comuns, podem ser passados como parâmetro, armazenados em variáveis e até mesmo serem devolvidas como resultado de outras funções. Funções que recebem outras funções como parâmetro, ou devolvem alguma função como resultado são chamadas de ‘higher-order functions’. Tais funções nos permite diferentes tipos de refatorações e abstrações de código, quebrando bastante o paradigma de programação orientada a objeto, por isso exigem um bom entendimento de seu funcionamento para aproveitar bem da flexibilidade que Scala disponibiliza.

Implementando Higher-Order Functions


Vamos ver um exemplo de função que recebe outra função como parâmetro. Abra o terminal do Scala e como exemplo vamos implementar uma função que calcula a soma entre dois números passados:

[cc_javascript]
scala> def somaEntre(a: Int, b: Int) = if (a > b) 0 else a + somaEntre(a+1, b)
[/cc_javascript]

Tente compilar o código acima e perceba o erro que irá aparecer no console: “5: error: recursive method somaEntre needs result type“. Aqui vale ressaltar que apesar de parecer que Scala não tem tipos bem definidos, o que acontece na verdade é que os tipos podem ser inferidos pelo compilador. Neste caso não estamos dando nenhuma pista para o compilador de qual o tipo que somaEntre devolve, logo ele tenta associar o tipo como Unit (visto no post anterior) e obtem um erro de sintática, pois o valor devolvido no if é do tipo Int. Para que o código compile basta adicionar o tipo ao valor que a função devolve:

[cc_javascript]
scala> def somaEntre(a: Int, b: Int):Int = if (a > b) 0 else a + somaEntre(a+1, b)
somaEntre: (a: Int,b: Int)Int

scala> somaEntre(1, 10)
res0: Int = 55

[/cc_javascript]

Agora imagine que ao invés da soma, queremos calcular a soma dos quadrados dos números entre os números passados. Para isso poderíamos implementar as seguintes funções:

[cc_javascript]
scala> def quadrado(x: Int) = x * x
quadrado: (x: Int)Int

scala> def somaQuadradosEntre(a: Int, b: Int):Int = if (a > b) 0 else quadrado(a) + somaQuadradosEntre(a+1, b)
somaQuadradosEntre: (a: Int,b: Int)Int

scala> somaQuadradosEntre(1, 10)
res1: Int = 385
[/cc_javascript]

Acredito que tenha ficado claro o padrão seguido em ambas as funções, o que podemos fazer agora é refatorar o código de uma maneira mais funcional por assim dizer. Ao invés de criar uma função diferente para somar os valores entre os dois números passados, vamos usar a mesma funçao de maneira que agora ela receba a função que deve ser executada com o número antes de ser somado ao total:

[cc_javascript]
scala> def somaEntreComF(a: Int, b: Int, f: Int => Int): Int = if(a > b) 0 else f(a) + somaEntreComF(a+1, b, f)
somaEntreComF: (a: Int,b: Int,f: (Int) => Int)Int
[/cc_javascript]

Dessa forma conseguimos passar uma função que será aplicada aos números entre a e b antes de somá-los, por exemplo para o segundo caso: quadrado.

[cc_javascript]
scala> somaEntreComF(1, 10, quadrado)
res2: Int = 385
[/cc_javascript]

Bom, só que queremos que o comportamento antigo seja mantido, onde era devolvido apenas a soma dos valores sem que se aplique nenhuma função. Nesse caso basta implementarmos uma função bem conhecida nas linguagens funcionais, ‘id’:

[cc_javascript]
scala> def id(x: Int) = x
id: (x: Int)Int

scala> somaEntreComF(1, 10, id)
res4: Int = 55
[/cc_javascript]

Só para se ter uma idéia da flexibilidade que conseguimos com Scala, vamos supor que queremos poder escolher se calcularemos a soma ou a subtração dos valores entre os dois passados. Antes de olhar uma maneira de fazer, tente implementar do seu jeito. A resposta está no fim do post (1).

Funções anônimas


Existe um problema com a função id que fica bem claro quando queremos utilizá-la em outros lugares. Com a função definida da maneira que definimos tivemos que fixar um tipo, no caso Int, e se fomos usá-la em um lugar como String por exemplo, não será possível. Poderíamos definir uma função id com o tipo Any, mas neste caso não conseguiríamos utilizá-la em nosso exemplo, pois a operação de + não está definida em Any.

Para facilitar nossa vida, e para evitar a definição de funções que serão pequenas e para casos específicos, existe um açucar sintático em Scala chamado de funções anônimas. Como o próprio nome diz, nada mais são do que funções sem um nome definido. Em nosso exemplo não é necessário criar a função id se fizermos a seguinte chamada:

[cc_javascript]
scala> somaEntreComF(1, 10, (x: Int) => x)
res6: Int = 55
[/cc_javascript]

Repare que a sintaxe é exatamente a mesma de quando definimos uma função da maneira convencional, apenas não explicitamos um nome para ela. Podemos usar a mesma idéia inclusive para o segundo exemplo com a soma dos quadrados:

[cc_javascript]
scala> somaEntreComF(1, 10, (x: Int) => x * x)
res7: Int = 385
[/cc_javascript]

Como exercício, pegue o código que você criou no primeiro exercício e faça a chamada apenas utilizando funções anônimas, sem que seja preciso definir por exemplos uma função soma ou multiplica para somar ou multiplicar os resultados. A resposta está no fim do post também (2).

Mas lembrando que em Scala o compilador tenta sempre ser nosso amigo :), temos algumas definições na assinatura de somaEntreComF que garantem que a função passada como parâmetro tem que receber um Int e devolver outro. Portanto é possível saber o tipo do argumento passado em nossa função anônima. Outra consideração, é que como temos apenas um parâmetro na função passada, não precisamos dos () para explicitar o parâmetro. Ficamos com uma chamada bem mais limpa, tanto para a função id como quadrado:

[cc_javascript]
scala> somaEntreComF(1, 10, x => x)
res9: Int = 55

scala> somaEntreComF(1, 10, x => x * x)
res10: Int = 385
[/cc_javascript]

Faça o mesmo com nosso exercício e remova os tipos dos argumentos em uma chamada para a função, a resposta está no final do post (3).

Currying


Vimos funções que recebem outras funções como parâmetro, e vimos também que existe um outro tipo de higher-order functions, que são as funções que devolvem outra função como parâmetro. Para fazer o currying de parâmetros em funções precisamos utilizar esse tipo de funções. Vamos usar nosso exemplo implementado até aqui para entender melhor do que se trata.

Imagine que vamos fazer chamadas seguidas de nossa função somaEntreComF para intervalo de números diferentes, porém com o mesma função a ser aplicada entre os números. Teríamos algo como o seguinte:

[cc_javascript]
scala> somaEntreComF(5, 15, x => x)
res13: Int = 110

scala> somaEntreComF(10, 20, x => x)
res14: Int = 165
[/cc_javascript]

Nesse caso estamos repetindo o parâmetro da função para ambas as chamadas, e talvez iremos perceber que iremos repetir muitas outras vezes. Para isso existe uma funcionalidade em Scala (não é exclusivo de Scala) que permite uma abstração maior sobre esse código. Ao invés de devolvermos o resultado da operação, vamos devolver uma função, que ao executada com os parâmetros que você precise no momento, lhe dê o resultado. Para isso basta implementar a seguinte função:

[cc_javascript]
scala> def somaEntre(f: Int => Int): (Int, Int) => Int = { def retFunc(a: Int, b: Int): Int = if (a > b) 0 else f(a) + retFunc(a+1, b); retFunc }
somaEntre: (f: (Int) => Int)(Int, Int) => Int
[/cc_javascript]

Definimos uma função somaEntre que recebe uma outra função como parâmetro que recebe um Int e devolve outro Int, e estamos dizendo que a função somaEntre devolve outra função que recebe dois parâmetros do tipo Int e devolve um Int. Na implementação de somaEntre estamos definindo um função que será devolvida, lembrando que funções são elementos de primeira classe em Scala, portanto podemos criá-los dentro de outras funções, e implementando a função que será devolvida da mesma maneira que nossa função somaEntreComF. A diferença, é que agora estamos fixando a função f que é executada sobre os números do intervalo. Tal técnica é chamada de currying.

Para aplicar nossa nova função podemos executar o seguinte trecho de código:

[cc_javascript]
scala> def somaIdEntre = somaEntre(x => x)
somaIdEntre: (Int, Int) => Int

scala> somaIdEntre(1, 10)
res15: Int = 55

scala> def somaQuadradoEntre = somaEntre(x => x * x)
somaQuadradoEntre: (Int, Int) => Int

scala> somaQuadradoEntre(1, 10)
res16: Int = 385
[/cc_javascript]

Agora temos duas funções diferentes que recebem o intervalo de números para serem executadas, e aplicam a função específica de cada uma, só que dessa vez se aparecer uma outra maneira de tratar os números antes de somá-los, basta executar somaEntre passando a nova operação e a função nova já estará pronta sem que tenha que se definir sempre a nova operação para executar a soma.

Podemos executar as funções com currying sem definir uma outra função para armazená-la, e a sintaxe é bem intuitiva:

[cc_javascript]
scala> somaEntre(x => x)(1, 10)
res17: Int = 55

scala> somaEntre(x => x * x)(1, 10)
res18: Int = 385
[/cc_javascript]

Isso é possível pois a execução das funções é feita da esquerda para a direita, portanto 1 e 10 são parâmetros passados para a função devolvida por somaEntre.

Como exercício implemente nossa função de exercício de maneira que seja feito o currying da função executada entre os números do intervalo. Para isso utilize nossa função definida neste último exemplo somaEntre. Resposta no final do post (4), e sim a resposta é meio feia, mas a idéia é entender o conceito ;).

Próximos passos


No próximo post iremos tratar mais a fundo de classes e objetos. Para isso usaremos a IDE de Scala disponível como plugin do Eclipse que pode ser encontrada em http://www.scala-ide.org/.

É importante que a IDE esteja funcionando corretamente para facilitar a compreensão do comportamento e da organização de classes e objetos em Scala.

Respostas dos exercícios:

Resposta (1):
[cc_javascript]
scala> def aplicaFEntreComG(f: (Int, Int) => Int, a: Int, b: Int, g: Int => Int): Int = if (a > b) 0 else f(g(a), aplicaFEntreComG(f, a+1, b, g))
aplicaFEntreComG: (f: (Int, Int) => Int,a: Int,b: Int,g: (Int) => Int)Int

scala> def soma(x: Int, y: Int) = x + y
soma: (x: Int,y: Int)Int

scala> aplicaFEntreComG(soma, 1, 10, quadrado)
res5: Int = 385
[/cc_javascript]

Resposta (2):
[cc_javascript]
scala> aplicaFEntreComG((a: Int, b: Int) => a + b, 1, 10, (x: Int) => x * x)
res8: Int = 385
[/cc_javascript]

Resposta (3):
[cc_javascript]
scala> aplicaFEntreComG((a, b) => a + b, 1, 10, x => x)
res11: Int = 55

scala> aplicaFEntreComG((a, b) => a + b, 1, 10, x => x * x)
res12: Int = 385
[/cc_javascript]

Resposta (4):
[cc_javascript]
scala> def aplicaFEntre(f: (Int, Int) => Int):((Int => Int) => (Int, Int) => Int) = {
def retFunc(g: Int => Int): (Int, Int) => Int = {
def ret(a:Int, b: Int): Int =
if (a > b)
0
else
f(g(a), ret(a+1, b));
ret
};
retFunc
}
aplicaFEntre: (f: (Int, Int) => Int)((Int) => Int) => (Int, Int) => Int

scala> aplicaFEntre((x, y) => x + y)(x => x)(1, 10)
res19: Int = 55

scala> aplicaFEntre((x, y) => x + y)(x => x * x)(1, 10)
res20: Int = 385

scala> def somaAplicaFEntre = aplicaFEntre((x, y) => x + y)
somaAplicaFEntre: ((Int) => Int) => (Int, Int) => Int

scala> def somaQuadradoEntre = somaAplicaFEntre(x => x * x)
somaQuadradoEntre: (Int, Int) => Int

scala> somaQuadradoEntre(1, 10)
res22: Int = 385
[/cc_javascript]

Dúvidas, críticas e sugestões são bem vindas, como de praxe 🙂 .
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: