You are on page 1of 48

3Programação com funções

Este capítulocapas

Compreendendo e representando funções


Usando lambdas
Usando funções de ordem superior e curried
Usando os tipos certos

No capítulo 1, você aprendeu que uma das técnicas mais importantes para uma
programação mais segura é separar claramente as partes dos programas que não
dependem de nada além dos dados de entrada da parte que depende do estado do
mundo externo. Com programas compostos porsubprogramas (chamados procedi-
mentos, métodos ou funções), essa separação também se aplica transitivamente a
esses subprogramas. Em Java, eles são chamados de métodos, mas em Kotlin são
chamados de funções, o que provavelmente é mais apropriado porque função é
um termo matemático com uma definição precisa (como é comum na matemá-
tica). As funções Kotlin podem ser comparadas a funções matemáticas quando
não têm nenhum efeito além de retornar um valor que depende apenas de seu ar-
gumento. Tais funções são frequentemente chamadasfunções puras por progra-
madores. Para escrever programas mais seguros, portanto, deve-se

Use funções puras para cálculos


Use efeitos puros para disponibilizar o resultado de cálculos para o mundo
externo

Funções puras são necessárias para garantir que sempre retornem o mesmo re-
sultado dado o mesmo argumento. Caso contrário, seu programa serianondetermi-
nistic , o que significa que você nunca pode verificar se o programa está correto.
Efeitos puros podem parecer menos importantes, mas se você pensar sobre isso,
efeitos impuros são efeitos que incluem cálculos, e essas partes computacionais
não são facilmente testáveis, portanto devem ser colocadas em funções (puras)
separadas.

Para alguns programadores que procuram maneiras de escrever programas mais


seguros, diferenciar claramente funções puras de efeitos puros e nunca misturá-
los não é o objetivo final. As funções puras são fáceis de testar, mas os efeitos pu-
ros não. É possível mudar isso? Sim, tratando os efeitos de uma forma diferente.
Isso é o que a programação funcional pura faz. Neste paradigma, tudo é uma fun-
ção. Valores são funções, funções são funções e efeitos são funções. Em vez de
aplicar efeitos, os programadores funcionais usam funções que retornam dados
que representam o efeito pretendido em uma forma não avaliada. Nesse para-
digma de programação, tudo é uma função e tudo são dados porque não há dife-
rença entre dados e funções.

Por que não usar simplesmente programação funcional pura? Embora seja possí-
vel, às vezes é difícil se você não estiver usando uma linguagem projetada especi-
ficamente para isso. Linguagens como Java e Kotlin oferecem muitas ferramentas
para a adoção de algumas técnicas de programação funcional, mas o suporte de
efeitos funcionais é limitado. No capítulo 12, mostrarei como processar os efeitos
funcionalmente porque essa técnica é usada em todos os tipos de programação
(muitas vezes sem que o programador perceba). Embora isso seja algo que você
pode fazer, e algo que você deva fazer às vezes, provavelmente nem sempre deve
tratar os efeitos dessa maneira.

Neste capítulo, você aprenderá como usar funções puras para cálculos e como
usar versões curried de funções. Você pode ter problemas para entender algumas
partes do código apresentado neste capítulo. Isso é esperado porque é difícil intro-
duzir funções sem usar outras construções funcionais como List , Option e ou-
tras. Seja paciente. Todos os componentes inexplicados serão discutidos nos capí-
tulos seguintes.

3.1 O que é uma função?

Nesta seção, vamos dar uma olhada mais profunda nas funções. Uma função é
principalmente um conceito matemático. Ele representa um relacionamento en-
tre um conjunto de origem (chamado dedomínio da função ) e um conjunto de
destino (chamado de domínio da função ) . O domínio e o contradomínio não pre-
cisam ser distintos. Uma função, por exemplo, pode ter o mesmo conjunto de nú-
meros inteiros para seu domínio e contradomínio.

3.1.1 Compreendendo a relação entre dois conjuntos de funções

Paraser uma função, um relacionamento deve atender a uma condição: todos os


elementos do domínio devem ter um e apenas um elemento correspondente no
contradomínio, conforme mostra a figura 3.1 . Isso tem algumas implicações
interessantes:

Não pode haver elementos no domínio sem valor correspondente no


contradomínio.
Não pode haver dois elementos no contradomínio correspondentes ao mesmo
elemento do domínio.
Pode haver elementos no contradomínio sem nenhum elemento correspon-
dente no conjunto de origem.
Pode haver elementos no contradomínio com mais de um elemento correspon-
dente no conjunto de origem.

NOTA O conjunto de elementos do contradomínio que possuem um elemento cor-


respondente no domínio é chamado de imagem da função.

A Figura 3.1 ilustra uma função. Você pode, por exemplo, definir a função

Figura 3.1 Todos os elementos do domínio de uma função devem ter um e apenas um elemento correspon-
dente no contradomínio.

f(x) = x + 1

onde x é um inteiro positivo. Esta função representa a relação entre cada inteiro
positivo e seu sucessor. Você pode dar qualquer nome a esta função. Em particu-
lar, você pode dar um nome que o ajudará a lembrar o que é, como:

sucessor(x) = x + 1

Isso pode parecer uma boa ideia, mas você não deve confiar cegamente em um
nome de função. Você poderia alternativamente ter definido a função da seguinte
forma:
predecessor(x) = x + 1

Nenhum erro ocorre aqui porque não existe nenhum relacionamento obrigatório
entre um nome de função e a definição da função. Mas seria uma má ideia usar
esse nome. Na verdade, o nome que você dá a uma função não faz parte da fun-
ção. É uma maneira conveniente de se referir a ele.

Observe que estou falando sobre o que é uma função (sua definição) e não o que
ela faz . Uma função não faz nada. o successor função não adiciona 1 ao seu ar-
gumento x . Você pode adicionar 1 a um inteiro para calcular seu sucessor, mas a
successor função não faz isso. A successor função é apenas a relação entre
um inteiro e o próximo em ordem crescente:

sucessor(x)

A expressão successor(x) é equivalente apenas a x + 1 , o que significa que


cada vez que você encontrar successor(x) , poderá substituí-la por (x + 1) .
Observe os parênteses usados ​para isolar a expressão. Eles não são necessários
quando a expressão é usada sozinha, mas podem ser necessários em algunsoca-
siões.

3.1.2 Uma visão geral das funções inversas em Kotlin

UMAfunção pode ou não ter uma função inversa. Se f(x) é uma função de A a
B ( A sendo o domínio e B o contradomínio), a função inversa é notada como f -
1 (x) e tem B como domínio e A como contradomínio. Se você representar o tipo
da função como A –> B , a função inversa (se existir) terá o tipo B –> A .

OBSERVAÇÃO Kotlin usa uma sintaxe ligeiramente diferente para representar ti-
pos de função, com o tipo de origem entre parênteses: (A) -> B e (B) -> A . A
partir de agora, usarei a sintaxe do Kotlin.

O inverso de uma função é uma função se atender aos mesmos requisitos de qual-
quer função: ter um e apenas um valor de destino para cada valor de origem.
Como resultado, o inverso de successor(x) , um relacionamento que você cha-
mará predecessor(x) (embora você também possa chamá-lo de xyz ), não é
uma função em N (o conjunto de inteiros positivos incluindo 0) porque 0 não tem
predecessor em N . Por outro lado, se successor(x) for considerado com o con-
junto de inteiros com sinal (positivos e negativos, indicados como Z ), o inverso de
successor é uma função.
Algumas outras funções simples não têm inversa. Por exemplo, a função

f(x) = (2 * x)

não tem inverso se definido de N a N porque o inverso de f seria f'(x) = x /


2 , que não pertence a N quando x é ímpar. Tem um inverso se você defini-lo
como uma função de N para o conjunto de parinteiros.

3.1.3 Trabalhando com funções parciais

Paraser uma função, um relacionamento tem dois requisitos:

1. Deve ser definido para todos os elementos do domínio.


2. Nenhum elemento do domínio pode ter relação com mais de um elemento do
contradomínio.

Um relacionamento que não é definido para todos os elementos do domínio, mas


que atende ao restante do requisito, geralmente é chamado de função parcial . E,
por analogia, uma função não parcial éàs vezes chamada de função total , que é
redundante. Estritamente falando, uma função verdadeira é sempre total, e uma
função parcial não é uma função. Mas é útil conhecer o vocabulário usado por
muitos programadores.

A relação predecessor(x) é uma função parcial em N (o conjunto de inteiros


positivos mais 0), mas é uma função total em N* , que é o conjunto de inteiros po-
sitivos sem 0, e seu contradomínio é N . As funções parciais são importantes na
programação porque muitos bugs são resultado do uso de uma função parcial
como se fosse uma função total. Por exemplo, a relação f(x) = 1/x é uma fun-
ção parcial de N a Q (os números racionais) porque não é definida para 0. É uma
função total de N* a Q, mas também é uma função total de N a (Q mais erro). Ao
adicionar um elemento ao contradomínio (a condição de erro), você pode trans-
formar a função parcial em total. Mas para fazer isso, a função precisa de uma
maneira de retornar um erro. Você consegue ver uma analogia com programas de
computador? Mais adiante neste livro, você verá que transformar funções parci-
ais em funções totais é uma parte importante da segurança.programação.

3.1.4 Entendendo a composição da função

Funçõessão blocos de construção que podem ser compostos para construir outras
funções. A composição das funções f e g é indicada como f ° g , que se lê como
f round g . Se f(x) = x * 2 e g(x) = x + 1 , então
f ° g(x) = f(g(x)) = f(x + 1) = (x + 1) * 2

As duas notações f ° g (x) e f(g(x)) são equivalentes. Mas escrever uma


composição como f(g(x)) implica usar x como um espaço reservado para o ar-
gumento. Usando a f ° g notação, você pode expressar uma composição de fun-
ção sem usar esse espaço reservado. Se você aplicar esta função a 5 , obterá o
seguinte:

f ° g (5) = f(g(5)) = f(5 + 1) = 6 * 2 = 12

É interessante notar que f ° g geralmente é diferente de g ° f , embora às ve-


