Translate

Mostrando las entradas con la etiqueta monads. Mostrar todas las entradas
Mostrando las entradas con la etiqueta monads. Mostrar todas las entradas

domingo, 3 de noviembre de 2024

Monadex en Elixir


Elixir, aunque no tiene una implementación nativa de mónadas como Haskell, permite patrones funcionales que se pueden potenciar con bibliotecas como Monadex. Monadex nos brinda las clásicas Maybe y Either, permitiéndonos manejar valores opcionales y errores de forma compositiva, y aprovechando el poder de la programación funcional.

Primero, agrega Monadex a tu archivo mix.exs:

defp deps do

  [

    {:monadex, "~> 1.1"}

  ]

end


Ejecuta mix deps.get para instalar la dependencia.


La mónada Maybe es útil cuando deseas trabajar con valores que podrían ser nil. Monadex provee funciones para encapsular operaciones en un flujo que se detiene automáticamente si algún valor es nil.

Supongamos que estamos buscando datos de un usuario y aplicando transformaciones a su información.


alias Monadex.Maybe


def get_user_info(user) do

  Maybe.return(user)

  |> Maybe.and_then(&get_name/1)

  |> Maybe.and_then(&upcase_name/1)

end


defp get_name(%{name: name}), do: Maybe.return(name)

defp get_name(_), do: Maybe.nothing()


defp upcase_name(name), do: Maybe.return(String.upcase(name))


user = %{name: "Juan"}

get_user_info(user) # Retorna: {:ok, "JUAN"}


Either es útil para operaciones que pueden tener éxito o devolver un error. Similar a Maybe, Either permite encadenar operaciones pero distingue entre :ok y :error.


Procesemos datos que pueden fallar en algún paso.


alias Monadex.Either


def process_data(data) do

  Either.return(data)

  |> Either.and_then(&validate/1)

  |> Either.and_then(&transform/1)

end


defp validate(data) do

  if valid?(data), do: Either.ok(data), else: Either.error("Invalid data")

end


defp transform(data), do: Either.ok("Processed: #{data}")


process_data("valid_data")  # Retorna: {:ok, "Processed: valid_data"}

process_data("invalid")      # Retorna: {:error, "Invalid data"}


Monadex facilita el manejo de errores y valores opcionales en Elixir, siguiendo patrones funcionales.

Dejo link: https://github.com/rob-brown/MonadEx


jueves, 2 de junio de 2022

Mónadas en Cats parte 9

Veamos otra mónada útil: el tipo Either de la biblioteca estándar de Scala. En Scala 2.11 y versiones anteriores, muchas personas no consideraban a Either como una mónada porque no tenía métodos map y flatMap. En Scala 2.12, sin embargo, esto cambio. 

En Scala 2.11, tampoco tenía un mapa predeterminado o un método flatMap. Esto hizo que la versión Scala 2.11 de Either fuera inconveniente para usar en comprensiones. Tuvimos que insertar llamadas a .right en cada cláusula del generador:

val either1: Either[String, Int] = Right(10)

val either2: Either[String, Int] = Right(32)

for {

  a <- either1.right

  b <- either2.right

} yield a + b


En Scala 2.12, se rediseñó, el moderno Either toma la decisión de que el lado derecho representa el caso de éxito y, por lo tanto, admite map y flatMap directamente. Esto hace que las comprensiones sean mucho más agradables:

for {

a <- either1

b <- either2

} yield a + b

// res1: Either[String, Int] = Right(42)


Cats retrotrae este comportamiento a Scala 2.11 a través de la importación cats.syntax.either, lo que nos permite usar el bien Either en todas las versiones compatibles de Scala. En Scala 2.12+ podemos omitir esta importación o dejarla en su lugar sin romper nada:


import cats.syntax.either._ // for map and flatMap

for {

a <- either1

b <- either2

} yield a + b



miércoles, 25 de mayo de 2022

Mónadas en Cats parte 8

Ya demostramos la sintaxis de map y flatMap de Cats al escribir un método que abstraía sobre diferentes mónadas:

import cats.Monad

import cats.syntax.functor._ // for map

import cats.syntax.flatMap._ // for flatMap

def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] =

for {

x <- a

y <- b

} yield x*x + y*y

Este método funciona bien en Opcion y List, pero no podemos llamarlo pasando valores simples:

sumSquare(3, 4)
// error: no type parameters for method sumSquare: (a: F[Int], b: F[
Int])(implicit evidence$1: cats.Monad[F])F[Int] exist so that it
can be applied to arguments (Int, Int)
...

