Translate

lunes, 14 de marzo de 2022

Monoides y Semigrupos

La adición de Ints es una operación binaria cerrada, lo que significa que sumar dos Ints siempre produce otro Int:

2 + 1

// res0: Int = 3

También existe el elemento de identidad 0 con la propiedad de que a + 0 == 0 + a== a para cualquier Int a:

2 + 0
// res1: Int = 2
0 + 2
// res2: Int = 2

También hay otras propiedades de la adición. Por ejemplo, no importa en qué orden agreguemos elementos porque siempre obtenemos el mismo resultado. Esta es una propiedad conocida como asociatividad:

(1 + 2) + 3
// res3: Int = 6
1 + (2 + 3)
// res4: Int = 6

Las mismas propiedades para la suma también se aplican para la multiplicación, siempre que usemos 1 como identidad en lugar de 0:

1 * 3
// res5: Int = 3
3 * 1
// res6: Int = 3

La multiplicación, como la suma, es asociativa:

(1 * 2) * 3
// res7: Int = 6
1 * (2 * 3)
// res8: Int = 6

También podemos agregar Cadenas, usando la concatenación de cadenas como nuestro operador binario:

"One" ++ "two"
// res9: String = "Onetwo"

y la cadena vacía como la identidad:

"" ++ "Hello"
// res10: String = "Hello"
"Hello" ++ ""
// res11: String = "Hello"

Una vez más, la concatenación es asociativa:

("One" ++ "Two") ++ "Three"
// res12: String = "OneTwoThree"
"One" ++ ("Two" ++ "Three")
// res13: String = "OneTwoThree"

Tenga en cuenta que usamos ++ arriba en lugar del + más habitual para sugerir un paralelo con secuencias. Podemos hacer lo mismo con otros tipos de secuencias, usando la concatenación como operador binario y la secuencia vacía como nuestra identidad.

Formalmente, un monoide para un tipo A es:

• una operación o función que permite "combinar" dos elementos y retornar uno del mismo tipo, la firma sería : (A, A) => A
• un elemento vacío de tipo A

Esta definición se traduce muy bien en código Scala. Aquí hay una versión simplificada de la definición de Cats:

trait Monoid[A] {

   def combine(x: A, y: A): A

   def empty: A

}

Además de proporcionar las operaciones de combinar y vaciar, los monoides deben obedecer formalmente varias leyes. Para todos los valores x, y y z, en A, la combinación debe ser asociativa y el vacío debe ser un elemento de identidad:

def associativeLaw[A](x: A, y: A, z: A)

   (implicit m: Monoid[A]): Boolean = {

    m.combine(x, m.combine(y, z)) ==

    m.combine(m.combine(x, y), z)

}

def identityLaw[A](x: A)

   (implicit m: Monoid[A]): Boolean = {

   (m.combine(x, m.empty) == x) &&

   (m.combine(m.empty, x) == x)

}

La resta de enteros, por ejemplo, no es un monoide porque la resta no es asociativa:

(1 - 2) - 3
// res14: Int = -4

1 - (2 - 3)
// res15: Int = 2

En la práctica, solo necesitamos pensar en las leyes cuando escribimos nuestras propias instancias de Monoid. Las instancias ilegales son peligrosas porque pueden producir resultados impredecibles cuando se usan con el resto de la maquinaria de Cats. La mayoría de las veces podemos confiar en las instancias proporcionadas por Cats y asumir que los autores de la biblioteca saben lo que están haciendo.