zes possam ser equivalentes. Por exemplo:

g ° f (5) = g(f(5)) = g(5 * 2) = 10 + 1 = 11

As funções são aplicadas na ordem inversa da escrita. Se você escrever f ° g ,


primeiro se inscreve g e depois f .

3.1.5 Usando funções de vários argumentos

Desta formaaté agora falei apenas sobre funções de um argumento. E as funções


de vários argumentos? Simplificando, não existe uma função de vários argumen-
tos. Lembra da definição de uma função? Uma função é um relacionamento entre
um conjunto de origem e um conjunto de destino. Não é um relacionamento entre
dois ou mais conjuntos de origem e um conjunto de destino. Uma função não
pode ter vários argumentos. Mas o produto de dois conjuntos é em si um con-
junto. Uma função de tal produto de conjuntos em um conjunto pode parecer uma
função de vários argumentos. Vamos considerar a seguinte função:

f(x, y) = x + y

Pode haver uma relação entre N x N e N ; nesse caso, é uma função. Mas tem
apenas um argumento que é um elemento de N x N . N x N é o conjunto de to-
dos os pares possíveis de números inteiros. Um elemento desse conjunto é um par
de inteiros, e um par é um caso especial do conceito de tupla mais geral usado
para representar combinações de vários elementos. Um par éuma tupla de dois
elementos.

As tuplas geralmente são indicadas entre parênteses, assim (3, 5) como uma tu-
pla e um elemento de N x N . A função f pode ser aplicada a esta tupla:
f((3, 5)) = 3 + 5 = 8

Nesse caso, você pode, por convenção, simplificar a escrita removendo um con-
junto de parênteses:

f(3, 5) = 3 + 5 = 8

No entanto, ainda é uma função de uma tupla e não uma função de duasargumen-
tos.

3.1.6 Funções de curry

Funçõesde tuplas podem ser pensados ​de forma diferente. A função f(3,
5) pode ser considerada como uma função de N um conjunto de funções de N . O
exemplo anterior, portanto, poderia ser reescrito como

f(x)(y) = x + y

Nesse caso, você pode escrever

f(x) = g

o que significa que o resultado da aplicação da função f ao argumento x é uma


nova função g . Aplicando esta g função para y dá o seguinte:

g(y) = x + y

Ao aplicar g , x não é mais uma variável, mas umconstante . Não depende do ar-
gumento ou de qualquer outra coisa. Se você aplicar isso a (3, 5) , obterá o
seguinte:

f(3)(5) = g(5) = 3 + 5 = 8

A única novidade aqui é que o contradomínio de f é um conjunto de funções em


vez de um conjunto de números. O resultado da aplicação f a um número inteiro
é uma função. O resultado da aplicação dessa função resultante a um número in-
teiro é um número inteiro.
f(x)(y) é a forma atual da função f(x, y) . Aplicar essa transformação a uma
função de uma tupla (que você pode chamar de função de vários argumentos, se
preferir) é chamado de currying , em homenagem ao matemático Haskell Curry,
embora não tenha sido ele o inventor dissotransformação.

3.1.7 Usando funções parcialmente aplicadas

oA forma irregular da função de adição pode não parecer natural e você pode se
perguntar se ela corresponde a algo no mundo real. Com a versão curry, você está
considerando ambos os argumentos separadamente. Um dos argumentos é consi-
derado primeiro e aplicar a função a ele fornece uma nova função. Essa nova fun-
ção é útil por si só ou é simplesmente uma etapa no cálculo global?

No caso de uma adição, currying não parece tão útil. E, a propósito, você poderia
começar com qualquer um dos dois argumentos e não faria diferença. A função
intermediária seria diferente, mas não o resultado final.

Para ajudá-lo a entender a utilidade do currying, imagine que você está viajando
para um país estrangeiro, usando uma calculadora portátil (ou seu smartphone)
para converter de uma moeda para outra. Você prefere ter que digitar a taxa de
conversão toda vez que quiser calcular um preço ou prefere colocar a taxa de
conversão na memória? Qual solução seria menos propensa a erros? Considere
uma nova função de um par de valores:

f(taxa, preço) = preço / 100 * (100 + taxa)

Essa função parece ser equivalente a isso:

g(preço, taxa) = preço / 100 * (100 + taxa)

Vamos agora considerar as versões ao curry dessas duas funções:

f(taxa)(preço) g(preço)(taxa)

Você sabe disso f e g são funções. Mas o que são f(rate) e g(price) ? Sim,
com certeza, esses são os resultados da aplicação f de rate e g para price .
Mas quais são os tipos desses resultados?

f(rate) é uma função de uma taxa para um preço. Se rate = 9 , esta função
aplica uma taxa de 9% a um preço, retornando um novo preço. Você poderia cha-
mar a função resultante apply9percentTax(price) e provavelmente seria uma
ferramenta útil porque a taxa de imposto não muda com frequência.

Por outro lado, g(price) é uma função de uma taxa para um preço. Se o preço
for $ 100, ele fornecerá uma nova função aplicando um preço de $ 100 a um im-
posto variável. Pode não parecer tão útil, embora isso dependa do problema que
você precisa resolver. Se o problema for calcular quanto um montante fixo cres-
cerá dada uma taxa de juros variável, esta versão seria mais útil.

Funções como f(rate) e g(price) às vezes são chamadas de funções parcial-


mente aplicadas, em referência aos formulários f(rate, price) e g(price,
rate) . Funções parcialmente aplicadas podem ter grandes consequências em re-
lação à avaliação de argumentos. Voltarei a este assunto na seção3.2.4.

3.1.8 As funções não têm efeitos

Lembrarque funções puras apenas retornam um valor e não fazem mais nada.
Eles não alteram nenhum elemento do mundo externo (com o exterior sendo rela-
tivo à função em si), não alteram seus argumentos e não explodem (ou lançam
uma exceção ou qualquer outra coisa) se ocorrer um erro. Eles podem, no en-
tanto, retornar uma exceção ou qualquer outra coisa, como uma mensagem de
erro. Mas eles devem devolvê-lo, não jogá-lo, nem registrá-lo, nem imprimi-lo. Eu
entro em mais detalhes sobre funções puras emseção 3.2.4.

3.2 Funções em Kotlin

No capítulo 1, você usou o que Kotlin chama de funções, mas que são, na verdade,
métodos. Em muitas linguagens, métodos são uma forma de representar (até
certo ponto) funções. Conforme discutido no capítulo 2, Kotlin chama funções de
métodos e usa a palavra-chave fun paraintroduzir funções. Mas há dois proble-
mas com essas funções . Dados e funções são fundamentalmente a mesma coisa.
Qualquer dado é, de fato, uma função, e qualquer função é um dado.

3.2.1 Entendendo as funções como dados

Funçõestêm tipos, como qualquer outro dado, como String ou Int , e podem ser
atribuídos a uma referência. Os tipos de dados também podem ser passados ​como
argumentos para outras funções e podem ser retornados por funções, como você
verá em breve. As funções também podem ser armazenadas em estruturas de da-
dos como listas ou mapas, ou mesmo em um banco de dados. Mas as funções de-
claradas com fun (como os métodos Java) não podem ser manipuladas dessa ma-
neira. Kotlin possui todos os mecanismos necessários para transformar qualquer
método em uma verdadeira função.

3.2.2 Entendendo os dados como funções

Lembra da definição de uma função? Uma função é um relacionamento entre um


conjunto de origem e um conjunto de destino que deve atender a algum requisito
específico. Agora imagine uma função tendo o conjunto de todos os elementos
possíveis como seu conjunto de origem e o inteiro 5 como seu conjunto de destino.
Acredite ou não, esta é uma função que cumpre todos os requisitos da definição
de uma função. É um tipo específico de função cujo resultado não depende de seu
argumento. Isso é chamado defunção constante . Você não precisa especificar ne-
nhum argumento para chamá-lo e não há necessidade de dar um nome especial.
Vamos chamá-lo de 5. Isso é pura teoria, mas vale a pena lembrar. será útilmais
tarde.

3.2.3 Usando construtores de objetos como funções

Objetosconstrutores são, de fato, funções. Ao contrário do Java, que usa uma sin-
taxe especial para criar objetos, Kotlin usa a sintaxe de função para isso. (Mas não
é a sintaxe que torna os objetos funções; os objetos Java também são funções.) Em
Kotlin, você pode obter uma instância de objeto usando o nome da classe seguido
pela lista de argumentos do construtor entre parênteses, assim:

val pessoa = Pessoa("Elvis")

Isso levanta uma questão importante. Eu disse que uma função pura deve sempre
retornar o mesmo valor para o mesmo argumento. Você pode se perguntar se os
construtores são funções puras. Considere o seguinte exemplo:

val elvis = Pessoa("Elvis")


val theKing = Person("Elvis")

Dois objetos são criados com o mesmo argumento, então ele deve retornar o
mesmo valor.

val elvis = Pessoa("Elvis")


val theKing = Person("Elvis")

println(elvis == theKing) // deve retornar "true"


O teste de igualdade retorna true apenas se um equals função foi definida de
acordo. Este será o caso se Person for uma classe de dados (como visto no capí-
tulo 2):

classe de dados Person(val name: String)

Caso contrário, cabe a você definirigualdade.

3.2.4 Usando funções divertidas do Kotlin

Vocêpode se lembrar que mencionei funções puras antes. Seja qual for a maneira
como você as declara, as funções Kotlin declaradas com a palavra- fun chave não
têm garantia de serem funções reais. O que os programadores chamam de fun-
ções raramente são funções reais que os programadores criaram uma expressão
para significar funções reais. Eles os chamam de funções puras . (Por analogia, as
outras funções são chamadasfunções impuras .) Nesta seção, explicarei o que
torna uma função pura e darei alguns exemplos de funções puras.

Aqui está o que é necessário para uma função/método ser uma função pura:

Não deve modificar nada fora da função. Nenhuma mutação interna pode ser
visível do lado de fora.
Ele não deve mudar seu argumento.
Não deve lançar erros ou exceções.
Deve sempre retornar um valor.
Quando chamado com o mesmo argumento, deve retornar sempre o mesmo
resultado.