Sería increíblemente útil si pudiéramos usar sumSquare con parámetros que estuvieran en una mónada o que no estuvieran en ninguna mónada. Esto nos permitiría abstraernos sobre código monádico y no monádico. Afortunadamente, Cats proporciona el tipo de identificación para cerrar la brecha:

import cats.Id
sumSquare(3 : Id[Int], 4 : Id[Int])
// res1: Id[Int] = 25

Id nos permite llamar a nuestro método monádico usando valores simples. Sin embargo, la semántica exacta es difícil de entender. ¡Convertimos los parámetros a sumSquare como Id[Int] y recibimos un Id[Int] como resultado! ¿Qué está sucediendo? Aquí está la definición de Id para explicar:

package cats
type Id[A] = A

id es en realidad un alias de tipo que convierte un tipo atómico en un constructor de tipo de parámetro único. Podemos emitir cualquier valor de cualquier tipo a una identificación correspondiente:

"Dave" : Id[String]
// res2: Id[String] = "Dave"
123 : Id[Int]
// res3: Id[Int] = 123
List(1, 2, 3) : Id[List[Int]]
// res4: Id[List[Int]] = List(1, 2, 3)

Cats proporciona instancias de varias clases de tipos para Id, incluidos Functor y Monad. Estos nos permiten llamar map, flatMap y pure con valores simples:

val a = Monad[Id].pure(3)
// a: Id[Int] = 3
val b = Monad[Id].flatMap(a)(_ + 1)
// b: Id[Int] = 4
import cats.syntax.functor._ // for map
import cats.syntax.flatMap._ // for flatMap
for {
x <- a
y <- b
} yield x + y
// res5: Id[Int] = 7

La capacidad de abstracción sobre el código monádico y no monádico es extremadamente poderosa. Por ejemplo, podemos ejecutar código de forma asincrónica en producción usando Future y sincrónicamente en prueba usando Id. 

domingo, 22 de mayo de 2022

Mónadas en Cats parte 7

 La sintaxis de las mónadas proviene de tres lugares:

  • cats.syntax.flatMap proporciona sintaxis para flatMap;
  • cats.syntax.functor proporciona sintaxis para el map;
  • cats.syntax.applicative proporciona sintaxis para pure.
En la práctica, a menudo es más fácil importar todo de una sola vez desde cats.implicits. Sin embargo, usaremos las importaciones individuales aquí para mayor claridad.

Podemos usar pure para construir instancias de una mónada. A menudo necesitaremos especificar el parámetro de tipo para eliminar la ambigüedad de la instancia particular que queremos.

import cats.instances.option._  // for Monad
import cats.instances.list._ // for Monad
import cats.syntax.applicative._ // for pure

1.pure[Option]
// res5: Option[Int] = Some(1)
1.pure[List]
// res6: List[Int] = List(1)

Es difícil demostrar los métodos flatMap y map directamente en las mónadas de Scala como Option y List, porque definen sus propias versiones explícitas de esos métodos. En su lugar, escribiremos una función genérica que realice un cálculo en los parámetros que vienen envueltos en una mónada a elección del usuario:

import cats.Monad
import cats.syntax.functor._ // for map
import cats.syntax.flatMap._ // for flatMap

def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] = a.flatMap(x => b.map(y => x*x + y*y))

import cats.instances.option._ // for Monad
import cats.instances.list._  // for Monad

sumSquare(Option(3), Option(4))
// res7: Option[Int] = Some(25)

sumSquare(List(1, 2, 3), List(4, 5))
// res8: List[Int] = List(17, 26, 20, 29, 25, 34)

Podemos reescribir este código usando comprensiones. El compilador "hará lo correcto" reescribiendo nuestra comprensión en términos de flatMap y map e insertando las conversiones implícitas correctas para usar nuestra Monad:

def sumSquare[F[_]: Monad](a: F[Int], b: F[Int]): F[Int] =
for {
    x <- a
    y <- b
} yield x*x + y*y

sumSquare(Option(3), Option(4))
// res10: Option[Int] = Some(25)

sumSquare(List(1, 2, 3), List(4, 5))
// res11: List[Int] = List(17, 26, 20, 29, 25, 34)

Eso es más o menos todo lo que necesitamos saber sobre las generalidades de las mónadas en Cats. 

sábado, 21 de mayo de 2022

Mónadas en Cats parte 6

Cats proporciona instancias para todas las mónadas en la biblioteca estándar (Option, List, Vector, etc.) a través de cats.instances:

import cats.instances.option._ // for Monad

Monad[Option].flatMap(Option(1))(a => Option(a*2))

