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
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
.
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...
- Windows
- Windows (corto)
- Linux/Mac
$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"
$Group = 'com\github\username'
$OptionTestDir = "monads\src\test\kotlin\$Group\option"
md $OptionTestDir
"package $Group.option" -replace '\\', '.' > `
"$OptionTestDir\OptionTest.kt"
GROUP="com/github/username"
OPTION_TEST_DIR="monads/src/test/kotlin/$GROUP/option"
mkdir -p "$OPTION_TEST_DIR"
echo "package ${GROUP//\//.}.option" > "$OPTION_TEST_DIR/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 enSome
, creando una instancia deOption
que indica que el valor está presente.flatMap
: Permite aplicar una función a un valor encapsulado enSome
, devolviendo una nueva mónadaOption
. Si el valor esNone
, no se realiza ninguna operación y se devuelveNone
.
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
.
?.
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
- Seguridad: Evita el uso de valores nulos, proporcionando un enfoque más seguro para trabajar con valores opcionales.
- Composición limpia: Facilita el encadenamiento de operaciones sobre valores opcionales sin necesidad de verificaciones explícitas de
null
. - 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 elOption
esNone
.map: (Option<T>, (T) -> U) -> Option<U>
: Aplica una transformación al valor contenido enOption
.
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.