Vejamos um exemplo de puro efunções impuras conforme mostrado na listagem a


seguir.

Listagem 3.1 Funções puras e impuras

class FunFunções {

var percent1 = 5
var privado percent2 = 9
val percent3 = 13

fun add(a: Int, b: Int): Int = a + b

fun mult(a: Int, b: Int?): Int = 5


fun div(a: Int, b: Int): Int = a / b

fun div(a: Duplo, b: Duplo): Duplo = a / b

fun applyTax1(a: Int): Int = a / 100 * (100 + percent1)

fun applyTax2(a: Int): Int = a / 100 * (100 + percent2)

fun applyTax3(a: Int): Int = a / 100 * (100 + percent3)

fun append(i: Int, list: MutableList<Int>): List<Int> {


list.add(i)
lista de retorno
}

fun append2(i: Int, lista: List<Int>) = lista + i


}

Você pode dizer quais dessas funções/métodos representam funções puras? Pense
por alguns minutos antes de ler a resposta a seguir. Pense em todas as condições e
todo o processamento feito dentro das funções. Lembre-se que o que conta é o
que é visível de fora. Não se esqueça de considerar condições excepcionais:

fun add(a: Int, b: Int): Int = a + b

O primeiroA função, add , é uma função pura porque sempre retorna um valor
que depende apenas de seus argumentos. Não muda seus argumentos e não inte-
rage de forma alguma com o mundo exterior. Esta função pode causar um erro se
a soma a + b ultrapassar o valor máximo Int . Mas não lançará uma exceção. O
resultado será incorreto (um valor negativo); este é outro problema. O resultado
deve ser o mesmo sempre que a função for chamada com os mesmos argumentos.
Isso não significa que o resultado deve ser exato!

Exatidão

O termo exatonão significa nada por si só. Geralmente significa que ele se encaixa
no que é esperado. Para dizer se o resultado da implementação de uma função é
exato, você deve conhecer a intenção do implementador. Normalmente, você não
terá nada além do nome da função para determinar a intenção, o que pode ser
uma fonte de mal-entendidos.

A segunda função
fun mult(a: Int, b: Int?): Int = 5

é uma função pura. O fato de sempre retornar o mesmo valor quaisquer que se-
jam os argumentos é irrelevante, assim como o fato de que o nome da função não
tem nada a ver com o que a função retorna. Esta função é uma constante.

o div A função trabalhando Int não é uma função pura porque lançará uma ex-
ceção se o divisor for 0 :

fun div(a: Int, b: Int): Int = a / b

Para criar div uma função pura, você pode testar o segundo parâmetro e retor-
nar um valor se for nulo. Teria que ser um Int , então seria difícil encontrar um
valor significativo, mas esse é outro problema. A div função trabalhando em
doubles é uma função pura porque dividir por 0.0 não lançará uma exceção, mas
retornará Infinity , que é uma instância de Double :

fun div(a: Duplo, b: Duplo): Duplo = a / b

Infinity está absorvendo para adição, o que significa que adicionar


Infinity a qualquer coisa retorna Infinity . Não é totalmente absorvente
para multiplicação porque multiplicar Infinity por 0.0 resulta em NaN (não é
um número). Apesar do nome, NaN é uma instância de Double . Vamos conside-
rar o próximo trecho de código:

var percent1 = 5
fun applyTax1(a: Int): Int = a / 100 * (100 + percent1)

O applyTax1 método não parece ser uma função pura porque seu resultado de-
pende do valor de percent1 , que é público, e pode ser modificado entre duas
funçõeschamadas. Como consequência, duas chamadas de função com o mesmo
argumento podem retornar valores diferentes: percent1 pode ser considerado
um parâmetro implícito, mas este parâmetro não é avaliado ao mesmo tempo que
o argumento explícito. Isso não é um problema se você usar o percent1 valor
apenas uma vez dentro da função, mas se você o ler duas vezes, ele poderá mudar
entre as duas operações de leitura. Se você precisar usar o valor duas vezes, de-
verá lê-lo uma vez e mantê-lo em uma variável local. Isso significa que o método
applyTax1 é uma função pura do par (a, percent1) , mas não é uma função
pura de a .
Neste exemplo, a própria classe pode ser considerada um argumento implícito su-
plementar porque todas as suas propriedades são acessíveis de dentro da função.
Esta é uma noção importante. Todos os métodos/funções de instância podem ser
substituídos por métodos/funções que não sejam de instância adicionando um ar-
gumento do tipo da classe envolvente. A applyTax1 função pode ser reescrita
fora do FunFunctions (ou mesmo dentro) como

fun applyTax1(ff: FunFunctions, a: Int): Int = a / 100 * (100 + ff.percent1)

Essa função pode ser chamada de dentro da classe, passando uma referência
this para os argumentos, como applyTax1(this, a) . Ele também pode ser
chamado de fora porque é público, desde que uma referência a uma FunFuncti-
ons instância esteja disponível. Aqui, applyTax1 é uma função pura do par
(this, a) :

var privado percent2 = 9

fun applyTax2(a: Int): Int = a / 100 * (100 + percent2)

O resultado da applyTax2 função depende do valor de percent2 , que é mutá-


vel (declarado com var ). O resultado applyTax2 varia se percent2 for modifi-
cado. No entanto, nenhum código realmente altera essa variável, portanto,
applyTax2 é uma função pura. No entanto, é inseguro porque pode se tornar im-
puro sem qualquer modificação da própria função adicionando outra função mu-
tating percent2 . Esta é uma boa razão para sempre tornar tudo imutável, a me-
nos que você realmente precise de mutações. Por padrão, sempre use o val pala-
vra-chave:

val percent3 = 13
fun applyTax3(a: Int): Int = a / 100 * (100 + percent3)

A applyTax3 função é uma função pura de a porque ao contrário de


applyTax1 , ela usa percent3 , que é imutável:

fun append1(i: Int, lista: MutableList<Int>): List<Int> {


list.add(i)
lista de retorno
}
o append1 A função muda seu argumento antes de retorná-lo, e essa mutação é
visível de fora da função, portanto não é uma função pura:

fun append2(i: Int, lista: List<Int>) = lista + i

A append2 função parece adicionar um elemento ao seu argumento. Mas este


não é o caso. A expressão list + i é avaliada para uma nova lista (imutável)
contendo os mesmos elementos da lista original mais o elemento adicionado.
Nada é mutante. append2 é um purofunção.

3.2.5Usando notação de objeto versus notação funcional

você temvisto que funções de instância acessando propriedades de classe podem


ser consideradas como tendo a instância de classe envolvente como um parâme-
tro implícito. As funções que não acessam a instância de classe envolvente podem
ser movidas com segurança para fora da classe. Você pode colocar essas funções
no objeto complementar (tornando-as aproximadamente equivalentes aos méto-
dos estáticos Java) ou no nível do pacote, fora de qualquer classe. Você também
pode colocar funções que acessam a instância delimitadora no objeto complemen-
tar ou no nível do pacote se seu parâmetro implícito (a instância delimitadora) for
explícito. Considere a Payment classe do capítulo 1:

classe Payment(val creditCard: CreditCard, valor val: Int) {


fun combine(pagamento: Pagamento): Pagamento =
if (creditCard == payment.creditCard)
Pagamento(cartão de crédito, valor + pagamento.valor)
senão
throw IllegalStateException("Cartas não coincidem.")

objeto complementar {
fun groupByCard(pagamentos: List<Pagamento>): List<Pagamento> =
Payments.groupBy { it.creditCard }
.valores
.map { it.reduce(Pagamento::combinar) }
}
}

a combine funçãoacessa a classe envolvente creditCard e os amount campos.


Como resultado, ele não pode ser colocado fora da classe ou no objeto comple-
mentar. Esta função tem a classe envolvente como um parâmetro implícito. Você
poderia tornar esse parâmetro explícito, o que permitiria definir a função no ní-
vel do pacote ou no objeto complementar:
fun combine(pagamento1: Pagamento, pagamento2: Pagamento): Pagamento =
if (pagamento1.creditCard == pagamento2.creditCard)
Pagamento(pagamento1.cartão de crédito, pagamento1.valor + pagamento2.valor)
senão
throw IllegalStateException("Cartas não coincidem.")

Uma função declarada no objeto complementar ou no nível do pacote permite ga-


rantir que não haja acesso indesejado ao escopo delimitador. Mas isso muda a
maneira como a função pode ser usada. Se usada de dentro da classe, a função
pode ser chamada passando a this referência:

val novoPagamento = combine(este, outroPagamento)

Isso faz pouca diferença, mas tudo muda quando você precisa compor chamadas
de função. Se você precisar combinar vários pagamentos, uma função de instân-
cia escrita como

fun combine(pagamento: Pagamento): Pagamento =


if (creditCard == payment.creditCard)
Pagamento(cartão de crédito, valor + pagamento.valor)
senão
throw IllegalStateException("Cartas não coincidem.")

pode ser usado com notação de objeto como esta:

val novoPagamento = pagamento1.combine(pagamento2).combine(pagamento2)

Isso é muito mais fácil de ler do que isso:

import ...Payment.Companion.combine

val newPayment = combine(combine(pagamento1, pagamento2), pagamento3)

Este exemplo lança uma exceção se os cartões de crédito não corresponderem,


mas isso ocorre apenas porque você ainda não aprendeu como lidar com erros
funcionalmente. Este será o assunto deCapítulo 7.

3.2.6 Usando funções de valor

EUdisse anteriormente que funções podem ser usadas como dados, mas este não é
o caso de funções declaradas com o fun palavra-chave. Kotlin permite tratar fun-
ções como dados. Kotlin tem tipos de função e as funções podem ser atribuídas a
referências dos tipos correspondentes da mesma forma que outros tipos de dados
são tratados. Considere a seguinte função:

divertido duplo(x: Int): Int = x * 2

A mesma função pode ser declarada como

val duplo: (Int) -> Int = { x -> x * 2 }

o tipo de double função é (Int) -> Int . À esquerda da seta está o tipo de parâ-
metro, entre parênteses. O tipo de retorno é indicado no lado direito da seta. A de-
finição da função vem após o sinal de igual. É colocado entre chaves e assume a
forma de uma expressão lambda.

