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.

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s