El functor Función
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/functional-programming-kt
Ya vimos que un functor es una estructura que puede ser mapeada, es decir, que admite la aplicación de una función sobre sus elementos internos sin cambiar su estructura. Un ejemplo común es la lista: podemos mapear una función sobre cada elemento de una lista y obtener una nueva lista con los resultados.
Sin embargo, existe un tipo de funtor que es menos obvio, pero igualmente poderoso: el funtor función. En esta lección, exploraremos cómo las funciones mismas pueden ser consideradas funtores y cómo esto nos permite componer y transformar funciones de manera elegante y eficiente.
El funtor función
En el contexto de programación funcional y teoría de categorías, una función de tipo (T) -> R
puede ser considerada como un funtor en R
cuando fijamos T
. Esto significa que podemos definir una operación de mapeo sobre funciones que nos permite transformar su salida sin modificar su entrada.
Propiedades del funtor función
Si quieres crear los archivos desde la terminal...
- Windows
- Windows (corto)
- Linux/Mac
$Group = "com\github\username"
$FunctionTestDir = "functors\src\test\kotlin\$Group\functors\function"
New-Item -Path "$FunctionTestDir\FunctionFunctorTest.kt" `
-ItemType "file" -Force
$Group = "com\github\username"
$FunctionTestDir = "functors\src\test\kotlin\$Group\functors\function"
ni "$FunctionTestDir\FunctionFunctorTest.kt" -i f -f
GROUP="com/github/username"
FUNCTION_TEST_DIR="functors/src/test/kotlin/$GROUP/functors/function"
mkdir -p $FUNCTION_TEST_DIR
touch "$FUNCTION_TEST_DIR/FunctionFunctorTest.kt"
El funtor función debe cumplir con las dos propiedades fundamentales de los funtores:
- Código esencial
- Código completo
Identidad
Mapear la función identidad sobre un funtor no cambia el funtor.
val identity = function.map { it }
identity(420) shouldBe function(420)
Composición
Mapear la composición de dos funciones es lo mismo que mapear una función y luego mapear la otra.
val composition = function.map { it + 1 }.map { it * 2 }
val composed = function.map { (it + 1) * 2 }
composition(420) shouldBe composed(420)
package com.github.username.functors.function
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
class FunctionFunctorTest : FreeSpec({
"A function functor" - {
"when mapped with the identity function" - {
"should return the same function" {
with(FunctionFunctor<Int>()) {
val function = { x: Int -> x * 2 }
val identity = function.map { it }
identity(420) shouldBe function(420)
}
}
}
"when composed with two functions" - {
"should return the same function as the composition" {
with(FunctionFunctor<Int>()) {
val function = { x: Int -> x * 2 }
val composition = function.map { it + 1 }.map { it * 2 }
val composed = function.map { (it + 1) * 2 }
composition(420) shouldBe composed(420)
}
}
}
}
})
Estas propiedades garantizan que el funtor función se comporte de manera coherente con las expectativas matemáticas de un funtor.
Definición
Si quieres crear los archivos desde la terminal...
- Windows
- Windows (corto)
- Linux/Mac
$Group = "com\github\username"
$FunctionMainDir = "functors\src\main\kotlin\$Group\functors\function"
$Group = "com\github\username"
GROUP="com/github/username"
Consideremos una función f: (T) -> R
. Queremos aplicar una transformación g: (R) -> S
a la salida de f
, obteniendo una nueva función h: (T) -> S
. Esto es posible gracias a la composición de funciones.
La operación de mapeo para el funtor función se define como:
class FunctionFuntor<T> {
fun <R, S> ((T) -> R).map(f: (R) -> S): (T) -> S = { t -> f(this(t)) }
}
Aquí, this
es la función original de tipo (T) -> R
, y f
es la función que queremos aplicar a su resultado.
Ejemplos Prácticos
Transformando la Salida de una Función
Supongamos que tenemos una función que obtiene la longitud de una cadena:
val getLength: (String) -> Int = { it.length }
Queremos transformar esta función para que, en lugar de devolver la longitud, devuelva si la longitud es par o impar:
with(FunctionFuntor<String>()) {
val isLengthEven: (String) -> Boolean = getLength.map { it % 2 == 0 }
}
Aquí, hemos mapeado la función getLength
con una transformación que convierte un Int
en un Boolean
.
Composición vs. Mapeo
Aunque podríamos lograr lo mismo mediante la composición tradicional de funciones:
val isLengthEven: (String) -> Boolean = { s -> (getLength(s) % 2 == 0) }
El uso del funtor función y su operación map
nos permite expresar esta transformación de manera más declarativa y generalizable.
Aplicaciones en Programación Reactiva
En programación reactiva, es común trabajar con flujos de datos y transformar las emisiones a medida que fluyen por el sistema. El funtor función permite aplicar transformaciones a las funciones que generan o manipulan estos datos.
Por ejemplo, si tenemos una función que obtiene datos de una API:
val fetchData: (Request) -> Response = { request -> /* ... */ }
Podemos transformar su salida para extraer solo la información que nos interesa:
with(FunctionFuntor<Request>()) {
val extractData: (Request) -> Data = fetchData.map { response -> response.data }
}
Implementación generalizada
En Kotlin, podemos extender la funcionalidad de las funciones utilizando la programación de alto nivel y las funciones de extensión.
Definición de la Función map
Podemos definir la función de extensión map
para cualquier función:
fun <T, R, S> ((T) -> R).map(f: (R) -> S): (T) -> S = { t -> f(this(t)) }
Esto nos permite encadenar transformaciones de manera fluida:
val originalFunction: (Int) -> Int = { it * 2 }
val transformedFunction: (Int) -> String = originalFunction
.map { it + 3 }
.map { "Result: $it" }
println(transformedFunction(5)) // Output: Result: 13
En este ejemplo, hemos aplicado dos transformaciones sucesivas sobre la función original.
Funtor Contravariante
Es importante notar que también existe el concepto de funtor contravariante, que en lugar de transformar la salida de una función, transforma su entrada.
La operación para un funtor contravariante se define como:
fun <R, A, B> ((A) -> R).contramap(f: (B) -> A): (B) -> R = { b -> this(f(b)) }
Esto permite modificar el tipo de entrada de una función mediante una transformación.