No exemplo anterior, o lambda consiste em um nome dado ao parâmetro, uma


seta e uma expressão que ao ser avaliada será o resultado retornado pela função.
Aqui, essa expressão é simples, então pode ser escrita em uma única linha. Se a
expressão fosse mais complexa, poderia ser escrita em várias linhas. Nesse caso, o
resultado seria a avaliação da última linha como no exemplo a seguir:

val doubleThenIncrement: (Int) -> Int = { x ->


val duplo = x * 2
duplo + 1
}

A última linha não deve ter um return .

Funções de tuplas não são diferentes. Aqui está uma função que representa a adi-
ção de números inteiros:

val add: (Int, Int) -> Int = { x, y -> x + y }

Como você pode ver, no tipo de função, o argumento (único) é incluído entre pa-
rênteses. Por outro lado, nenhum parêntese pode ser usado para incluir o parâ-
metro na expressão lambda.

Quando o parâmetro não é uma tupla (ou, mais precisamente, quando é uma tu-
pla de um único elemento), você pode usar o nome especial it :
val duplo: (Int) -> Int = { it * 2 }

Isso simplifica a sintaxe, embora às vezes torne o código menos legível, especial-
mente quando várias implementações de função são aninhadas.

OBSERVAÇÃO Neste exemplo, double não é o nome da função. Uma função não
tem nome. Aqui, ele é atribuído a uma referência do tipo correspondente, para
que você possa manipulá-lo da mesma forma que faria com qualquer outro dado.
Quando você escreve

número val: Int = 5

você não diria que esse number é o nome de 5 . É o mesmo para funções.

Você pode se perguntar por que o Kotlin tem dois tipos de funções. Como funções
são valores, por que você deveria usar a fun palavra-chave para definir funções?

Como eu disse no início da seção 3.2.4, as funções definidas com fun não são real-
mente funções. Você pode chamá-los de métodos, subprogramas, procedimentos
ou qualquer outra coisa. Eles podem representar funções puras (sempre retor-
nando o mesmo valor para um determinado argumento sem nenhum outro efeito
visível de fora), mas você não pode tratá-los como dados.

Por que você deve usá-los? Porque fun as funções são muito mais eficientes. Eles
são uma otimização. Cada vez que você usar uma função apenas para passar um
argumento e obter o valor de retorno correspondente, estará usando a versão de-
finida com fun . Isso não é absolutamente obrigatório, mas é sábio.

Por outro lado, toda vez que você quiser usar uma função como dados (por exem-
plo, para passá-la para outra função - um argumento que você verá em breve), ou
para obtê-la como o valor de retorno de outra função, ou para armazenar em uma
variável, um mapa ou qualquer outra estrutura de dados, você estará usando uma
expressão do tipo função.

Você pode se perguntar como pode converter de uma forma para outra. É bem
simples. Você só precisará converter de fun para o tipo de expressão porque não
pode criar fun emtempo de execução.

3.2.7 Usando referências de função

KotlinGenericNameoferece a mesma funcionalidade que o Java oferece sob o


nome de referências de método. Mas, como você deve se lembrar, em Kotlin os
métodos são chamados de funções, então as referências de método são chamadas
de referências de função em Kotlin. Aqui está um exemplo de uso de uma
fun função em um lambda:

divertido duplo(n: Int): Int = n * 2

val mutliplyBy2: (Int) -> Int = { n -> double(n) }

Você também poderia ter escrito

val mutliplyBy2: (Int) -> Int = { double(it) }

Usar uma referência de função simplifica a sintaxe:

val mutliplyBy2: (Int) -> Int = ::duplo

Aqui, a double função é chamada no mesmo objeto, classe ou pacote da


mutliplyBy2 função. Se fosse uma função de instância de outra classe, você po-
deria usar a seguinte sintaxe, desde que tenha uma referência a uma instância
dessa classe disponível:

classe MinhaClasse {
divertido duplo(n: Int): Int = n * 2
}

val foo = MinhaClasse()


val mutliplyBy2: (Int) -> Int = foo::double

Alternativamente, se double estiver definido em outro pacote, você deverá im-


portá-lo:

importar outro.pacote.duplo

val mutliplyBy2: (Int) -> Int = ::duplo

No caso de uma função definida no objeto companheiro de uma classe (algo equi-
valente a um método estático Java), você pode importá-la ou usar a seguinte
sintaxe:

val mutliplyBy2: (Int) -> Int = (MinhaClasse)::duplo


Esta é uma abreviação para

val mutliplyBy2: (Int) -> Int = MyClass.Companion::double

Não se esqueça .Companion dos parênteses. Caso contrário, você obterá um re-
sultado completamente diferente:

classe MinhaClasse {
objeto complementar {
divertido duplo(n: Int): Int = n * 2
}
}

val mutliplyBy2: (MinhaClasse, Int) -> Int = MinhaClasse::duplo

Nesse caso, o multiplyBy2 tipo de função não é (Int) -> Int ,


mas (MyClass, Int) -> Int .

3.2.8 Funções de composição

Sevocê usa fun funções, compor parece simples:

quadrado divertido(n: Int) = n * n

divertido triplo(n: Int) = n * 3

println(quadrado(triplo(2)))

36

Mas isso não é composição de funções. Neste exemplo, você está compondo apli-
cativos de função. A composição de função é uma operação binária em funções,
assim como a adição é uma operação binária em números, então você pode com-
por funções programaticamente, usando outra função.

Exercício 3.1

Escreva a compose função (declarada com 'fun'), permitindo compor funções de


Int a Int .

NOTA AS soluções seguem cada exercício, mas primeiro você deve tentar resolver
o exercício sem olhar para a resposta. O código da solução também aparece no
site do livro. Este exercício é simples, mas alguns exercícios serão bastante difí-
ceis, por isso pode ser difícil evitar trapacear. Lembre-se de que quanto mais você
pesquisa, mais aprende.

Dica

A compose função levarácomo parâmetros duas funções do tipo (Int) ->


Int e retornará uma função do mesmo tipo. A compose função pode ser decla-
rada com fun , mas os parâmetros precisam ser valores. Você pode transformar
uma função myFunc declarado com fun em uma função de valor ( val ), prefi-
xando seu nome com :: .

Solução

Aqui está a solução usando um lambda:

fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int = { x -> f(g(x)) }

Alternativamente, pode ser simplificado como

fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int = { f(g(it)) }

Você pode usar esta função para compor square e triple :

val squareOfTriple = compose(::quadrado, ::triplo)


println(squareOfTriple(2))
36

Agora você pode começar a ver como esse conceito é poderoso! Mas dois grandes
problemas permanecem. A primeira é que suas funções só podem receber Int ar-
gumentos inteiros ( ) e retornar inteiros. Vamos lidar com issoprimeiro.

3.2.9 Reutilizando funções

Paratornar sua função mais reutilizável, você pode transformá-la em uma função
polimórfica usando parâmetros de tipo.

Exercício 3.2

Torne a compose função polimórfica usando parâmetros de tipo.


Dica

Declare os parâmetros de tipo entre a fun palavra-chave e o nome da função. Em


seguida, substitua os Int tipos pelos parâmetros corretos, cuidando da ordem de
execução. Lembre-se de que você está definindo f ° g , o que significa que deve
primeiro aplicar g e depois aplicar f ao resultado. Especifique o tipo de retorno.
Se os tipos não corresponderem, não será compilado.

Solução

O exercício não consiste em escrever a implementação da função. É a mesma im-


plementação que a não polimórfica. Trata-se de encontrar a assinatura certa:

fun <T, U, V> compose(f: (U) -> V, g: (T) -> U): (T) -> V = { f(g(it)) }

Aqui, você está vendo o benefício de um sistema de tipo forte com tipos parame-
trizados. Com parâmetros de tipo, você pode não apenas definir um compose fun-
ção que funciona para qualquer tipo (desde que os tipos correspondam), mas, ao
contrário da Int versão, você não pode errar. Se você alternar f e g , nãocompi-
lação mais longa.

3.3 Recursos de funções avançadas

Até agora, você viu como criar, aplicar e compor funções. Mas você não respon-
deu a uma pergunta fundamental: por que você precisa de funções representadas
como dados? Você não poderia simplesmente usar a fun versão? Antes de res-
ponder a esta pergunta, você precisa considerar o problema defunções multi-argu-
mento .

3.3.1 E as funções de vários argumentos?

Dentrona seção 3.1.5, eu disse que não existem funções de vários argumentos,
mas apenas funções de uma tupla de elementos. A cardinalidade de uma tupla
pode ser o que você precisar, e existem nomes específicos para tuplas com alguns
elementos: par, trio, quarteto e assim por diante. Existem outros nomes possíveis
e alguns preferem chamá-los de tuple2, tuple3, tuple4 e assim por diante. Kotlin
predefiniu Pair e Triple . Mas eu também disse que os elementos do argu-
mento podem ser aplicados um a um, com cada aplicação de um elemento retor-
nando uma nova função, exceto a última.

Vamos tentar definir uma função para somar dois números inteiros. Você aplicará
uma função ao primeiro inteiro e isso retornará uma função. O tipo é o seguinte:
(Int) -> (Int) -> Int

Nesta sintaxe, (Int) é o tipo do argumento e (Int) -> Int é o tipo do valor de
retorno. Para lembrar como o -> símbolo se associa, você pode pensar nisso
como se houvesse parênteses ao redor do valor de retorno, então é equivalente a

(Int) -> ((Int) -> Int)

O tipo de argumento é Int e o tipo de retorno é uma função que recebe um argu-
mento do tipo Int e retorna um Int .

Exercício 3.3

Escreva uma função para somar dois Int valores.

Solução

Essa função receberá um Int como argumento e retornará uma função de


Int para Int , portanto, o tipo será (Int) -> (Int) -> Int . Vamos dar-lhe o
nome add . Será implementado usando lambdas. O resultado final é mostrado
aqui:

val add: (Int) -> (Int) -> Int = { a -> { b -> a + b} }

