Kotlin Piping e composição

Mar 18 2023
Simplifique seu código com tubulação e composição Kotlin
A programação funcional idiomática em Kotlin pode parecer estranha e “errada” por causa da maneira como estamos acostumados a trabalhar com Kotlin. Kotlin é diferente de Haskell e F#.
Foto de Lucas Kapla no Unsplash

A programação funcional idiomática em Kotlin pode parecer estranha e “errada” por causa da maneira como estamos acostumados a trabalhar com Kotlin. Kotlin é diferente de Haskell e F#. Por exemplo, se você escrever o mesmo código apenas em Kotlin, o código Kotlin não parecerá correto. Isso porque não estamos acostumados a escrever Kotlin dessa maneira. Além disso, Kotlin não é uma linguagem funcional, mas a suporta.

Fazer a maioria das coisas que são “normais” em outras linguagens de FP, como, por exemplo, funções curried, aplicação parcial, etc., não faz muito sentido em Kotlin. Claro que o currying e a aplicação parcial têm seu uso, mas na maioria dos casos, não faz sentido. Mesmo o F# permite vários parâmetros de função, mas o compilador o divide em várias funções que usam um único parâmetro.

Kotlin suporta paradigmas de programação funcional. Veremos dois conceitos centrais populares em linguagens funcionais e veremos como eles se parecem em Kotlin.

Tubulação

A tubulação encadeia funções e envia a saída de uma função como um argumento de entrada para outra. Já temos uma maneira de fazer isso, mas não é tão limpo e legível. Aqui está o que parece:

val addOne = { x: Int -> x + 1 }
val addTwo = { x: Int -> x + 2 }
val addThree = { x: Int -> x + 3 }

addThree(addTwo(addOne(1)))

Vamos canalizar.

Kotlin não tem um operador de pipe especial como |>em F#.

Podemos definir uma função infixa de extensão, pipe, que recebe um lambda que atuará como nosso operador de pipe.

A pipefunção encadeia os dois lambdas para que a saída do primeiro lambda se torne a entrada do segundo lambda.

Nesse caso, teríamos um código assim:

val addOne = { x: Int -> x + 1 }
val addTwo = { x: Int -> x + 2 }
val addThree = { x: Int -> x + 3 }

val f = addOne pipe addTwo pipe addThree
val result = f(1)

Mas essa não é a maneira idiomática de Kotlin. Na minha opinião, o operador pipe, como o do F#, |>, é necessário porque o F# não oferece suporte a funções de extensão da mesma forma que o Kotlin.

F#

let f = [ 1; 2; 3; 4; 5 ] |> List.map (fun x -> x * 2) |> List.map string

fun f() = listOf(1, 2, 3, 4, 5).map { it * 2 }.map(Int::toString)

Minha opinião é baseada em um tamanho de amostra muito pequeno: Kotlin e F# :D. Portanto, não leve isso muito a sério sem fazer sua própria pesquisa.

Dito isto, não vou mostrar o pipe infix funmas, aqui está outro exemplo:

val addOne = { x: Int -> x + 1 }
val addTwo = { x: Int -> x + 2 }
val addThree = { x: Int -> x + 3 }

val result = addOne(1).let(addTwo).let(addThree)

val addOne: Int.() -> Int = { this + 1 }
val addTwo: Int.() -> Int = { this + 2 }
val addThree: Int.() -> Int = { this + 3 }

val result = 1.addOne().addTwo().addThree()

Como você pode ver, o Kotlin é flexível e você tem muitas opções para fazer isso. Claro, vai depender do seu caso de uso.

Composição

Composição é uma forma de combinar funções com assinaturas corretas em novas funções com assinaturas combinadas. Isso é confuso. Vejamos na prática:

val intToDouble: (Int) -> Double = { x: Int -> x * 1.0 }
val doubleToString: (Double) -> String = { x: Double -> x.toString() }
val intToString: (Int) -> String = doubleToString compose intToDouble

Como você pode ver, estamos compondo intToDoublee doubleToStringem intToString.

A parte confusa pode ser que a composição é aplicada e lida da direita para a esquerda. Está para trás.

O equivalente a intToStringseria { x: Int -> (x * 1.0).toString() }.

Observe que isso funciona apenas com funções que usam apenas um parâmetro -> funções curried.

Tubulação vs Composição

A tubulação e a composição parecem bastante semelhantes. Qual é a diferença?

Tanto a tubulação quanto a composição são técnicas para combinar funções para criar funcionalidades mais complexas.

A tubulação é o processo de pegar a saída de uma função e enviá-la como entrada para outra função. A tubulação é útil quando queremos aplicar uma sequência de operações a um valor e criar um novo valor como resultado.

Composição é o processo de criação de novas funções combinando duas ou mais funções. A composição é útil quando queremos combinar várias funções em uma única função que executa todas as operações em sequência.

Portanto, a principal diferença é que, com a tubulação, criamos um pipeline pelo qual nossos dados/valor se movem, enquanto com a composição, criamos uma nova função a partir de várias funções e essa nova função é aplicada aos nossos dados/valor.

Existem algumas diferenças e não podemos simplesmente substituir compose. Vamos tentar isso com o exemplo de composição acima.

val intToDouble: (Int) -> Double = { x: Int -> x * 1.0 }
val doubleToString: (Double) -> String = { x: Double -> x.toString() }
val intToStringPipe = doubleToString.let(intToDouble)

Já dissemos que esses dois são lidos e compostos em direções diferentes, então vamos consertar isso.

val intToDouble: (Int) -> Double = { x: Int -> x * 1.0 }
val doubleToString: (Double) -> String = { x: Double -> x.toString() }
val intToStringPipe = intToDouble.let(doubleToString)

Vamos compilar:

val intToDouble: (Int) -> Double = { x: Int -> x * 1.0 }
val doubleToString: (Double) -> String = { x: Double -> x.toString() }
val intToStringPipe: String = intToDouble(1).let(doubleToString)

Se quisermos um equivalente de composição, mas com tubulação, ficaria assim:

val intToDouble: (Int) -> Double = { x: Int -> x * 1.0 }
val doubleToString: (Double) -> String = { x: Double -> x.toString() }
val intToStringPipe: (Int) -> String = { x: Int -> intToDouble(x).let(doubleToString) }

Espero que você tenha gostado deste artigo e aprendido algo novo. Deixe-me saber o que você pensa sobre isso. Sinta-se à vontade para compartilhar suas experiências com programação funcional em Kotlin, especialmente como ela se compara a linguagens funcionais.