Patrón Type Class: definición de leyes y test

En la entrada anterior, Patrón Type Class , realicé la definición, descripción y mostré ejemplos del patrón type class. La estructura del patrón está compuesta por un conjunto de elementos trait y un objeto que se comporta como dichos trait. A este objeto, para ciertos tipos de datos, se pueden definir leyes matemáticas para poder realizar test de dicho componente.

En la presente entrada, Patrón Type Class: definición de leyes y test , realizaré la definición del type class monoiede para la operación lógica suma y producto. Para ello, definiré un Type Class con la estructura definida en la anterior entrada añadiendo la definición de las leyes.

Definición de Monoide

En la serie que llevo publicado de la librería Scalaz, publiqué un post con título Scalaz IV: Tipos etiquetados, propiedad asociativa y monoides , en donde se describe el concepto de Monoide, así como, la descripción de unos ejemplos.

En la programación funcional, aparecen escenarios en donde es necesario definir funciones binarias cuyos parámetros de entrada y de salida son del mismo tipo; este tipo de función, se define en la entidad Semigrupos. Desde un punto de vista del lenguaje Scala, un Semigrupo se define de la siguiente forma:

trait Semigroup[A]{
  def combine(x:A, y:A):A
}

El Semigrupo lo definimos con un trait que recibe un tipo parametrizado A y define una función combine cuyos dos parámetros de entrada y la salida son del mismo tipo.

Unos ejemplo matemáticos que representan el Semigroup[A] pueden ser los siguientes:

  1.  1 + 2
  2.  2 + 1
  3. 1 + (2 + 3)
  4. (1 + 2) + 3
  5. (1 * 2) * 3
  6. 1 * (2 * 3)

Como deducimos de los ejemplos anteriores: podemos decir que se cumple la propiedad asociativa, comparando los resultados de los puntos de los ejemplo 1-2, 2-3 y 5-6. Así, podemos afirmar que Semigroup[A] cumple la propiedad asociativa; pero, en función de la operación que se aplique, puede no cumplir esta regla; por ejemplo, la operación resta, no cumple la propiedad asociativa. Unos ejemplos pueden ser los siguientes:

  1. 1 – (2 – 3)
  2. (1 – 2) – 3

De los ejemplos anteriores de la operación resta, el resultado de las operaciones es distinto en los ejemplos del punto 1 y 2.

Las operaciones matemáticas pueden cumplir otras propiedades; como por ejemplo, la propiedad de identidad. La propiedad de identidad necesita un valor vacío o valor zero el cual, en función de la operación, el valor del elemento no varía; por ejemplo: en la operación de suma, el valor zero o vacío es el valor 0; y, para la operación de multiplicación, el valor zero o vacío es el valor 1. Unos ejemplos con el entero 2 y aplicando el elemento vacío, cuyo resultado no cambia el entero, son los siguientes:

  1. Operación suma por la izquierda: 2 + 0 = 2
  2. Operación suma por la derecha: 0 + 2 = 2
  3. Operación multiplicación por la izquierda: 2 * 1 = 2
  4. Operación multiplicación por la derecha: 1 * 2 = 2

Llegado a este punto, los escenarios de conjuntos de elementos en donde se definen unas funciones que cumple las propiedades de asociatividad e identidad, son definidos como Monoides. La definición de Monoide en lenguaje Scala es la siguiente:

trait Monoid[A] extends Semigroup[A]{
  def empty: A
}

En los siguientes apartados, realizaré la descripción de monoides para las funciones lógicas suma (OR) y multiplicación (AND).

Monoide: operación lógica suma (OR)

La definición del Type class de la operación lógica suma (OR) en lenguaje Scala es la siguiente:

trait MonoidSuma[A] extends Monoid[A]{}
object MonoidSuma extends MonoidSumaInstances with MonoidSumaSyntax with MonoidSumaLaws
trait MonoidSumaInstances{
  def apply[A](implicit monoid: MonoidSuma[A]) = monoid
  implicit val monoidBooleanSuma = new MonoidSuma[Boolean] {
    // 1-FORMA
    // override def combine(x: Boolean, y: Boolean): Boolean = (x, y) match{
    // case (true, true ) => true
    // case (true, false) => true
    // case (false, true) => true
    // case (false, false) => false
    // }
    override def combine(x: Boolean, y: Boolean): Boolean = x || y
    override def empty: Boolean = false
  }
}
trait MonoidSumaSyntax{
  object syntax{
    def ++++[A](a:A, b:A)(implicit monoide: MonoidSuma[A]) = monoide.combine(a,b)
    def emptySuma [A](implicit monoide: MonoidSuma[A]) = monoide.empty
  }
}
trait MonoidSumaLaws{
  import MonoidSuma.syntax._
  trait Laws[A]{
    implicit val instance: MonoidSuma[A]
    def asociatividad(a1:A, a2:A, a3:A): Boolean = ++++( ++++(a1,a2), a3) == ++++(a1, ++++(a2,a3) )
    def izquierdaIdentidad(a1:A): Boolean = ++++( a1, emptySuma ) == a1
    def derechaIdentidad(a1:A): Boolean = ++++( emptySuma, a1 ) == a1
  }
  object Laws{
    def apply[A](implicit monoide:MonoidSuma[A]) = new Laws[A] {
      implicit val instance: MonoidSuma[A] = monoide // Dfinición de la referencia del trait.
    }
  }
}

La estructura del type class es la siguiente: trait MonoidSuma, objeto MonoidSuma, trait MonoidSumaInstances, trait MonoidSumaSyntax y MonoidSumaLaws. De la estructura de type class descrita en la entrada anterior, aparece el elemento nuevo trait MonoidSumaLaws en donde se define las funciones con las propiedades matemáticas de asociatividad y de identidad, así como, el objeto con el constructor del monoide.

Para realizar las pruebas del type class de la operación suma es necesario probar las leyes del type class y, para realizar las pruebas, se definen unos test que verifican las leyes matemáticas del type class los cuáles, para el type class función suma lógica, es el siguiente:

class MyMonoidTest extends FlatSpec with Matchers{
  "Test de las leyes del Monoide lógico Suma" should "cumple las leyes asociativas y de identidad" in {
    import es.ams.cap2monoidsemigroup.MonoidSuma.Laws
    val laws = Laws.apply
    assert( laws.asociatividad( true, false, true) == true)
    assert( laws.izquierdaIdentidad(true) == true )
    assert( laws.derechaIdentidad(true) == true )
  }
}

Monoide: operación lógica producto (AND)

La definición del Type class de la operación lógica producto es la siguiente:

trait MonoidProducto[A] extends Monoid[A]{}
object MonoidProducto extends MonoidProductoInstances with MonoidProductoSyntax with MonoidProductoLaws
trait MonoidProductoInstances{
  def apply[A](implicit monoid: MonoidProducto[A]) = monoid
  implicit val monoidProductoSuma = new MonoidProducto[Boolean] {
    // 1-FORMA
    // override def combine(x: Boolean, y: Boolean): Boolean = (x, y) match{
    // case (true, true ) => true
    // case (true, false) => false
    // case (false, true) => false
    // case (false, false) => false
    // }
    override def combine(x: Boolean, y: Boolean): Boolean = x && y
    override def empty: Boolean = true
  }
}
trait MonoidProductoSyntax{
  object syntax{
    def ****[A](a:A, b:A)(implicit monoide: MonoidProducto[A]) = monoide.combine(a,b)
    def emptyProducto [A](implicit monoide: MonoidProducto[A]) = monoide.empty
  }
}
trait MonoidProductoLaws{
  import MonoidProducto.syntax._
  trait Laws[A]{
    implicit val instance : MonoidProducto[A]
    def asociatividad(a1:A, a2:A, a3:A):Boolean = ****( ****(a1, a2), a3) == ****( a2, ****(a2, a3))
    def izquierdaIdentidad(a1:A):Boolean = ****(a1, emptyProducto) == a1
    def derechaIdentidad(a1:A):Boolean = ****(emptyProducto, a1) == a1
  }
  object Laws{
    def apply[A](implicit monoide:MonoidProducto[A]) = new Laws[A]{
      implicit val instance: MonoidProducto[A] = monoide
    }
  }
}