Se preferir um nome de tipo mais curto, você pode criar um alias:

typealias IntBinOp = (Int) -> (Int) -> Int

val add: IntBinOp = { a -> { b -> a + b} }


val mult: IntBinOp = { a -> { b -> a * b} }

Aqui IntBinOp , o alias do tipo, significa Integer Binary Operador . O número de


argumentos não é limitado. Você pode definir funções com quantos argumentos
precisar. Como eu disse na primeira parte deste capítulo, funções como a
add função ou o mult função são ditas como sendo a forma atual das funções
equivalentes detuplas.

3.3.2 Aplicando funções atuais

você temvisto como escrever tipos de função curried e como implementá-los. Mas
como aplicá-los? Bem, você os aplica como qualquer função. Você aplica a função
ao primeiro argumento, depois aplica o resultado ao próximo argumento e assim
por diante até o último. Por exemplo, você pode aplicar o add função para 3 e 5 :

println(adicionar(3)(5))

3.3.3 Implementando funções de ordem superior

Dentroseção 3.2.8, você escreveu uma fun função para compor funções. Esta fun-
ção tomou como argumento uma tupla de duas funções e retornou uma função.
Mas, em vez de usar uma fun função (na verdade, um método), você pode usar
uma função de valor. Esse tipo especial de função, que usa funções como argu-
mentos e retorna funções, é chamado de função de ordem superior (HOF).

Exercício 3.4

Escreva uma função de valor para compor duas funções; por exemplo, o square
e triple funções usadas anteriormente.

Solução

Este exercício é fácil se você seguir o procedimento correto. A primeira coisa a fa-
zer é escrever o tipo. Esta função funcionará em dois argumentos, portanto, será
uma função com curry. Os dois argumentos e o tipo de retorno serão funções de
Int para Int :

(Int) -> Int

Você pode chamar isso de T . Você deseja criar uma função usando um argu-
mento do tipo T (o primeiro argumento) e retornando uma função de T (o se-
gundo argumento) para T (o valor de retorno). O tipo da função é então o
seguinte:

(T) -> (T) -> T

Se você substituir T por seu valor, obterá o tipo real:

((Int) -> Int) -> ((Int) -> Int) -> (Int) -> Int
O principal problema aqui é o comprimento da linha! Vamos agora adicionar a
implementação, que é bem mais fácil que o tipo:

x -> { y -> { z -> x(y(z)) } } }

O código completo é mostrado aqui:

val compor: ((Int) -> Int) -> ((Int) -> Int) -> (Int) -> Int =
{ x -> { y -> { z -> x(y(z)) } } }

Se preferir, você pode aproveitar a inferência de tipo e omitir a especificação do


tipo de retorno. Mas você terá que especificar o tipo de cada argumento:

val compose = { x: (Int) -> Int -> { y: (Int) -> Int ->
{ z: Int -> x(y(z)) } } }

Ou você pode usar um alias:

typealias IntUnaryOp = (Int) -> Int

val compose: (IntUnaryOp) -> (IntUnaryOp) -> IntUnaryOp =


{ x -> { y -> { z -> x(y(z)) } } }

Você pode testar este código com as funções square e triple :

typealias IntUnaryOp = (Int) -> Int

val compose: (IntUnaryOp) -> (IntUnaryOp) -> IntUnaryOp =


{ x -> { y -> { z -> x(y(z)) } } }

val quadrado: IntUnaryOp = { it * it }

val triplo: IntUnaryOp = { it * 3 }

val squareOfTriple = compose(quadrado)(triplo)

Neste código, você começa aplicando o primeiro argumento, que fornece uma
nova função para aplicar ao segundo argumento. O resultado é uma função, que é
a composição dos dois argumentos da função. Aplicar esta nova função a (por
exemplo) 2 fornece o resultado de primeiro aplicar triple e 2 depois aplicar
square ao resultado (que corresponde à definição de composição de função):
println(squareOfTriple(2))

36

Preste atenção na ordem dos parâmetros: triple é aplicado primeiro e depois


square é aplicado ao resultado retornado por triple .

3.3.4 Criação de HOFs polimórficos

Sua compose função está bem, mas pode compor apenas funções de Int a Int .
compose Uma função polimórficatambém permitiria que você compusesse fun-
ções de tipos diferentes, desde que o tipo de retorno de uma fosse o mesmo que o
tipo de argumento da outra.

Exercício 3.5 (difícil)

Escreva uma versão polimórfica da compose função.

Dica

Você enfrentará um problema com este exercício devido à falta de propriedades


polimórficas em Kotlin. No Kotlin, você pode criar classes, interfaces e funções po-
limórficas, mas não pode definir propriedades polimórficas. A solução é armaze-
nar a função em uma função, classe ou interface, em vez de em uma propriedade.

Solução

O primeiro passo parece ser “parametrizar o tipo” o exemplo do Exercício 3.3:

val <T, U, V> superiorComposição: ((U) -> V) -> ((T) -> U) -> (T) -> V =
{f->
{ g ->
{ x -> f(g(x)) }
}
}

Mas isso não é possível porque o Kotlin não permite propriedades parametrizadas
autônomas. Para ser parametrizada, uma propriedade deve ser criada em um es-
copo definindo os parâmetros de tipo. Somente classes, interfaces e funções decla-
radas com fun podem definir parâmetros de tipo, então você precisa definir sua
propriedade dentro de um desses elementos. O mais prático é uma fun função:
fun <T, U, V> upperCompose(): ((U) -> V) -> ((T) -> U) -> (T) -> V =
{f->
{ g ->
{ x -> f(g(x)) }
}
}

A fun função chamada higherCompose() não recebe nenhum parâmetro e sem-


pre retorna o mesmo valor. É uma constante. O fato de ser definido como uma
fun função é irrelevante desse ponto de vista. Não é uma função para compor
funções. É apenas uma fun função que retorna uma função de valor que compõe
funções de valor. Você pode preferir evitar indicar o tipo de retorno. Nesse caso,
você deve indicar os tipos de parâmetro:

fun <T, U, V> upperCompose() =


{ f: (U) -> V ->
{ g: (T) -> U ->
{ x: T -> f(g(x)) }
}
}

Agora você pode usar esta função para compor triple e square :

val squareOfTriple = upperCompose () (quadrado) (triplo)

Mas isso não compila, produzindo o seguinte erro:

Erro:(79, 24) Kotlin: Falha na inferência de tipo:


Não há informações suficientes para inferir o parâmetro T em fun <T, U, V>
upperCompose(): ((U) -> V) -> ((T) -> U) -> (T) -> V
Especifique-o explicitamente.

O compilador está dizendo que não pôde inferir os tipos reais para os parâmetros
de tipo T , U e . V Se você acha que o tipo dos parâmetros ( (Int) -> Int ) deve
ser informação suficiente para inferir os tipos T , U e V , então você é mais es-
perto do que Kotlin!

A solução é ajudar o compilador informando quais são os tipos reais T , U e V .


Inserir as informações de tipo após o nome da função pode fazer isso:

val squareOfTriple = upperCompose<Int, Int, Int>()(quadrado)(triplo)


Exercício 3.6 (fácil agora!)

Escreva a higherAndThen funçãoque compõe as funções ao contrário, o que sig-


nifica que higherCompose(f, g) é equivalente a higherAndThen(g, f) .

Solução

fun <T, U, V> upperAndThen(): ((T) -> U) -> ((U) -> V) -> (T) -> V =
{ f: (T) -> U ->
{ g: (U) -> V ->
{ x: T -> g(f(x)) }
}
}

Parâmetros de função de teste

Se você tiver alguma dúvida sobre a ordem dos parâmetros, você deve testar es-
ses HOFs com funções de tipos diferentes. O teste com funções de Int a Int será
ambíguo porque você poderá compor as funções em ambas as ordens, portanto,
um erro será difícil de detectar. Aqui está um teste usando funções de diferentes
tipos:

fun testHigherCompose() {

val f: (Duplo) -> Int = { a -> (a * 3).toInt() }


val g: (Longo) -> Duplo = { a -> a + 2,0 }

assertEquals(Integer.valueOf(9), f(g(1L)))
assertEquals(Integer.valueOf(9),
upperCompose<Long, Double, Int>()(f)(g)(1L))
}

3.3.5 Usando funções anônimas

Acimaaté agora, você tem usado funções nomeadas. Freqüentemente, você não
definirá nomes para funções; você os usará como funções anônimas. Vejamos um
exemplo. Em vez de escrever

val f: (Double) -> Double = { Math.PI / 2 - it }


val sin: (Duplo) -> Duplo = Math::sin
val cos: Double = compose(f, sin)(2.0)

você pode usar funções anônimas como mostrado aqui:


val cosValue: Double =
compose({ x: Double -> Math.PI / 2 - x }, Math::sin)(2.0)

Aqui, você está usando o compose função definida com fun no nível do pacote.
Mas isso também se aplica a HOFs:

val cos = upperCompose<Double, Double, Double>()


({ x: Double -> Math.PI / 2 - x })( Math::sin)

val valor cos = cos(2.0)

Os dois parâmetros na cos definição da função são incluídos em conjuntos sepa-


rados de parênteses. Ao contrário da compose função, higherCompose édefinido
na forma curried, aplicando um parâmetro de cada vez. Observe também que o
lambda pode ser colocado fora dos parênteses:

val cos = upperCompose<Double, Double, Double>()()


{ x: Double -> Math.PI / 2 - x }( Math::sin)

Além do fato de que as linhas estão quebradas devido ao comprimento de linha li-
mitado neste livro, a última forma pode parecer um pouco estranha, mas é a for-
matação recomendada em Kotlin.

Quando usar funções anônimas e quando usar funções nomeadas

Exceto em casos especiais em que funções anônimas não podem ser usadas, cabe
a você escolher entre funções de valor anônimas e nomeadas. (As funções decla-
radas com fun sempre têm um nome.) Como regra geral, as funções usadas ape-
nas uma vez são definidas como instâncias anônimas. Mas usado uma vez signi-
fica que você escreve a função uma vez. Isso não significa que é instanciado ape-
nas uma vez.