// res0: Option[Int] = Some(2)

import cats.instances.list._ // for Monad

Monad[List].flatMap(List(1, 2, 3))(a => List(a, a*10))

// res1: List[Int] = List(1, 10, 2, 20, 3, 30)

import cats.instances.vector._ // for Monad

Monad[Vector].flatMap(Vector(1, 2, 3))(a => Vector(a, a*10))

// res2: Vector[Int] = Vector(1, 10, 2, 20, 3, 30)

Cats también proporciona una mónada para Future. A diferencia de los métodos de la propia clase Future, los métodos pure y flatMap de la mónada no pueden aceptar parámetros ExecutionContext implícitos (porque los parámetros no forman parte de las definiciones de la característica Monad). Para evitar esto, Cats requiere que tengamos un ExecutionContext en el alcance cuando invocamos una Monad para Future: 

import cats.instances.future._ // for Monad
import scala.concurrent._
import scala.concurrent.duration._

val fm = Monad[Future]
// error: Could not find an instance of Monad for scala.concurrent.
Future
// val fm = Monad[Future]
//
 ^^^^^^^^^^^^^

Poner el ExecutionContext en el alcance corrige la resolución implícita requerida para invocar la instancia:

import scala.concurrent.ExecutionContext.Implicits.global

val fm = Monad[Future]
// fm: Monad[Future] = cats.instances.FutureInstances$$anon$1@7ba44cd6

La instancia de Monad usa el ExecutionContext capturado para llamadas posteriores a pure y flatMap:

val future = fm.flatMap(fm.pure(1))(x => fm.pure(x + 2))
Await.result(future, 1.second)
// res4: Int = 3

Además de lo anterior, Cats proporciona una gran cantidad de nuevas mónadas que no tenemos en la biblioteca estándar. Nos familiarizaremos con algunos de estos en un momento.



jueves, 19 de mayo de 2022

Mónadas en Cats parte 5

La clase de tipo de mónada es cats.Monad. Monad extiende otras dos clases de tipos:

FlatMap, que proporciona el método flatMap, y Applicative, que proporciona pure. Applicative también extiende Functor, que le da a cada Monad un método de map.

Aquí hay algunos ejemplos que usan pure y flatMap, y map:

import cats.Monad

import cats.instances.option._ // for Monad

import cats.instances.list._ // for Monad

val opt1 = Monad[Option].pure(3)

// opt1: Option[Int] = Some(3)

val opt2 = Monad[Option].flatMap(opt1)(a => Some(a + 2))

// opt2: Option[Int] = Some(5)

val opt3 = Monad[Option].map(opt2)(a => 100 * a)

// opt3: Option[Int] = Some(500)


val list1 = Monad[List].pure(3)

// list1: List[Int] = List(3)

val list2 = Monad[List].flatMap(List(1, 2, 3))(a => List(a, a*10))

// list2: List[Int] = List(1, 10, 2, 20, 3, 30)

val list3 = Monad[List].map(list2)(a => a + 123)

// list3: List[Int] = List(124, 133, 125, 143, 126, 153)


Monad proporciona muchos otros métodos, incluidos todos los métodos de Functor.

lunes, 16 de mayo de 2022

Mónadas en Cats parte 4

Si bien solo hemos hablado de flatMap anteriormente, el comportamiento monádico se captura formalmente en dos operaciones:

  • una operación de tipo A => F[A];
  • flatMap, de tipo (F[A], A => F[B]) => F[B].

La primera operación proporciona una forma de crear un nuevo contexto monádico a partir de un valor simple. flatMap proporciona el paso de secuenciación que ya hemos discutido, extrayendo el valor de un contexto y generando el siguiente contexto en la secuencia. Aquí hay una versión simplificada de la clase de tipos Monad en Cats:

trait Monad[F[_]] {

    def pure[A](value: A): F[A]

    def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]

}

pure y flatMap deben obedecer un conjunto de leyes que nos permiten secuenciar operaciones libremente sin problemas técnicos ni efectos secundarios no deseados: 

Identidad izquierda: llamar a pure y a flatMap con func es lo mismo que llamar a func:

pure(a).flatMap(func) == func(a)

Identidad derecha: pasar pure a flatMap es lo mismo que no hacer nada:

m.flatMap(pure) == m

Asociatividad: flatMap sobre dos funciones f y g es lo mismo que flatMap sobre f y luego flatMap sobre g:

m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))

Toda mónada es también un funtor. Podemos definir map de la misma manera para cada mónada usando los métodos existentes, flatMap y pure:

trait Monad[F[_]] {
    def pure[A](a: A): F[A]
    def flatMap[A, B](value: F[A])(func: A => F[B]): F[B]
    def map[A, B](value: F[A])(func: A => B): F[B] = flatMap(value)(a => pure(func(a)))
}




domingo, 15 de mayo de 2022

Mónadas en Cats parte 3

Future es una mónada que secuencia los cálculos sin preocuparse de que puedan ser asíncronos:

import scala.concurrent.Future

import scala.concurrent.ExecutionContext.Implicits.global


def doSomethingLongRunning: Future[Int] = ???

def doSomethingElseLongRunning: Future[Int] = ???


def doSomethingVeryLongRunning: Future[Int] =

    for {

         result1 <- doSomethingLongRunning

         result2 <- doSomethingElseLongRunning

    } yield result1 + result2

Una vez más, especificamos el código que se ejecutará en cada paso, y flatMap se ocupa de todas las terribles complejidades subyacentes de los grupos de subprocesos y los programadores.

Si ha hecho un uso extensivo de Future, sabrá que el código anterior ejecuta cada operación en secuencia. Esto se vuelve más claro si expandimos la comprensión para mostrar las llamadas anidadas a flatMap:

def doSomethingVeryLongRunning: Future[Int] =
    doSomethingLongRunning.flatMap { result1 =>
        doSomethingElseLongRunning.map { result2 =>
            result1 + result2

Cada Futuro en nuestra secuencia es creado por una función que recibe el resultado de un Futuro anterior. En otras palabras, cada paso en nuestro cálculo solo puede comenzar una vez que finaliza el paso anterior.



Podemos ejecutar futuros en paralelo, por supuesto, pero esa es otra historia y se contará en otro momento. Las mónadas tienen que ver con la secuenciación.

viernes, 13 de mayo de 2022

Mónadas en Cats parte 2

Cuando nos encontramos por primera vez con flatMap como desarrolladores en ciernes de Scala, tendemos a pensar en él como un patrón para iterar sobre Listas. Esto se ve reforzado por la sintaxis de las comprensiones for, que se parecen mucho a los bucles for imperativos:

for {

x <- (1 to 3).toList

y <- (4 to 5).toList

} yield (x, y)

// res5: List[(Int, Int)] = List(

// (1, 4),

// (1, 5),

// (2, 4),

// (2, 5),

// (3, 4),

// (3, 5)

// )

Sin embargo, hay otro modelo mental que podemos aplicar que destaca el comportamiento monádico de List. Si pensamos en las listas como conjuntos de resultados intermedios, flatMap se convierte en una construcción que calcula permutaciones y combinaciones.

Por ejemplo, en la comprensión anterior hay tres valores posibles de x y dos valores posibles de y. Esto significa que hay seis valores posibles de (x, y). flatMap está generando estas combinaciones a partir de nuestro código, que establece la secuencia de operaciones:

• obtener x
• obtener y
• crear una tupla (x, y)

jueves, 12 de mayo de 2022

Mónadas en Cats


Las mónadas son una de las abstracciones más comunes en Scala. Muchos programadores de Scala rápidamente se familiarizan intuitivamente con las mónadas, incluso si no las conocemos por su nombre. Informalmente, una mónada es cualquier cosa con un constructor y un método flatMap. Todos los funtores también son mónadas, incluidos Option, List y Future. Incluso tenemos una sintaxis especial para admitir mónadas: para comprensiones. Sin embargo, a pesar de la ubicuidad del concepto, la biblioteca estándar de Scala carece de un tipo concreto para abarcar "cosas que se pueden mapear planas". Esta clase de tipo es uno de los beneficios que nos brinda Cats. 

Pero que es una monada? Esta es la pregunta que se ha planteado en mil publicaciones de blog, con explicaciones y analogías que involucran conceptos tan diversos como gatos, comida mexicana, trajes espaciales llenos de desechos tóxicos y monoides en la categoría de endofuntores (lo que sea que eso signifique). Vamos a resolver el problema de explicar las mónadas de una vez por todas afirmando de manera muy simple: una mónada es un mecanismo para secuenciar cálculos. ¡Eso fue fácil! Problema resuelto, ¿verdad? Pero, dijimos que los funtores eran un mecanismo de control para exactamente lo mismo. Ok, tal vez necesitemos más discusión...

Los funtores nos permiten secuenciar cálculos ignorando alguna complicación. Sin embargo, los funtores están limitados porque solo permiten que esta complicación ocurra una vez al comienzo de la secuencia. No tienen en cuenta más complicaciones en cada paso de la secuencia.

Aquí es donde entran las mónadas. El método flatMap de una mónada nos permite especificar qué sucede a continuación, teniendo en cuenta una complicación intermedia. El método flatMap de Option tiene en cuenta las opciones intermedias. El método flatMap de List maneja listas intermedias. Y así. En cada caso, la función que se pasa a flatMap especifica la parte específica de la aplicación del cómputo, y flatMap se encarga de la complicación permitiéndonos flatMap nuevamente. Aterricemos las cosas mirando algunos ejemplos.

Option nos permite secuenciar cálculos que pueden o no devolver valores. Aquí hay unos ejemplos:

def parseInt(str: String): Option[Int] = scala.util.Try(str.toInt).toOption

def divide(a: Int, b: Int): Option[Int] = if(b == 0) None else Some(a / b)

Cada uno de estos métodos puede "fallar" al devolver None. El método flatMap nos permite ignorar esto cuando secuenciamos operaciones:

def stringDivideBy(aStr: String, bStr: String): Option[Int] =

    parseInt(aStr).flatMap { aNum =>

        parseInt(bStr).flatMap { bNum =>

            divide(aNum, bNum)

        }

   }

La semántica es:

  • la primera llamada a parseInt devuelve None o Some;
  • si devuelve un Some, el método flatMap llama a nuestra función y nos pasa el entero aNum;
  • la segunda llamada a parseInt devuelve None o Some;
  • si devuelve Some, el método flatMap llama a nuestra función y nos pasa bNum;
  • la llamada a dividir devuelve Ninguno o Algunos, que es nuestro resultado.

En cada paso, flatMap elige si llamar a nuestra función, y nuestra función genera el siguiente cálculo en la secuencia. 

El resultado del cálculo es una Option, que nos permite volver a llamar a flatMap y así continúa la secuencia. Esto da como resultado el comportamiento de manejo de errores rápido que conocemos y amamos, donde un None en cualquier paso da como resultado un None en general:

stringDivideBy("6", "2")
// res0: Option[Int] = Some(3)
stringDivideBy("6", "0")
// res1: Option[Int] = None
stringDivideBy("6", "foo")
// res2: Option[Int] = None
stringDivideBy("bar", "2")
// res3: Option[Int] = None

Cada mónada también es un funtor, por lo que podemos confiar tanto en flatMap como en map para secuenciar cálculos que introducen y no introducen una nueva mónada. Además, si tenemos tanto flatMap como map, podemos usar las comprensiones para aclarar el comportamiento de la secuencia:

def stringDivideBy(aStr: String, bStr: String): Option[Int] =
    for {
        aNum <- parseInt(aStr)
        bNum <- parseInt(bStr)
        ans <- divide(aNum, bNum)
    } yield ans


miércoles, 19 de octubre de 2016

Spring 5: Functional Web Framework


Leo un post en spring.io titulado: Spring 5: Functional Web Framework. Que de funcional funcional, no tiene mucho. Pero si es una forma declarativa de crear web gracias a las clausuras que vienen en java 8.

Puff, con la primera linea resumí todo.

Bueno, volvamos. Spring 5 viene con novedades entre las cuales esta un nuevo framework declarativo. En verdad me hace recordar a Sinatra. Pero como esta de moda lo funcional, llamemos funcional a cualquier cosa.

Por lo tanto, salvo el nombre que ya lo critique, el framework y el post están  muy interesante. Veamos un ejemplo (del framework)

 public interface PersonRepository {
  Mono<Person> getPerson(int id);
  Flux<Person> allPeople();
  Mono<Void> savePerson(Mono<Person> person);
}

RouterFunction<?> route = route(GET("/person/{id}"),
  request -> {
    Mono<Person> person = Mono.justOrEmpty(request.pathVariable("id"))
      .map(Integer::valueOf)
      .then(repository::getPerson);
    return Response.ok().body(fromPublisher(person, Person.class));
  })
  .and(route(GET("/person"),
    request -> {
      Flux<Person> people = repository.allPeople();
      return Response.ok().body(fromPublisher(people, Person.class));
    }))
  .and(route(POST("/person"),
    request -> {
      Mono<Person> person = request.body(toMono(Person.class));
      return Response.ok().build(repository.savePerson(person));
    }));

Como ven usa un estilo monads.

HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
ReactorHttpHandlerAdapter adapter =
  new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create("localhost", 8080);
server.startAndAwait(adapter);

Y listo!

Para mayor info: https://spring.io/blog/2016/09/22/new-in-spring-5-functional-web-framework


viernes, 19 de junio de 2015