La estructura del type class es la siguiente: trait MonoidProducto, objeto MonoidProducto, trait MonoidProductoSyntax, trait MonoidProductoLaws y MonoidProductoLaws. De la estructura de type class descrita en la entrada anterior, aparece el elemento nuevo trait MonoidProductoLaws en donde se definen las funciones con las propiedades matemáticas de asociatividad y de identidad, así como, el objeto con el constructor del monoide.

Para realizar las pruebas del type class es necesario probar las leyes y, para realizar las pruebas, se define un test que verifican las leyes matemáticas del type class los cuáles, para el type class función producto lógica, es el siguiente:

class MyMonoidTest extends FlatSpec with Matchers{
  "Test de las leyes del Monoide lógico Producto" should "cumple las leyes asociativas y de identidad" in {
    import es.ams.cap2monoidsemigroup.MonoidProducto.Laws
    val laws = Laws.apply
    assert( laws.asociatividad( true, false, true) == true)
    assert( laws.izquierdaIdentidad(true) == true )
    assert( laws.derechaIdentidad(true) == true )
  }
}

La definición de las leyes se realiza utilizando las funciones definidas en la sintaxis y referenciando a los elementos implícitos de las instancias. ScalaTest es el framework seleccionado para la definición y ejecución de los test definidos de los type class.

Scalaz IV: Tipos etiquetados, propiedad asociativa y monoides

En la presente entrada, Scalaz IV: Tipos etiquetados, propiedad asociativa y monoides, me centrará en los tipos etiquetados y realizaré una breve introducción del concepto monoide para realizar operaciones con tipos etiquetados. Los tipos etiquetados hay que verlos como un wrapper de un tipo.

Tipos Etiquetados

En Scalaz existe el elemento Tag y su correspondiente alias, representado como “@@“. Las importaciones se realizan de la siguiente forma:

import scalaz.{@@, Tag}

Desde un punto conceptual, lo podemos definir de la siguiente forma:

type Tagged[U] = { type Tag = U }
type @@[T,U] = T with Tagged[U]

Supongamos que necesitamos definir el tipo Impuesto y, en función del caso de uso, el tipo impuesto puede estar definido como valor entero o real. La forma de abordar este escenario es utilizar un tipo etiquetado. Así, definimos el tipo Impuesto de la siguiente forma:

sealed trait Impuesto

Para definir el tipo del valor, definimos el tipo etiquetado Impuesto de la siguiente forma:

def Impuesto[A](a:A): A @@ Impuesto = Tag[A, Impuesto](a)

El ejemplo anterior, define un Impuesto cuyo valor es de tipo A. Un ejemplo de creación de un impuesto del 80 por ciento lo podemos realizar de la siguiente forma:

 val impuesto1 = Impuesto(0.8)
 println(s"impuesto1=${impuesto1}")

La salida por consola es la siguiente:

 impuesto1=0.8

Este tipo es utilizado en combinación con otros tipos; por ejemplo, para el cálculo de un sueldo. Así, podemos definir el tipo etiquetado Sueldo de la siguiente forma:

 sealed trait Sueldo
 def Sueldo[A](a:A): A @@ Sueldo = Tag[A, Sueldo](a)

Si tenemos un impuesto y un sueldo de 10000, la operación de cálculo del sueldo neto a partir del impuesto es el siguiente:

 def calculoSueldoNeto(m: Double @@ Impuesto): Double @@ Sueldo = Sueldo( 10000 * Tag.unwrap(m))
 val sueldoNeto = calculoSueldoNeto( Impuesto(0.8) )
 println(s"sueldoNeto=${sueldoNeto}")

La salida por consola es la siguiente:

 sueldoNeto=8000.0

Propiedad asociativa

Según Wikipedia definimos la propiedad asociativa de la siguiente manera:

La asociatividad es una propiedad en el álgebra y la lógica proposicional que se cumple si, dados tres o más elementos cualquiera de un conjunto determinado, se verifica que existe una operación: op , que cumpla la igualdad:

A op (B op C) = (A op B) op C

Desde un punto de vista del programador en Scala, unos ejemplos de la propiedad asociativa son los siguientes:

  • Ejemplo de operaciones con enteros
(3 * 2) * ( 8 * 5) assert_=== 3 * ( 2 * ( 8 * 5))
  • Ejemplo con colecciones de String definidos en listas.
List("pr") ++ ( List("u") ++ List("eba")) assert_=== ( List("pr") ++ List("u") ) ++ List("eba")

En Scalaz, existe la posibilidad de aplicar la asociatividad utilizando la type classes SemigroupOps la cual tiene la siguiente definición:

 trait Semigroup[A] { self =>
   def append(a1: A, a2: => A): A
   [...]
 }
 
 trait SemigroupOps[A] extends Ops[A] {
  final def |+|(other: => A): A = A.append(self, other)
  final def mappend(other: => A): A = A.append(self, other)
  final def ⊹(other: => A): A = A.append(self, other)
 }

La importación de dichas funciones se realiza de la siguiente forma: import scalaz.Scalaz._

Sean un conjunto de elementos definidos por lista de enteros y cadenas. Se aplican la propiedad asociativa con las funciones del interfaz SemigroupOps de la siguiente manera:

Ejemplos con función mappend

 val result1 = List(1, 2, 3) mappend List(4, 5, 6)
 println(s"Resultado1 =${result1}")
 println
 val result2 = "uno" mappend "dos"
 println(s"Resultado2 =${result2}")
 println

La salida por consola es la siguiente:

 Resultado1 =List(1, 2, 3, 4, 5, 6)
 Resultado2 =unodos

Ejemplos con función |+|

 val result3 = List(1, 2, 3) |+| List(4, 5, 6)
 println(s"Resultado3 =${result3}")
 println
 val result4 = "uno" |+| "dos"
 println(s"Resultado4 =${result4}")
 println

La salida por consola es la siguiente:

 Resultado3 =List(1, 2, 3, 4, 5, 6)
 Resultado4 =unodos

Ejemplos con función ⊹

 val result5 = "uno" ⊹ "dos"
 println(s"Resultado5 =${result5}")
 println

La salida por consola es la siguiente:

 Resultado5 =unodos

Monoide

Un monoide es una abstracción de una función de orden superior HOF. Un monoide está compuesta por una función binaria que cumple la propiedad asociativa y una función binaria de identidad. En esta entrada, me centraré en la función binaria de identidad y su relación con los tipos etiquetados.

Una función binaria de identidad es aquella función que en una definición recursiva, corresponde con el caso base. En Scalaz, el paso base se define con ‘zero’. La definición es la siguiente:

 trait Monoid[A] extends Semigroup[A] { self =>
 ////
 /** The identity element for `append`. */
 def zero: A
 ...
 }

Unos ejemplos son los siguientes:

  • Monoide de una lista de enteros:
 val result1 = Monoid[List[Int]].zero
 println(s"Monoid[List[Int]].zero=${result1}")

La salida por consola es la siguiente:

 Monoid[List[Int]].zero=List()
  • Monoide de un String:
 val result2 = Monoid[String].zero
 println(s"Monoid[List[Int]].zero='${result2}'")

La salida por consola es la siguiente:

 Monoid[List[Int]].zero=''
  • Monoide de enteros:
 val result3 = Monoid[Int].zero
 println(s"Monoid[Int].zero='${result3}'")

La salida por consola es la siguiente:

 Monoid[Int].zero='0'
  • Monoide de un entero y la aplicación de la propiedad asociativa.
 val result4 = 10 |+| Monoid[Int].zero
 println(s"10 |+| Monoid[Int].zero='${result4}'")

La salida por consola es la siguiente:

 10 |+| Monoid[Int].zero='10'

Tags.Disjunction

El tipo etiquetado Disjunction corresponde al tipo lógico que define el operador OR. El caso base, o bien, caso zero en Scalaz corresponde con el valor FALSE.