No exemplo a seguir, você define uma fun função para calcular o cosseno de
um Double valor. A implementação da função usa duas funções anônimas por-
que você está usando uma expressão lambda e uma referência de função:

fun cos(arg: Double) = compose({ x -> Math.PI / 2 - x }, Math::sin)(arg)

Não se preocupe com a criação de funções anônimas. O Kotlin nem sempre criará
novos objetos toda vez que a função for chamada. Instanciar tais objetos é barato.
Em vez disso, você deve decidir se deseja usar funções anônimas ou nomeadas,
considerando apenas a clareza e a capacidade de manutenção do seu código. Se
estiver preocupado com desempenho e reutilização, você deve usar referências
de função sempre que possível.

Implementando inferência de tipo

Modeloinferência também pode ser um problema com funções anônimas. No


exemplo anterior, os tipos das duas funções anônimas podem ser inferidos pelo
compilador porque ele sabe que a compose função recebe duas funções como
argumentos:

fun <T, U, V> compose(f: (U) -> V, g: (T) -> U): (T) -> V = { f(g(it)) }

Mas isso nem sempre funcionará. Se você substituir o segundo argumento por um
lambda em vez de uma referência de função

fun cos(arg: Double) =


compose({ x -> Math.PI / 2 - x }, { y -> Math.sin(y)})(arg)}

o compilador é perdido e exibe a seguinte mensagem de erro:

Erro:(48, 28) Kotlin: falha na inferência de tipo: não há informações suficientes para inferir o parâmetro T em fun
Especifique-o explicitamente.
Erro:(48, 38) Kotlin: não é possível inferir um tipo para este parâmetro.
Especifique-o explicitamente.
Erro:(48, 64) Kotlin: não é possível inferir um tipo para este parâmetro.
Especifique-o explicitamente.

Kotlin agora não consegue inferir os tipos de ambos os argumentos. Para compilar
este código, você precisa adicionar anotações de tipo:

fun cos(arg: Double) =


compose({ x: Double -> Math.PI / 2 - x },
{ x: Double -> Math.sin(x)})(arg)

Esta é uma boa razão para preferirfunçãoreferências.

3.3.6 Definindo funções locais

Vocêvimos que você pode definir funções de valor localmente em funções, mas
Kotlin também permite definir fun funções dentro de funções, como visto a
seguirexemplo:

fun cos(arg: Duplo): Duplo {


fun f(x: Duplo): Duplo = Math.PI / 2 - x
fun sin(x: Duplo): Duplo = Math.sin(x)
return compose(::f, ::sin)(arg)
}

3.3.7 Implementação de fechamentos

você temvisto que funções puras não devem depender de nada além de seus argu-
mentos para avaliar seus valores de retorno. As funções Kotlin geralmente aces-
sam elementos fora da própria função, seja no nível do pacote ou como proprie-
dades de classe ou objeto. As funções podem até mesmo acessar membros de obje-
tos complementares de outras classes ou outros pacotes.

Eu disse que funções puras são funções que respeitam a transparência referen-
cial, o que significa que elas não têm efeitos observáveis ​além de retornar um va-
lor. Mas e as funções com valores de retorno dependendo não apenas de seus ar-
gumentos, mas também de elementos pertencentes ao escopo delimitador? Você
já viu esse caso, e esses elementos do escopo delimitador podem ser considerados
parâmetros implícitos das funções que os utilizam.

Isso também se aplica a lambdas, e os lambdas Kotlin não têm a mesma limitação
que os lambdas Java: eles podem acessar variáveis ​mutáveis ​do escopo envol-
vente. Vejamos um exemplo:

valor taxa de imposto = 0,09


fun addTax(preço: Double) = preço + preço * taxRate

Neste exemplo, a addTax função fecha sobre a taxRate variável. É importante


notar que addTax não é uma função price porque nem sempre dará o mesmo
resultado para o mesmo argumento. Mas pode ser visto como uma função da tu-
pla (price, taxRate) .

Closures são compatíveis com funções puras se você os considerar como argu-
mentos implícitos adicionais. Eles podem causar problemas ao refatorar o código
e também quando as funções são passadas como parâmetros para outras funções.
Isso pode resultar em programas difíceis de ler e manter.

Uma maneira de tornar os programas mais fáceis de ler e manter é torná-los mais
modulares, o que significa que cada parte dos programas pode ser usada como
módulos independentes. Isso pode ser obtido usando funções de tuplas de
argumentos:

valor taxa de imposto = 0,09

fun addTax(taxRate: Double, preço: Double) = preço + preço * taxRate

println(addTax(taxRate, 12.0))

Isso também se aplica a funções de valor:

valor taxa de imposto = 0,09

val addTax = { taxRate: Double, preço: Double -> preço + preço * taxRate }

println(addTax(taxRate, 12.0))

A addTax função recebe um único argumento, que é um par de Double . Ao con-


trário de Java, Kotlin permite o uso de argumentos de cardinalidade maior que 2.
(Em Java, você pode usar o Function interface para um único argumento e o Bi-
Function interface para um par de argumentos. Se você quiser triplos ou mais,
você deve definir suas próprias interfaces.)

Mas você já viu que pode usar a versão ao curry para obter o mesmo resultado.
Uma função curried recebe um único argumento e retorna uma função rece-
bendo um único argumento, retornando… e assim por diante até retornar o valor
final. Aqui está a versão ao curry da addTax função de valor:

valor taxa de imposto = 0,09

val addTax = { taxRate: Double ->


{ preço: Duplo ->
preço + preço * taxRate
}
}

println(addTax(taxRate)(12.0))

Uma versão ao curry de uma fun função faz pouco sentido. Você pode usar a
fun para a primeira função, mas é forçado a retornar uma função de valor.
fun função não sãovalores.
3.3.8 Aplicando funções parcialmente e currying automático

oas versões de fechamento e curry no exemplo anterior fornecem os mesmos re-


sultados e podem ser vistas como equivalentes. Na verdade, eles são semantica-
mente diferentes. Como já disse, os dois parâmetros desempenham papéis total-
mente diferentes. A taxa de imposto não deve mudar com frequência, enquanto o
preço deve ser diferente em cada invocação. Isso aparece claramente na versão
de encerramento. A função fecha sobre um parâmetro que não muda (porque é
um val ). Na versão curried, ambos os argumentos podem mudar em cada cha-
mada, embora a taxa de imposto não mude com mais frequência do que na ver-
são de encerramento.

É comum precisar alterar a taxa de imposto, como quando você tem várias taxas
de imposto para diferentes categorias de produtos ou para diferentes destinos de
envio. no tradicionalprogramação de objetos, transformar a classe em um compu-
tador fiscal parametrizado poderia acomodar isso. Aqui está um exemplo:

class TaxComputer(private val rate: Double) {

fun compute(preço: Duplo): Duplo = preço * taxa + preço


}

Com esta classe, você pode criar várias TaxComputer instâncias para várias taxas
de imposto, e essas instâncias podem ser reutilizadas sempre que necessário:

val tc9 = TaxComputer(0.09)


preço val = tc9.compute(12.0)

Você pode conseguir a mesma coisa com uma função curried aplicando-a
parcialmente:

val tc9 = addTax(0.09)


preço val = tc9(12.0)

Aqui, a addTax função é aquela do final da seção 3.3.7. O tipo de tc9 agora é
(Double) -> Double ; é uma função que recebe a Double como argumento e
retorna a Double com o imposto adicionado.

Você pode ver que currying eaplicação parcial estão intimamente relacionados.
Currying consiste em substituir uma função de uma tupla por uma nova função
que você pode aplicar parcialmente, um argumento após o outro. Esta é a princi-
pal diferença entre uma função curried e uma função de uma tupla. Com uma
função de tupla, todos os argumentos são avaliados antes que a função seja
aplicada.

Com a versão atual, todos os argumentos devem ser conhecidos antes que a fun-
ção seja totalmente aplicada, mas um único argumento pode ser avaliado e a fun-
ção parcialmente aplicada a ele. Você não é obrigado a totalmente curry a função.
Uma função de três argumentos pode ser transformada em uma função de uma
tupla que produz uma função de um único argumento.

Abstração sendo a essência da programação, currying e aplicação parcial de fun-


ções podem ser abstraídas para fazer isso automaticamente. Nas seções anterio-
res, você usou principalmente funções curried e não funções de tuplas. O uso de
funções curried apresenta uma grande vantagem: a aplicação parcial desse tipo
de função é absolutamente direta.

Exercício 3.7 (fácil)

Escreva uma fun função para aplicar parcialmente uma função curried de dois
argumentos ao seu primeiro argumento.

Solução

Você não tem nada para fazer! A assinatura desta função é a seguinte:

fun <A, B, C> parcialA(a: A, f: (A) -> (B) -> C): (B) -> C

Você pode ver imediatamente que aplicar parcialmente o primeiro argumento é


tão simples quanto aplicar o segundo argumento (uma função) ao primeiro:

fun <A, B, C> parcialA(a: A, f: (A) -> (B) -> C): (B) -> C = f(a)

(Se quiser ver um exemplo de como partialA pode ser usado, consulte o teste de
unidade para este exercício no código que o acompanha.)

Você deve ter notado que a função original era do tipo (A) -> (B) -> C . E se
você quiser aplicar parcialmente esta função ao segundo argumento?

Exercício 3.8

Escreva uma fun função para aplicar parcialmente uma função curried de dois
argumentos ao seu segundo argumento.
Solução

Com sua função anterior, a resposta para o problema seria uma função com a se-
guinte assinatura:

fun <A, B, C> parcialB(b: B, f: (A) -> (B) -> C): (A) -> C

Este exercício é um pouco mais difícil, mas ainda simples se você considerar cui-
dadosamente os tipos. Lembre-se, você deve sempre confiar nos tipos! Eles não
vão te dar uma solução imediata em todos os casos, mas vão te levar até a solução.
Esta função tem apenas uma implementação possível, portanto, se você encontrar
uma implementação que compila, pode ter certeza de que está correta!

O que você sabe é que deve retornar uma função de A para C . Você pode iniciar
a implementação escrevendo isto:

fun <A, B, C> parcialB(b: B, f: (A) -> (B) -> C): (A) -> C =
{ a: A ->

Aqui, a é uma variável do tipo A . Após a seta para a direita, você deve escrever
uma expressão composta pela função f e as variáveis a ​e b , e deve resultar em
uma função de A a C . A função f é uma função de A a (B) -> C , então você
pode começar aplicando-a ao A que você tem:

fun <A, B, C> parcialB(b: B, f: (A) -> (B) -> C): (A) -> C =
{ a: A ->
f(a)
}

Isso lhe dá uma função de B para C . Você precisa de um C e já tem um B , então,


mais uma vez, a resposta é direta:

fun <A, B, C> parcialB(b: B, f: (A) -> (B) -> C): (A) -> C =
{ a: A ->
f(a)(b)
}

É isso! Na verdade, você não tinha quase nada a fazer a não ser seguir os tipos.
Como eu disse, o mais importante é que você tenha uma versão ao curry da fun-
ção. Você provavelmente aprenderá rapidamente como escrever funções curry di-
retamente. Uma tarefa que volta com frequência ao tentar levar a abstração ao li-
mite para escrever programas mais reutilizáveis ​é converter funções com argu-
mentos de tupla em funções curried. Como você acabou de ver, isso é extrema-
mente simples.

Exercício 3.9 (fácil)

Converta a seguinte função em uma função curried:

fun <A, B, C, D> func(a: A, b: B, c: C, d: D): String = "$a, $b, $c, $d"

(Concordo que esta função é totalmente inútil, mas é apenas um exercício.)

Solução

Mais uma vez, você não tem muito o que fazer além de substituir as vírgulas pelas
setas à direita e adicionar parênteses. Lembre-se, porém, que você deve definir
esta função em um escopo que aceite parâmetros de tipo, o que não é o caso de
uma propriedade. Você deve defini-lo em uma classe, uma interface ou uma fun-
ção 'divertida' com todos os parâmetros de tipo necessários.

Aqui está uma solução usando uma função. Primeiro, escreva a fun declaração
de função anexa com os parâmetros de tipo:

fun <A,B,C,D> curry()

Em seguida, pense na assinatura da função resultante. O primeiro parâmetro será


um A, então você pode escrever

fun <A,B,C,D> curry(): (A) ->

Em seguida, faça a mesma coisa com o segundo tipo de parâmetro:

fun <A,B,C,D> curry(): (A) -> (B) ->

Em seguida, continue até que nenhum parâmetro seja deixado:

fun <A,B,C,D> curried(): (A) -> (B) -> (C) -> (D) ->
Adicione o tipo de retorno da função resultante:

fun <A,B,C,D> curried(): (A) -> (B) -> (C) -> (D) -> String

Para a implementação, liste quantos parâmetros forem necessários, separando-os


com setas à direita e abrindo chaves (começando com chave e terminando com
seta):

fun <A,B,C,D> curried() =


{ a: A ->
{b:B->
{c:C->
{d:D->

}
}
}
}

Por fim, adicione a implementação, que é a mesma da função original, e feche to-
das as chaves:

fun <A,B,C,D> curried() =


{ a: A ->
{b:B->
{c:C->
{d:D->
"$a, $b, $c, $d"
}
}
}
}

O mesmo princípio pode ser aplicado para alterar uma função de qualquer tupla.

Exercício 3.10

Escreva uma função para curry uma função de a (A, B) para C .

Solução

Novamente, você tem que seguir os tipos. Você sabe que a função vai pegar um
parâmetro do tipo (A, B) -> C e vai retornar (A) -> (B) -> C , então a assi-
natura é a seguinte:

divertido <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C

Agora, para a implementação, você terá que retornar uma função curried de dois
argumentos, então você pode começar com isso:

fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C =
{a->
{b->

}
}

Eventualmente, você precisará avaliar o tipo de retorno. Para isso, você pode usar
a função f e aplicá-la aos parâmetros a e b :

fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C =
{a->
{b->
f(a, b)
}
}

Mais uma vez, se compilar, não pode estar errado. Este é um dos inúmeros benefí-
cios de contar com um sistema de tipo forte! (Isso nem sempre é verdade, mas
você aprenderá nos próximos capítulos como fazer isso acontecer maismuitas
vezes.)

3.3.9 Alternando argumentos de funções parcialmente aplicadas

Sevocê tem uma função de dois argumentos, você pode querer aplicar apenas o
primeiro argumento para obter uma função parcialmente aplicada. Digamos que
você tenha a seguinte função:

val addTax: (Duplo) -> (Duplo) -> Duplo =


{ x ->
{ y ->
a + a / 100 * x
}
}
Você pode querer primeiro aplicar o imposto para obter uma nova função de um
argumento que pode ser aplicado a qualquer preço:

val add9percentTax: (Duplo) -> Duplo = addTax(9.0)

Então, quando você quiser adicionar impostos a um preço, você pode fazer isso:

val preçoIncludingTax = add9percentTax(preço);

Isso é bom, mas e se a função inicial for a seguinte:

val addTax: (Duplo) -> (Duplo) -> Duplo =


{ x ->
{ y ->
x + x / 100 * y
}
}

Nesse caso, o preço é o primeiro argumento. Aplicar apenas o preço é provavel-


mente inútil, mas como aplicar apenas o imposto? (Suponha que você não tenha
acesso à implementação.)

Exercício 3.11

Escreva uma função 'divertida' para trocar os argumentos de uma função com
curry.

Solução

A função a seguir retorna uma função curried com os argumentos na ordem in-
versa. Poderia ser generalizado para qualquer número de argumentos e para
qualquer arranjo deles:

fun <T, U, V> swapArgs(f: (T) -> (U) -> V): (U) -> (T) -> (V) =
{ u -> { t -> f(t)(u) } }

Dada esta função, você pode aplicar parcialmente qualquer um dos dois argu-
mentos. Por exemplo, se você tiver uma função que calcula o pagamento mensal
de um empréstimo a partir de uma taxa de juros e um valor
valor do pagamento = { valor -> { taxa -> ... } }

Você pode facilmente criar uma função de um argumento para calcular o paga-
mento de um valor fixo e uma taxa variável, ou uma função que calcula o paga-
mento de uma taxa fixa e uma taxa variávelquantia.

Declarando a função de identidade

você temvisto que você pode tratar funções como dados. Eles podem ser passados ​
como argumentos para outras funções, podem ser retornados por funções e po-
dem ser usados ​em operações exatamente como números inteiros ou strings. Em
exercícios futuros, você aplicará operações a funções e precisará de um elemento
neutro para essas operações. UMAelemento neutro atua como 0 para adição, ou 1
para multiplicação, ou a string vazia para concatenação de strings.

Um elemento neutro é neutro apenas para uma determinada operação. Para adi-
ção de inteiros, 1 não é neutro e para multiplicação, 0 é ainda menos neutro. Aqui,
estou falando de um elemento neutro para composição de funções. Esta função
específica é uma função que retorna seu argumento. Por esse motivo, é chamada
de função identidade . Por extensão, o termo elemento identidade é frequente-
mente usado em vez de elemento neutro para operações como adição, multiplica-
ção ou concatenação de strings. A função de identidade em Kotlin pode ser
expressasimplesmente:

identidade val = { it }

Usando os tipos certos

DentroNos exemplos anteriores, você usou tipos padrão como Int , Double e
String para representar entidades comerciais como preços e taxas de impostos.
Embora esta seja uma prática comum na programação, ela causa problemas que
devem ser evitados. Como eu disse, você deve confiar mais em tipos do que em
nomes. Chamar um Double “ price ” não o torna um preço. Isso apenas mostra
sua intenção. Chamar outro Double “ taxRate ” mostra uma intenção diferente,
que nenhum compilador pode impor.

Para tornar os programas mais seguros, você precisa usar tipos mais poderosos
que o compilador possa verificar. Isso evita mexer com tipos, como adicionar
taxRate a um arquivo price . Se você fizer isso inadvertidamente, o compilador
verá apenas um Double sendo adicionado a um Double , o que é perfeitamente
legítimo, mas totalmente errado.
Evitando problemas com tipos padrão

vamosexamine um problema simplificado e veja como resolvê-lo usando tipos pa-


drão leva a problemas. Imagine que você tem produtos com nome, preço e peso e
precisa criar notas fiscais que representem as vendas dos produtos. Estas faturas
devem mencionar os produtos, as quantidades, o preço total e o peso total. Você
poderia representar a Product com a seguinte classe:

classe de dados Product(val name: String, val price: Double, val weight: Double)

Em seguida, você pode usar uma OrderLine classe para representar cada linha
de um pedido:

classe de dados OrderLine(val product: Product, val count: Int) {

fun peso() = produto.peso * contagem

quantia divertida() = product.price * count


}

Isso se parece com um bom e velho objeto, inicializado com a Product e an Int ,
e representando uma linha de um pedido. Também possui funções para retornar
o preço total e o peso total da linha.

Continuando com a decisão de usar tipos padrão, você usará


List<OrderLine> para representar um pedido. A listagem a seguir mostra como
você pode lidar com pedidos.

Listagem 3.2 Lidando com pedidos

pacote com.fpinkotlin.functions.listing03_02

classe de dados Product(val name: String, val price: Double, val weight: Double)

classe de dados OrderLine(val product: Product, val count: Int) {

fun peso() = produto.peso * contagem

quantia divertida() = product.price * count


}

loja de objetos { ①
@JvmStatic ②
fun main(args: Array<String>) {
val pasta de dente = Product("Pasta de dente", 1.5, 0.5)
val escova de dentes = Product("Escova de dentes", 3.5, 0.3)
val orderLines = listOf(
OrderLine(pasta de dente, 2),
OrderLine(escova de dentes, 3))
val peso = orderLines.sumByDouble { it.amount() }
val preço = orderLines.sumByDouble { it.weight() }
println("Preço total: $preço")
println("Peso total: $peso")
}
}

① Store é um objeto singleton.

② A anotação @JvmStatic torna a função principal chamável como se fosse um mé-


todo Java estático.

A execução desse programa exibe o seguinte resultado no console:

Preço total: 1,9


Peso total: 13,5

Isso é bom, mas errado! Embora o erro seja óbvio, o problema é que o compilador
não informou nada sobre isso. (Você pode ver o erro olhando o Store código.)
Mas o mesmo erro pode ter sido cometido ao criar um Product , e a criação de
um Product pode ter acontecido em outro lugar.

A única maneira de detectar esse erro é testar o programa, mas os testes não po-
dem provar que um programa está correto. Eles só podem provar que você não
foi capaz de provar que está incorreto escrevendo outro programa (que, a propó-
sito, também pode estar incorreto). Caso você não tenha percebido (o que é im-
provável), o problema está nas seguintes linhas:

val peso = orderLines.sumByDouble { it.amount() }


val preço = orderLines.sumByDouble { it.weight() }

Você misturou preços e pesos incorretamente, o que o compilador não pôde per-
ceber porque ambos são duplos.

OBSERVAÇÃO Se você aprendeu sobre modelagem, talvez se lembre de uma re-


gra antiga: as classes não devem ter várias propriedades do mesmo tipo. Em vez
disso, eles devem ter uma propriedade com uma cardinalidade específica. Aqui,
isso significaria que a Product deveria ter uma propriedade de tipo Double com
cardinalidade 2. Essa certamente não é a maneira correta de resolver o problema,
mas é uma boa regra a ser lembrada. Se você estiver modelando objetos com vá-
rias propriedades do mesmo tipo, provavelmente está fazendo errado.

O que você pode fazer para evitar tais problemas? Primeiro, você deve perceber
que preços e pesos não são números; são quantidades. As quantidades podem ser
números, mas os preços são quantidades de unidades monetárias e os pesos são
quantidades de unidades de peso. Você nunca deve estar na situação de adicionar
onças edólares.

Definindo tipos de valor

Paraevitar esse problema, você deve usar tipos de valor. Tipos de valor são tipos
que representam valores. Você pode definir um tipo de valor para representar um
preço como este:

classe de dados Price(val value: Double)

Você pode fazer o mesmo para o peso:

classe de dados Weight(val value: Double)

Mas isso não resolve o problema porque você poderia escrever isto:

val total = preço.valor + peso.valor

O que você precisa fazer é definir adição para Price e para Weight , e você pode
fazer isso com uma função:

classe de dados Price(val value: Double) {

operador fun plus(preço: Preço) = Preço(este.valor + preço.valor)


}

classe de dados Weight(val value: Double) {

operador fun plus(peso: Peso) = Peso(este.valor + peso.valor)


}
A palavra- operator chavesignifica que você poderá usar o nome da função em
uma posição infixa. Além disso, como a função se chama plus , você poderá subs-
tituir esse nome pelo + símbolo, assim:

val preço total = Preço(1,0) + Preço(2,0)

Você também precisa de multiplicação, mas a multiplicação é um pouco diferente.


A adição adiciona coisas do mesmo tipo, enquanto a multiplicação multiplica coi-
sas de um tipo por um número. A multiplicação não é comutativa quando não é
aplicada apenas a números. Aqui está um exemplo de multiplicação para Price :

classe de dados Price(val value: Double) {

fun plus(preço: Preço) = Preço(este.valor + preço.valor)

fun times(num: Int) = Price(this.value * num)


}

Agora você não usa mais sumByDouble para calcular a soma da Price lista. Você
pode definir uma função equivalente sumByPrice . Se você estiver interessado,
pode examinar a implementação sumByDouble e adaptá-lo aos preços. Mas há
uma maneira muito melhor de ir.

A redução de uma coleção a um único elemento é chamada de fold ou reduce .


A distinção nem sempre é clara, mas na maioria das vezes é entendida como de-
pendente de duas condições:

Se você fornece um elemento para começar com ( fold ) ou não ( reduce )


Se o resultado deve ser do mesmo tipo dos elementos ( reduce ) ou não (
fold )

A diferença é que se a coleção estiver vazia, reduce não terá resultado, enquanto
fold resultará no elemento inicial que você fornecer. No capítulo 6, você apren-
derá mais sobre como isso funciona. Por enquanto, você precisa usar a fold fun-
çãooferecido por Kotlincoleções. Esta função recebe dois parâmetros: o valor ini-
cial e uma função que permite compor o resultado atual com o elemento atual,
enquanto itera sobre cada elemento.

uma reduce funçãoé muito parecido com um fold , embora não tenha valor ini-
cial. Ele deve então tomar o primeiro elemento como o valor inicial, o que implica
que o tipo de resultado é o mesmo que o tipo de elemento. Se aplicado a uma cole-
ção vazia, o resultado é null ou um erro, ou qualquer outra representação do
fato de que não há resultado.

No exemplo a seguir, como você usará fold , usará um preço zero—


Price(0.0) —e um peso zero— Weight(0.0) —para os valores iniciais. A fun-
ção usada como segundo parâmetro usa a adição que você acabou de definir. Para
torná-lo uma função de valor, você pode usar um lambda:

val zeroPreço = Preço(0,0)


val zeroPeso = Peso(0,0)
val preçoAdição = { x, y -> x + y }

o Product classe precisa ser modificado da seguinte forma:

classe de dados Product(val name: String, val price: Price, val weight: Weight)

OrderLine não precisa de nenhuma modificação:

classe de dados OrderLine(val product: Product, val count: Int) {

fun peso() = produto.peso * contagem

quantia divertida() = product.price * count


}

o * operadoragora é automaticamente substituído por uma chamada para as ti-


mes funções que você definiu. Agora você pode reescrever o Store objeto com es-
tes tipos e operações:

loja de objetos {

@JvmStatic
fun main(args: Array<String>) {
val pasta de dente = Produto("Pasta de dente", Preço(1,5), Peso(0,5))
val escova de dentes = Produto("Escova de dentes", Preço(3,5), Peso(0,3))
val orderLines = listOf(
OrderLine(pasta de dente, 2),
OrderLine(escova de dentes, 3))
peso val: Peso =
orderLines.fold(Weight(0.0)) { a, b -> a + b.weight() }
preço val: Preço =
orderLines.fold(Price(0.0)) { a, b -> a + b.amount() }
println("Preço total: $preço")
println("Peso total: $peso")
}
}

Você não pode mais mexer com os tipos sem o compilador avisá-lo. Isso implica
que você especifique os tipos para val weight: Weight e val price: Price .
O Kotlin é capaz de inferir os tipos, mas, ao especificá-los, você permite que o
compilador informe se os tipos inferidos diferem do que você espera.

Mas você pode fazer muito melhor do que isso. Primeiro, você pode adicionar va-
lidação a Price e Weight . Nenhum deles deve ser construído com valor 0, ex-
ceto de dentro da própria classe, para o elemento identidade. Você pode usar um
construtor privado e uma função de fábrica. Aqui está como ele vai para Price :

classe de dados Preço construtor privado (valor de valor privado: Duplo) {

substitui fun toString() = value.toString()

operador fun plus(preço: Preço) = Preço(este.valor + preço.valor)

operador fun times(num: Int) = Price(this.value * num)

objeto complementar {
identidade val = Preço(0.0)

operador divertido invocar(valor: Duplo) =


se (valor > 0)
Valor do preço)
senão
lançar IllegalArgumentException(
"O preço deve ser positivo ou nulo")
}
}

O construtor agora é privado e a invoke função do objeto complementar é decla-


rada como operator e contém o código de validação. O nome invoke é um
nome especial como plus e times que, quando usado com o operator palavra-
chave, permite substituir um operador.

Aqui, o operador sobrecarregado é () , que corresponde à invocação de uma fun-


ção. Como resultado, você pode usar a função de fábrica exatamente como o cons-
trutor, que agora é privado. O construtor privado é usado no objeto complemen-
tar para criar o resultado da função. Também é usado para criar o identity va-
lor usado para dobrar. Aqui está o código final do Store objeto:
loja de objetos {

@JvmStatic
fun main(args: Array<String>) {
val pasta de dente = Produto("Pasta de dente", Preço(1,5), Peso(0,5))
val escova de dentes = Produto("Escova de dentes", Preço(3,5), Peso(0,3))
val orderLines = listOf(
OrderLine(pasta de dente, 2),
OrderLine(escova de dentes, 3))
peso val: Peso =
orderLines.fold(Weight.identity) { a, b ->
a + b.peso()
}
preço val: Preço =
orderLines.fold(Price.identity) { a, b ->
a + b.quantia()
}
println("Preço total: $preço")
println("Peso total: $peso")
}
}

Nada mudou para a criação de um preço ou peso. A sintaxe para chamar a invo-
ke função é semelhante à forma como você usou anteriormente o construtor, que
agora é privado. O valor “zero” (chamado identity ) usado para a dobra é lido
do objeto companheiro. Não poderia ser criado a partir do invoke função devido
à validaçãocódigojogando umexceção.

OBSERVAÇÃO O construtor privado de uma classe de dados não é privado porque


é exposto pela copy função gerada. Mas isso não é um problema. Você só pode co-
piar um objeto que já foi validado.

Resumo

Uma função representa um relacionamento entre um conjunto de origem e


um conjunto de destino. Ele estabelece uma correspondência entre os elemen-
tos do conjunto de origem (o domínio) e os elementos do conjunto de destino
(o contradomínio).
As funções puras não têm efeitos visíveis além de retornar um valor.
As funções têm apenas um argumento, que pode ser uma tupla de vários
elementos.
Você pode alterar funções de tuplas para aplicá-las a um elemento da tupla por
vez.
Quando uma função curried é aplicada apenas a alguns de seus argumentos,
diz-se que ela foi parcialmente aplicada.
No Kotlin, as funções podem ser representadas por fun funções, que são méto-
dos, ou por funções de valor, que podem ser tratadas como dados.
As funções de valor podem ser implementadas usando lambdas ou referências
a fun funções.
Você pode compor funções para criar novas funções.
Você pode usar lambdas e referências de função em locais onde uma função de
valor é esperada.
Você pode usar tipos de valor para tornar os programas mais seguros, permi-
tindo que o compilador detecte problemas de tipo.

You might also like