Skip to main content

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...

$Group = "com\github\username"
$FunctionTestDir = "functors\src\test\kotlin\$Group\functors\function"
New-Item -Path "$FunctionTestDir\FunctionFunctorTest.kt" `
-ItemType "file" -Force

El funtor función debe cumplir con las dos propiedades fundamentales de los funtores:

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)

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...

$Group = "com\github\username"
$FunctionMainDir = "functors\src\main\kotlin\$Group\functors\function"

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.

Ejemplo

Supongamos que tenemos una función que valida la longitud de una cadena para asegurarse de que cumple con una longitud mínima:

val validateLength: (String) -> Boolean = { it.length >= 5 }

Esta función toma una String como entrada y devuelve un Boolean, indicando si la longitud es válida o no.

Ahora supongamos que tenemos una clase Person con un campo name, y queremos reutilizar la función validateLength para validar la longitud del nombre de una persona. Aquí es donde el funtor contravariante entra en juego: podemos transformar la entrada de la función para que acepte un Person en lugar de una String.

data class Person(val name: String, val age: Int)

Usaremos el método contramap para transformar la función validateLength para que funcione con un Person en lugar de una String. Para ello, definimos una función que extrae el nombre (name) de la persona y luego aplicamos la transformación con contramap:

val validatePersonName: (Person) -> Boolean =
validateLength.contramap { person: Person -> person.name }

En este caso, hemos usado contramap para crear una nueva función validatePersonName que toma un Person como entrada, extrae su nombre y aplica la validación de longitud sobre el nombre.

Ahora podemos usar la función validatePersonName para validar el nombre de una persona:

val person = Person("Alice", 29)
println(validatePersonName(person)) // Output: true

val person2 = Person("Bob", 25)
println(validatePersonName(person2)) // Output: false
Explicación del Código
  1. La función validateLength originalmente tomaba una String como entrada y verificaba si la longitud de la cadena era mayor o igual a 5.
  2. Usamos contramap para transformar la función de (String) -> Boolean a (Person) -> Boolean mediante la extracción del nombre de la persona.
  3. Esto nos permitió reutilizar la lógica de validación sin modificar la función original.
Ejercicio
  1. Define una función llamada celsiusToFahrenheit que tome un Double representando una temperatura en grados Celsius y devuelva otro Double con la conversión a grados Fahrenheit. La fórmula de conversión es:

    F=C×95+32 F = C \times \frac{9}{5} + 32

  2. Luego, usa funtores para transformar esta función. Vas a crear dos transformaciones adicionales:
    • La primera transformación redondeará el valor a un entero.
    • La segunda transformación convertirá el valor redondeado en una cadena de texto que agregue la palabra "°F" al final.
Ver hints
  • Usa la función map para aplicar transformaciones a la función original.
  • Puedes usar la función kotlin.math.round para redondear un número.
Solución
fun celsiusToFahrenheit(celsius: Double) = celsius * 9 / 5 + 32

fun main() {
val temperature = 36.6
val celsiusToFahrenheitFormatted = ::celsiusToFahrenheit
.map { kotlin.math.round(it) }
.map { "$it°F" }
println("Temperatura original: $temperature°C")
println("Fahrenheit redondeado y formateado: ${celsiusToFahrenheitFormatted(temperature)}")
}

Beneficios

  • Composición Elegante: El funtor función permite encadenar transformaciones de manera clara y declarativa, mejorando la legibilidad y manteniendo la lógica de transformación bien estructurada.
  • Flexibilidad: Al ser aplicable a cualquier tipo de función, el funtor función proporciona una herramienta flexible para reutilizar y modificar el comportamiento de funciones existentes sin duplicar código.
  • Compatibilidad con la Programación Funcional: El uso del funtor función está alineado con los principios de la programación funcional, facilitando el uso de funciones de orden superior y la composición de transformaciones.
  • Inmutabilidad: Las transformaciones aplicadas mediante el funtor función no alteran la función original, lo que garantiza inmutabilidad y seguridad en el manejo de funciones.

Limitaciones

  • Complejidad Adicional: La implementación y uso del funtor función pueden resultar menos intuitivos para desarrolladores que no están familiarizados con los conceptos de la programación funcional o la teoría de categorías.
  • Limitaciones en la Transformación: El funtor función solo permite transformar la salida de la función, lo que puede ser una restricción si se requiere manipular la entrada sin cambiar la lógica interna de la función original.
  • Dificultad para Debuggear: Encadenar múltiples transformaciones puede hacer que el seguimiento y debuggeo del código sea más complicado, ya que los errores pueden ocurrir en cualquiera de las transformaciones aplicadas en la cadena.
  • Sobrecarga Conceptual: Entender y aplicar correctamente las leyes de los funtores, como las propiedades de identidad y composición, puede ser un desafío, especialmente para quienes no están acostumbradxs a los conceptos matemáticos subyacentes.

¿Qué Aprendimos?

En esta lección, exploramos el funtor función y cómo las funciones mismas pueden comportarse como funtores, permitiéndonos aplicar transformaciones de manera elegante y declarativa sobre sus resultados. Algunos puntos clave que cubrimos incluyen:

  1. Definición del Funtor Función: Comprendimos cómo una función de tipo (T) -> R puede ser tratada como un funtor en R al fijar el tipo de entrada T, lo que nos permite transformar la salida de la función sin modificar su entrada.
  2. Propiedades del Funtor: Analizamos las dos leyes fundamentales de los funtores — Identidad y Composición — y cómo se aplican al funtor función para garantizar consistencia y coherencia en las transformaciones.
  3. Implementación Práctica: Vimos cómo definir la operación map en Kotlin para encadenar transformaciones sobre funciones de forma fluida y flexible, manteniendo la inmutabilidad y promoviendo un código más claro y estructurado.
  4. Aplicaciones en Programación Reactiva: Exploramos cómo el funtor función es útil en la programación reactiva y en el manejo de flujos de datos, permitiendo transformar las emisiones de datos a medida que fluyen por el sistema.
  5. Comparación y Composición: Discutimos la diferencia entre usar map y la composición tradicional de funciones, resaltando cómo map ofrece una forma más declarativa y generalizable de aplicar transformaciones.

El funtor función es una herramienta poderosa en el arsenal de la programación funcional, que permite trabajar con funciones de manera abstracta y flexible, facilitando el diseño de soluciones modulares y reutilizables.