Professional Documents
Culture Documents
3 Programming With Functions - The Joy of Kotlin
3 Programming With Functions - The Joy of Kotlin
Este capítulocapas
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
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.
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.
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.
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)
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)
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
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.
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
f(x) = g
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
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) 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.
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.
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.
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.
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:
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:
Dois objetos são criados com o mesmo argumento, então ele deve retornar o
mesmo valor.
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.
class FunFunções {
var percent1 = 5
var privado percent2 = 9
val percent3 = 13
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:
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 :
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 :
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
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) :
val percent3 = 13
fun applyTax3(a: Int): Int = a / 100 * (100 + percent3)
objeto complementar {
fun groupByCard(pagamentos: List<Pagamento>): List<Pagamento> =
Payments.groupBy { it.creditCard }
.valores
.map { it.reduce(Pagamento::combinar) }
}
}
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
import ...Payment.Companion.combine
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:
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.
Funções de tuplas não são diferentes. Aqui está uma função que representa a adi-
ção de números inteiros:
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
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.
classe MinhaClasse {
divertido duplo(n: Int): Int = n * 2
}
importar outro.pacote.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:
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
}
}
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
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
Solução
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int = { x -> f(g(x)) }
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int = { f(g(it)) }
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.
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
Solução
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.
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 .
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
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
Solução
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))
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 :
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:
((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:
val compor: ((Int) -> Int) -> ((Int) -> Int) -> (Int) -> Int =
{ x -> { y -> { z -> x(y(z)) } } }
val compose = { x: (Int) -> Int -> { y: (Int) -> Int ->
{ z: Int -> x(y(z)) } } }
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
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.
Dica
Solução
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)) }
}
}
Agora você pode usar esta função para compor triple e square :
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!
Solução
fun <T, U, V> upperAndThen(): ((T) -> U) -> ((U) -> V) -> (T) -> V =
{ f: (T) -> U ->
{ g: (U) -> V ->
{ x: T -> g(f(x)) }
}
}
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() {
assertEquals(Integer.valueOf(9), f(g(1L)))
assertEquals(Integer.valueOf(9),
upperCompose<Long, Double, Int>()(f)(g)(1L))
}
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
Aqui, você está usando o compose função definida com fun no nível do pacote.
Mas isso também se aplica a HOFs:
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.
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:
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.
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
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:
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:
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:
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:
println(addTax(taxRate, 12.0))
val addTax = { taxRate: Double, preço: Double -> preço + preço * taxRate }
println(addTax(taxRate, 12.0))
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:
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
É 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:
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:
Você pode conseguir a mesma coisa com uma função curried aplicando-a
parcialmente:
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.
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
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)
}
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.
fun <A, B, C, D> func(a: A, b: B, c: C, d: D): String = "$a, $b, $c, $d"
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> 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
}
}
}
}
Por fim, adicione a implementação, que é a mesma da função original, e feche to-
das as chaves:
O mesmo princípio pode ser aplicado para alterar uma função de qualquer tupla.
Exercício 3.10
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.)
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:
Então, quando você quiser adicionar impostos a um preço, você pode fazer isso:
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.
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 }
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
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:
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.
pacote com.fpinkotlin.functions.listing03_02
classe de dados Product(val name: String, val price: Double, val weight: Double)
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")
}
}
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:
Você misturou preços e pesos incorretamente, o que o compilador não pôde per-
ceber porque ambos são duplos.
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.
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:
Mas isso não resolve o problema porque você poderia escrever isto:
O que você precisa fazer é definir adição para Price e para Weight , e você pode
fazer isso com uma função:
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 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.
classe de dados Product(val name: String, val price: Price, val weight: Weight)
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 :
objeto complementar {
identidade val = Preço(0.0)
@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.
Resumo