Table of Contents
**Bow es una biblioteca para la programación funcional mecanografiada en Swift**
Pero antes que nada…
¿Qué es la programación funcional?
La Programación Funcional es un paradigma de programación – un estilo de construcción de la estructura y los elementos de los programas informáticos – que trata la computación como la evaluación de las funciones matemáticas y evita los cambios de estado y los datos mutables. -Wikipedia
TL;DR: *La programación funcional es la programación con funciones.*
Las funciones deben ser:
– Total: Hay una salida para cada entrada.,
– Determinante: Para una entrada dada, la función siempre devuelve la misma salida.
– Puro: La evaluación de las funciones no causa otros efectos además de computar la salida.
Pero… ¿Todo estado es malo?
No, el estado oculto e implícito es malo.
La programación funcional no elimina el estado, sólo lo hace visible y explícito.
func add(x: Int, y: Int) -> Int {
return x + y
}
Por ejemplo, estas funciones son **total**, **determinista** y **pura**. Siempre devolverá la misma salida para la misma entrada, siempre devolverá una salida y no dispara ningún efecto secundario.
func divide(x: Int, y: Int) -> Int {
guard y != 0 else { fatalError("Division by 0") }
return x / y
}
Esta función es **determinista** y **pura** pero no **total**. Es una función **parcial** porque si el argumento de los segundos es 0, no hay forma de que proporcione una salida válida.
func dice(withSides sides: UInt32) -> UInt32 {
return 1 + arc4random_uniform(sides)
}
Dos invocaciones con la misma entrada producirán (lo más probable) valores diferentes, ya que la salida es aleatoria. Esto significa que la función es **no determinante.**
func save(data: String) -> Bool {
print("Saving data: \(data)")
return true
}
Esa función es **total** y **determinista** pero no es pura porque en cada ejecución hará algo más que computar la salida. Imprime un mensaje en ese ejemplo. Eso hace que esta función sea **impuro.**
¿Qué podemos hacer?
Trabajar con funciones impuras
El software del mundo real rara vez tiene funciones tan bien escritas como la función de **`add`** arriba. Por lo general, tenemos que tratar con funciones parciales, no determinantes y/o impuras, ya que el software tiene que tratar con errores y realizar efectos secundarios para ser útil. Además de presentar problemas que rompen la transparencia referencial, la composición de la función no puede hacerse en estas circunstancias. ¿Cómo podemos hacer frente a esas situaciones? Bow proporciona numerosos tipos de datos que pueden utilizarse para modelar diferentes efectos en nuestras funciones. Usando esos tipos de datos, como **`Option`**, **`Either`** o **`IO`** puede ayudarnos a transformar funciones parciales, no determinantes o impuras en totales, determinantes y puras. Una vez que las tenemos, podemos usar combinadores que nos ayudarán a componer bien esas funciones. Usaremos tipos de datos de arco para transformar algunas parciales, impuras y no deterministas. **Trabajando con la función anterior:**
func divide(x: Int, y: Int) -> Int {
guard y != 0 else { fatalError("Division by 0") }
return x / y
}
Utilizaremos un tipo de datos que proporciona el arco. **Option**
func divideOption(x : Int, y : Int) -> Option {
guard y != 0 else { return Option.none() }
return Option.some(x / y)
}
Ahora, divideOption es capaz de devolver un valor para cada posible entrada; es decir, es una función total. «Option» es similar a «Swift Optional». Eso significa que Option y Optional son isomórficas: hay un par de funciones (a saber, toOption y fromOption) que, cuando se componen, su resultado es la función de identidad. También podemos reescribir la función de división usando `Either`.
func divideEither(x : Int, y : Int) -> Either<DivideError, Int> {
guard y != 0 else { return Either.left(.divisionByZero) }
return Either.right(x / y)
}
Con Either nos permite ser más explícitos sobre el error en el tipo de retorno, ayudando a la persona que llama a estar preparada para hacer frente a las posibles salidas que pueda recibir. No obstante, **el tipo izquierdo no tiene por qué ser un error.** Esa es la principal diferencia entre el tipo de datos swift **Result vs Either.** Con el tipo Resultado tenemos el método para migrarlo a un cualquiera u opciones importando **BowResult.**
import BowResult
let result = Result<Int, DivideError>(value: 2)
let eitherFromResult = result.toEither()
let optionFromResult = result.toOption()
Efectos
Hasta ahora hemos visto algunos tipos de datos que Bow proporciona para ayudarnos a convertir nuestras funciones parciales en totales. Sin embargo, en muchos casos necesitamos realizar efectos, que hacen que nuestras funciones sean impuras. Para estos casos, Bow proporciona el tipo de datos «IO», con el fin de encapsular los efectos.
func fetchSomething() -> (String) {
//Server call
return "{ \"MyData\ }"
}
> El tipo IO encapsula un efecto de tipo A, pero no lo ejecuta
func fetchSomething() -> IO {
return IO.invoke {
return "{ \"MyData\ }"
}
}
Entonces, podemos usar las funciones `IO` para transformar el resultado a través de las capas de nuestra aplicación, y activar el efecto secundario con `.unsafePerformIO` cuando sea necesario.
fetchSomething()
.map(jsonToOurModel)
.unsafePerformIO()
Lo que obtenemos: R**eferential transparency**
Si una función es total, determinística y pura, tiene una propiedad llamada **referential transparency**. La transparencia referencial significa generalmente que siempre podemos sustituir una función con su valor de retorno sin que ello afecte al comportamiento de la aplicación.
Lo que obtenemos: R**eferential transparency**
func multiply(x: Int, y: Int) -> Int {
return x * y
}
let multi = multiply(x: 2, y: 2) * multiply(x: 2, y: 2)
//Produce the same result
let multiplyResult = multi * multi
Memorización
Otra consecuencia de tener una función que es referencialmente transparente es que puede ser memorizada. La memorización es una técnica utilizada para almacenar en caché valores ya computados, especialmente si tienen un alto costo para ser computados. Cuando se memoriza una función, se puede pensar en ella como una tabla de consulta donde se guardan las entradas y sus correspondientes salidas. Las invocaciones subsiguientes sólo recuperan el valor de la tabla en lugar de calcularlo realmente.
Ejemplo
func longRunningFunction(_ id: Int) -> Data {
//Assuming a long running function
return someData
}
let memoizedLongRunningFunc = memoize(longRunningFunction)
//Will compute for the first time that function and save the output
let longRunning1 = memoizedLongRunningFunc(1)
//Will use the memoized result given 0 computation cost
let longRunning2 = memoizedLongRunningFunc(1)
Composición de la función
En este punto si tuviéramos una función pura, determinista y total es fácil componerlas. Como las funciones también son referencialmente transparentes necesitamos alguna operación para combinarlas. La operación esencial para las funciones es la composición de la función. En Bow teníamos los operadores «compose» o «<<» operadores para recibir dos funciones y proporcionar una nueva función que se comporta como aplicar ambas funciones secuencialmente. En algunos casos, «compose« puede ser difícil de leer de derecha a izquierda, o simplemente no es conveniente de usar. Para esos casos, Bow tiene funciones de utilidad que invierten el orden de los argumentos usando `andThen` o `>>>.`
Ejemplo
Teníamos algunas funciones que tomaban una String y la devolvían basada en esa String con algunas modificaciones.
func shout(_ argument: String) -> String {
return argument.uppercased()
}
func exclaim(_ argument: String) -> String {
return argument + "!!!"
}
func applefy(_ argument: String) -> String {
return argument + "?"
}
func bananafy(_ argument: String) -> String {
return argument + "?"
}
func kiwify(_ argument: String) -> String {
return argument + "?"
}
Con la composición de la función Bow podemos hacerlo así:
let shoutFruitComposed = shout
>>> exclaim
>>> applefy
>>> bananafy
>>> kiwify
print(shoutFruit("I love fruit"))
// I LOVE FRUIT!!!???
Compare esta última operación con esta versión sin componer:
func shoutFruit(_ argument: String) -> String {
// Hard to read
return kiwify(bananafy(applefy(exclaim(shout(argument)))))
}
print(shoutFruitComposed("I love fruit"))
// I LOVE FRUIT!!!???
Tal vez puedas pensar que ese ejemplo no es útil en absoluto en una aplicación diaria. Así que aquí está un ejemplo más práctico usando la composición de funciones del sitio *pointfree.co’s*!
Conclusiones
Bow está todavía en una etapa temprana de desarrollo. Ha sido probado exhaustivamente pero necesita ser aplicado en algunos proyectos para evaluar su utilidad.
Algunos de los beneficios de usar Bow y la programación funcional son:
– Código más fácil de razonar
– Mantenibilidad
– Testabilidad
– Reutilización
Este artículo sólo cubre una pequeña porción de todo el poder que tiene el framework de Bow.
«>
Author
-
More than 10 years on software development field, and working on mobile development since 2013. Experience creating apps from scratch and working on big applications with several dependencies. Used to work with the latest technologies and take care of architectural decisions. Able to lead small iOs teams, always from the tech side.
Ver todas las entradas