Unos ejemplos del Tags.Disjunction son los siguientes:

 println(s"Tags.Disjunction(true)=${Tags.Disjunction(true)}")
 println(s"Tags.Disjunction(false)=${Tags.Disjunction(false)}")
 println(s"Tags.Disjunction(true) |+| Tags.Disjunction(true)=${Tags.Disjunction(true) |+| Tags.Disjunction(true)}")
 println(s"Tags.Disjunction(true) |+| Tags.Disjunction(false)=${Tags.Disjunction(true) |+| Tags.Disjunction(false)}")
 println(s"Tags.Disjunction(false) |+| Tags.Disjunction(true)=${Tags.Disjunction(false) |+| Tags.Disjunction(true)}")
 println(s"Tags.Disjunction(false) |+| Tags.Disjunction(false)=${Tags.Disjunction(false) |+| Tags.Disjunction(false)}")
 println(s"Monoid[Boolean @@ Tags.Disjunction].zero ${Monoid[Boolean @@ Tags.Disjunction].zero}")
 println(s"Monoid[Boolean @@ Tags.Disjunction].zero |+| Tags.Disjunction(true)=${Monoid[Boolean @@ Tags.Disjunction].zero |+| Tags.Disjunction(true)}")

La salida por consola es la siguiente:

 Tags.Disjunction(true)=true
 Tags.Disjunction(false)=false
 Tags.Disjunction(true) |+| Tags.Disjunction(true)=true
 Tags.Disjunction(true) |+| Tags.Disjunction(false)=true
 Tags.Disjunction(false) |+| Tags.Disjunction(true)=true
 Tags.Disjunction(false) |+| Tags.Disjunction(false)=false
 Monoid[Boolean @@ Tags.Disjunction].zero false
 Monoid[Boolean @@ Tags.Disjunction].zero |+| Tags.Disjunction(true)=true

Tags.Conjunction

El tipo etiquetado Conjunction corresponde con el tipo lógico que define el operador AND. El caso base, o bien, caso zero en Scalaz corresponde con el calor TRUE.

Unos ejemplos del Tags.Conjunction son los siguientes:

 println(s"Tags.Conjunction(true)=${Tags.Conjunction(true)}")
 println(s"Tags.Conjunction(false)=${Tags.Conjunction(false)}")
 println(s"Tags.Conjunction(true) |+| Tags.Conjunction(true)=${Tags.Conjunction(true) |+| Tags.Conjunction(true)}")
 println(s"Tags.Conjunction(true) |+| Tags.Conjunction(false)=${Tags.Conjunction(true) |+| Tags.Conjunction(false)}")
 println(s"Tags.Conjunction(false) |+| Tags.Conjunction(true)=${Tags.Conjunction(false) |+| Tags.Conjunction(true)}")
 println(s"Tags.Conjunction(false) |+| Tags.Conjunction(false)=${Tags.Conjunction(false) |+| Tags.Conjunction(false)}")
 println(s"Monoid[Boolean @@ Tags.Conjunction].zero=${Monoid[Boolean @@ Tags.Conjunction].zero}")
 println(s"Monoid[Boolean @@ Tags.Conjunction].zero |+| Tags.Conjunction(true)=${Monoid[Boolean @@ Tags.Conjunction].zero |+| Tags.Conjunction(true)}")

La salida por consola es la siguiente:

 Tags.Conjunction(true)=true
 Tags.Conjunction(false)=false
 Tags.Conjunction(true) |+| Tags.Conjunction(true)=true
 Tags.Conjunction(true) |+| Tags.Conjunction(false)=false
 Tags.Conjunction(false) |+| Tags.Conjunction(true)=false
 Tags.Conjunction(false) |+| Tags.Conjunction(false)=false
 Monoid[Boolean @@ Tags.Conjunction].zero=true
 Monoid[Boolean @@ Tags.Conjunction].zero |+| Tags.Conjunction(true)=true

Para el lector interesado, las entradas que he realizado sobre Scalaz son las siguientes: