Skip to main content

Mónada Option

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/functional-programming-kt

Be lazy...

Puedes ejecutar el siguiente comando para crear el módulo

./gradlew setupOptionModule

Mientras se crean los archivos necesarios, puedes leer el código para saber qué está pasando.

Preocúpate de que el plugin option esté aplicado en el archivo build.gradle.kts de tu proyecto.

./gradlew setupOptionModule

Preocúpate de que el nuevo módulo esté incluido en el archivo settings.gradle.kts.

La mónada Option es una de las estructuras fundamentales en la programación funcional. Su objetivo es representar la presencia o ausencia de un valor de manera explícita, eliminando la necesidad de utilizar valores nulos. Este enfoque previene errores comunes como el temido NullPointerException y mejora la seguridad del código al obligar a quien desarrolla a manejar explícitamente los casos en los que un valor podría estar ausente.

Option encapsula el concepto de tener o no un valor a través de dos componentes principales:

  • Some: Indica que el valor está presente.
  • None: Indica que el valor está ausente.

Esta estructura facilita operaciones seguras y permite componer funciones que pueden devolver resultados opcionales sin necesidad de realizar verificaciones explícitas de null.

Interpretación alternativa

Podemos pensar en Option como una lista que puede contener 0 o 1 elementos. Si la lista está vacía, el valor está ausente (None); si contiene un elemento, el valor está presente (Some). Esto nos puede ayudar a pensar qué operaciones debieran ser posibles de realizar sobre Option.

Leyes de la Mónada Option

Si quieres crear los archivos desde la terminal...

$Group = 'com\github\username'
$OptionTestDir = "monads\src\test\kotlin\$Group\option"
New-Item -Path $OptionTestDir -ItemType Directory -Force
"package $Group.option" -replace '\\', '.' |
Out-File -FilePath "$OptionTestDir\OptionTest.kt"

Implementación

A continuación, se presenta una implementación simple de Option en Kotlin que sigue el patrón monádico:

sealed class Option<out A> {
inline fun <B> flatMap(f: (A) -> Option<B>): Option<B> = when (this) {
is None -> this
is Some -> f(value)
}

companion object {
fun <T> pure(value: T): Option<T> = Some(value)
}
}

data object None : Option<Nothing>()

data class Some<out T>(val value: T) : Option<T>()

Explicación

  • pure: Toma un valor y lo envuelve en Some, creando una instancia de Option que indica que el valor está presente.
  • flatMap: Permite aplicar una función a un valor encapsulado en Some, devolviendo una nueva mónada Option. Si el valor es None, no se realiza ninguna operación y se devuelve None.

Ejemplo de Uso

Supongamos que queremos encadenar varias operaciones sobre un valor que puede estar presente o no:

val result = Option.pure(5)
.flatMap { Option.pure(it * 2) } // Multiplicamos por 2
.flatMap { Option.pure(it + 3) } // Sumamos 3
.flatMap { Option.pure(it / 4) } // Dividimos por 4

println(result) // Imprime: Some(2)

En este ejemplo, cada operación se encadena de forma segura utilizando flatMap, garantizando que si en algún punto el valor fuera None, el encadenamiento se detendría y devolvería None sin errores de acceso a null.

Semejanza con ?.

El operador de llamada segura, ?., en Kotlin se comporta de manera similar a flatMap cuando trabajamos con valores opcionales (Option). Ambos mecanismos permiten aplicar una operación a un valor únicamente si este está presente, evitando errores por acceso a valores nulos.

Ejemplo con ?.:

val result: Int? = 5
?.let { it * 2 } // Multiplicamos por 2
?.let { it + 3 } // Sumamos 3
?.let { it / 4 } // Dividimos por 4

println(result) // Imprime: 2

Ambos ejemplos (con Option y ?.) permiten encadenar operaciones de forma segura. La diferencia es que Option encapsula explícitamente el valor dentro de una estructura monádica (Some o None), mientras que el operador ?. trabaja con valores null. Sin embargo, el principio subyacente es el mismo: garantizar que las operaciones solo se apliquen si el valor está presente, sin causar errores por acceder a null.

Ejercicio: Tipos anulables

¿Son los tipos anulables en Kotlin mónadas? ¿Por qué?

Solución

No, los tipos anulables en Kotlin no se consideran mónadas en sentido estricto, aunque comparten algunas características monádicas.

La razón principal es que en la definición formal de una mónada, se requiere una función pure (también conocida como unit o return) que tome un valor de tipo A y lo envuelva en un contexto monádico M<A>. En Kotlin, no existe una función estándar que tome un valor no nulo y lo convierta explícitamente en un tipo anulable (A?). Aunque se puede asignar directamente un valor no nulo a un tipo anulable debido a la naturaleza del sistema de tipos de Kotlin, esto no equivale a un constructor monádico formal.

Ventajas de Usar Option

  1. Seguridad: Evita el uso de valores nulos, proporcionando un enfoque más seguro para trabajar con valores opcionales.
  2. Composición limpia: Facilita el encadenamiento de operaciones sobre valores opcionales sin necesidad de verificaciones explícitas de null.
  3. Expresividad: Hace que el código sea más expresivo y claro, al representar explícitamente la ausencia de un valor con None.
¿Por qué es importante que Option sea covariante en A?

Es importante que Option sea covariante en A para permitir la substitución de subtipos y garantizar la compatibilidad de tipos en contextos donde se espera un Option de un tipo más general.

Supongamos que tenemos una jerarquía de clases:

open class Animal
class Dog : Animal

Y una función que acepta un Option<Animal>:

fun handleAnimal(option: Option<Animal>) {
// Procesa el animal
}

Si Option es covariante en A, podemos pasar un Option<Dog> a esta función sin problemas:

val dogOption: Option<Dog> = Some(Dog())
handleAnimal(dogOption)

Sin la covarianza, el código anterior no sería válido porque Option<Dog> no sería un subtipo de Option<Animal>, y la función handleAnimal no aceptaría ese argumento.

Ejercicio: Extensiones para Option

Implementa las siguientes funciones para la mónada Option:

  • getOrElse: (Option<T>, T) -> T: Proporciona un valor por defecto si el Option es None.
  • map: (Option<T>, (T) -> U) -> Option<U>: Aplica una transformación al valor contenido en Option.
Solución
fun <T> getOrElse(option: Option<T>, default: T) = when (option) {
is None -> default
is Some -> option.value
}

fun <T, U> map(option: Option<T>, f: (T) -> U) = option.flatMap {
Option.pure(f(it))
}

Bibliografías Recomendadas

  • 📚 "6. Simple Algebraic Data Types". (2019). Bartosz Milewski, en Category Theory for Programmers, (pp. 55–68.) Millington Keynes.
  • 📚 "Handling errors without exceptions". (2021). Marco Vermeulen, Rúnar Bjarnason, Paul Chiusano, en Functional Programming in Kotlin, (pp. 56–76.) Manning Publications Co. LLC.