Scalaz II: type class básicos

En la preente entrada, Scalaz II: Básico, me centraré en las types classes de Scalaz con un funcionamiento básico; como son las type classes: Equal, Show y Enum.

Scalaz Equal

La type classes Equal define funciones para realizar comparaciones. Para utilizar el API Equal, hay que importar el API y la sintaxis de Scalaz de la siguiente forma:

 import scalaz.Equal
 import scalaz.Scalaz._

Tipos básicos

Unos ejemplos de comparaciones con tipos básicos son los siguientes:

 println(s"1 === 1=>${1 === 1}")
 println(s"1 === 'algo'=>${ 1 == "algo" }")
 println(s"1.some =/= 2.some=>${1.some =/= 2.some}")
 println(s"1 assert_=== 1=>${1 assert_=== 1}")

La salida por consola del snippet anterior es el siguiente:

1 === 1=>true
1 === 'algo'=>false
1.some =/= 2.some=>true
1 assert_=== 1=>()

Ejemplos con case class

Sea una case class para definir una entidad de negocio cualquiera y unas instancias de dicha case class definidas de la siguiente forma:

 case class LuzDeTrafico(nobre: String)
 val rojo = LuzDeTrafico("rojo")
 val amarillo = LuzDeTrafico("amarillo")
 val verde = LuzDeTrafico("verde")

Sea una referencia implícita que define una operación con el API Equal con la case class anterior:

implicit val TrafficLightEqual: Equal[LuzDeTrafico] = Equal.equal(_ == _)

Se puede definir operaciones de comparación de la siguiente forma:

 println(s"rojo === rojo?${rojo === rojo}")
 println(s"rojo === amarillo?${rojo === amarillo}")

La salida por consola del snippet anterior es el siguiente:

 rojo === rojo?true
 rojo === amarillo?false

Scalaz Show

La type classes Show permite definir estructuras de datos como String. Los siguientes ejemplos muestran las funciones del API Show:

 println(s"3.show=>${3.show}")
 println(s"3.shows=>${3.shows}")
 "Saludos a la consola".println
 println(s"Saludos a la consola con show=>${ "Saludos a la consola con show".show}")

La salida por consola del snippet anterior es el siguiente:

 3.show=>3
 3.shows=>3
 "Saludos a la consola"
 Saludos a la consola con show=>"Saludos a la consola con show"

Scalaz Enum

La type class Enum permite definir estructuras secuenciales ordenadas. Las estructuras que podemos definir son las siguientes: lista de enteros, Streams de enteros y lista de caracteres. Unos snippet de ejemplo son los siguientes:

  • Definición de un rango desde los caracteres ‘a’ hasta ‘e’
 val enum1 = 'a' to 'e'
 println(s"'a' to 'e'=>${enum1}")
 println("Impresión del rango de enum1")
 enum1.foreach(println(_))
 println

La salida por la consola es la siguiente:

 'a' to 'e'=>NumericRange a to e
 Impresión del rango de enum1
 a
 b
 c
 d
 e
  • Definición de una lista con los elementos desde el caracter ‘a’ hasta ‘e’ y una lista de enteros desde 1 hasta 20.
 val enum2 = 'a' |-> 'e'
 println(s"enum2=>${enum2}")
 val enum2_1 = 1 |-> 20
 println(s"enum2_1=>${enum2_1}")

La salida por la consola es la siguiente:

 enum2=>List(a, b, c, d, e)
 enum2_1=>List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
  • Definición de un Stream de enteros con los elementos comprendidos desde el valor 3 hasta 5.
 val enum3 = 3 |=> 5
 println(s"emun3=>${enum3}")
 println(s"emun3=>${enum3.headOption}")

La salida por la consola es la siguiente:

 emun3=>scalaz.EphemeralStream$$anon$5@37e547da
 emun3=>Some(3)

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

Scalaz I: Functores y funciones como functores

Scalaz es una librería de programación funcional para el lenguaje Scala. Scalaz proporciona un conjunto de estructuras de datos para completar la biblioteca Scala; todas estas estructuras, están definidas sobre type classes y sus correspondientes instancias.

En la presente entrada, Scalaz I: Functores y funciones como functores, me centraré en los functores y en definir funciones como functores mediante los elementos de la librería Scalaz.

Definición de función curring

Las funciones curring es aquella técnica funcional nombrada en honor al matemática Haskell Curry el cual fue el creador del lenguaje funcional Haskell.

Una función curry es aquella función que define un conjunto de parámetros y, en las invocaciones a dicha función, pueden emplearse un número menor de parámetros. Así, podemos definir un conjunto de funciones a partir de una función base. Un ejemplo de función curry es la siguiente:

 def funcionBase(x:Int)(y:Int)= x + y
 val funcionCurry1 = funcionBase(2)(_)
 println(s"funcionCurry1(3)=${funcionCurry1(3)}")

La función base con técnica curring es la función funcionBase la cual define una lista de parámetros separados con paréntesis. A partir de la función base, definimos la función funcionCurry1 cuyo primer parámetro tiene el valor fijo 2 y, el segundo parámetro, un valor a determinar. Una vez definida la funcionCurry, se realiza la invocación de la función con el valor variable.

La salida por consola del ejemplo anterior es la siguiente:

 funcionCurry1(3)=5

La función funcionCurry1 tiene como primer parámetro el valor enteror 2 y, si quisiéramos, podríamos definir tantas funciones como valores enteros como primer parámetro.

Definición de Functor

La teoría de categorías es aquel estudio de las matemáticas que trata mediante axiomas estructuras abstractas como si fueran una, utilizando objetos y morfismos. Los objetos y los morfirmos forman una categoría. Así, una categoría es un conjunto de objetos y unos morfismos los cuales son las transformaciones entre dichos objetos.

Dada una categoría A, un functor es aquel morfismo que transforma la categoría A en otro categoría B. A nivel práctico, desde un punto de vista de un desarrollador, un functor se puede ver como aquella operación que transforma un elemento A en otro B y, a un bajo nivel en el lenguaje Scala, se corresponde con la función map.

Funciones como curry

En el apartado anterior, Definición de función curring, he definido una función de tipo curry y, en el presente apartado, describiré cómo se transforma una función como tipo curry. Para transformar una función como curry hay que aplicar la función curried.

En el siguiente ejemplo, se define una función curry con nombre lista1; se define una lista de enteros a la cual se aplica una función para realizar la multiplicación de los elementos por un número; y, una vez definida, se realiza la invocación de la función por el operador a multiplicar. Como se observa en la segunda línea del siguiente ejemplo, se aplica la función map con un valor entero fijo correspondiente con el segundo parámetro de la función de lista1.

 val lista1 = List(1, 2, 3, 4) map { (_: Int) * (_: Int) }.curried
 val resultadoLista1 = lista1.map {_ (9)}
 println(s"Resultado1=${resultadoLista1}")
 println

La salida por consola del ejemplo anterior es la siguiente:

 Resultado1=List(9, 18, 27, 36)

Funciones como functores

En el apartado anterior con nombre Definición de Functor, he realiza una descripción a alto nivel de un functor. En el presente apartado, describiré cómo definir functores con funciones.

Las importaciones de los elementos necesarios de Scalaz para los ejemplos siguientes, son los siguientes:

 import scala.language.higherKinds
 import scalaz.std.list._
 import scalaz.std.option._
 import scalaz.syntax.functor._
 import scalaz.{Functor, Monad}

La definición de un Functor con Scalaz de una lista de enteros y un Option de enteros es el siguiente:

 val list: List[Int] = Functor[List].map(List(1, 2, 3))(_ * 2)
 val option: Option[String] = Functor[Option].map(Some(123))(_.toString)
 println("List(1, 2, 3), [_ * 2] =>" + list)
 println("Some(123), _.toString =>" + option)

En el ejemplo anterior, defino un Functor de un tipo List y un segundo Functor de tipo Option. A estos dos functores, se aplican la función map para realizar la transformación, mediante una función, de una lista y de un Option. La función map del elemento scalaz.Functor tiene como parámetros los datos y la función que los modifica.

La salida por consola del ejemplo es la siguiente:

 List(1, 2, 3), [_ * 2] =>List(2, 4, 6)
 Some(123), _.toString =>Some(123)

En muchos casos, es necesario realizar la función de un tipo genérico sin conocer los datos; para ello, utilizamos la función lift. Así, como ejemplo, podemos definir un Functor para un Option de la siguiente manera:

 def lifted: (Option[Int]) => Option[Int] = Functor[Option].lift((x: Int) => x + 1)
 println("0.- lift((x: Int) => x + 1; Some(54) =>" + lifted(Some(54)))

La salida por consola es la siguiente:

 0.- lift((x: Int) => x + 1; Some(54) =>Some(55)

Si deseamos realizar alguna operación sobre el resultado, se puede aplicar la función map como sigue:

 println("0.1.- lift((x: Int) => x + 1; Some(54) =>" + lifted(Some(54)).map((x: Int) => x + 5))

La salida por consola es la siguiente:

 0.1.- lift((x: Int) => x + 1; Some(54) =>Some(60)

Los ejemplos descritos hasta el momento han partido de la definición de un elemento Functor; pero, si deseamos definir una función y utilizarla como un Functor o una Mónada, ¿cómo se realiza? Para ello, es necesario definir la función y emplear la función lift. Para el caso de una Mónada, un ejemplo es el siguiente:

 val func = ((x: Int) => x + 1) lift Monad[Option]
 println("1 ->" + func(Some(87)))

La salida por consola es la siguiente:

 1 ->Some(88)

Si deseamos aplicar mas transformaciones con la función map de la Mónada, se realiza de la siguiente forma:

 println("1.1 ->" + func(Some(87)).map((x: Int) => x + 1))
 println("1.2 ->" + func(Some(87)).map((x: Int) => x + 1).map((x: Int) => x + 10))

La salida por consola es la siguiente:

 1.1 ->Some(89)
 1.2 ->Some(99)

Si deseamos definir la transformación como una función, se puede realizar de la siguiente forma:

 def func2(elem: Int) = func(Some(elem)).map((x: Int) => x + 1).map((x: Int) => x + 10)
 println("1.3 ->" + func2(87))

La salida por consola es la siguiente:

 1.3 ->Some(99)

La definición de una función para realizar un morfismo de una lista de enteros en otra lista de enteros, se puede definir de la siguiente manera.

 def liftedListInt: (List[Int] => List[Int]) = Functor[List].lift((x: Int) => x * 5)
 println("2 ->" + liftedListInt(List(1, 2, 3)))
 println("2.1 ->" + liftedListInt(List(1, 2, 3)).map((x: Int) => x * 5))

La salida por consola es la siguiente:

 2 ->List(5, 10, 15)
 2.1 ->List(25, 50, 75)

Otras funciones

Scalaz es un conjunto de componentes de type classes y, el patrón type classes, permite la definición de sintaxis. En el presente apartado, se identifican un conjunto de funciones pertenecientes a la sintaxis de functores. La importación de los elementos necesarios para los ejemplos son las siguientes:

 import scalaz.Functor
 import scalaz.Scalaz._

La selección de las funciones que he realizado son las siguientes:

  • Para convertir todos los elementos de una estructura a un elemento determinado, se emplea la función >|. Un ejemplo es el siguiente:
 val listaX = List(1,2,3) >| "x"
 println(s"List(1,2,3) >| 'x'=${listaX}")

La salida por consola es la siguiente:

 List(1,2,3) >| 'x'=List(x, x, x)
  • Para convertir todos los elementos de una estructura a un elemento determinado, se emplea la función as. Un ejemplo es el siguiente:
 val listaAs = List(1,2,3) as "x"
 println(s"List(1,2,3) as 'x'=${listaAs}")

La salida por consola es la siguiente:

 List(1,2,3) as 'x'=List(x, x, x)
  • Para convertir un conjunto de entrada en un conjunto de tuplas con elementos duplicados, se emplea la función fpair. Un ejemplo es el siguiente:
 val listaFPair = List(1,2,3).fpair
 println(s"List(1,2,3).fpair=${listaFPair}")

La salida por consola es la siguiente:

 List(1,2,3).fpair=List((1,1), (2,2), (3,3))
  • Para convertir un conjunto de entrada en un conjunto de duplas cuyo elemento derecho sea uno determinada, se emplea la función strengthR. Un ejemplo es el siguiente:
 val listaStrengthR = List(1,2,3).strengthR("x")
 println(s"List(1,2,3).strengthR('x')=${listaStrengthR}")

La salida por consola es la siguiente:

 List(1,2,3).strengthR('x')=List((1,x), (2,x), (3,x))
  • Para convertir un conjunto de entrada en un conjunto de duplas cuyo elemento izquierdo sea uno determinada, se emplea la función strengthL. Un ejemplo es el siguiente:
 val listaStrengthL = List(1,2,3).strengthL("x")
 println(s"List(1,2,3).strengthL('x')=${listaStrengthL}")

La salida por consola es la siguiente:

 List(1,2,3).strengthL('x')=List((x,1), (x,2), (x,3))
  • Para convertir un conjunto de entrada en un conjunto de tuplas vacías, se emplea la función void. Un ejemplo es el siguiente:
 val listaVoid = List(1,2,3).void
 println(s"List(1,2,3).void=${listaVoid}")

La salida por consola es la siguiente:

 List(1,2,3).void=List((), (), ())

La definición de un Functor o una Mónada mediante el patrón funcional type classes es una operación que requiere, para el desarrollador principiante en la programación funcional, un entendimiento de conceptos complejos. Mediante la utilización de Scalaz utilizar Functores y definir funciones como functores es un proceso comprensible y fácil de entender.

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

Notas de programación funcional

En la entrada de hoy, “Notas de programación funcional”, describiré ciertos conceptos generales de la programación funcional. Serán pequeñas píldoras documentales, o bien, mis notas sobre los conceptos iniciales en la programación funcional. No pretendo realizar definiciones formales, ni que la entrada sea formal; solo pretendo crear pequeñas notas las cuáles sirvan para allanar los primeros pasos en el estudio de la programación funcional.

El lenguaje Scala es un lenguaje híbrido con en el que se aplican dos paradigmas: el paradigma funcional y el paradigma orientado a objetos. El creador del lenguaje es Martin Odersky en el 2004. El lenguaje Scala está basado en el lenguaje Haskell y el lenguaje Erlang, adquiriendo de estos dos lenguajes, lo mejor de ellos. Del lenguaje Haskell, los principios de la programación funcional y, del lenguaje Erlang, el modelo de actores para la programación concurrente.

Funciones

En Scala hay diferentes tipos de funciones: funciones totales, funciones parciales, first class function,…No voy a realizar una descripción total de todas ellas, pero me centraré en el concepto de función genérico.

Definimos una función con la siguiente estructura:

 def nombreFunción([lista de parámetros]): [Tipo de retorno] = {
   Cuerpo de la función
   return [expresiónRetorno]
 }

Un ejemplo de función es la siguiente:

 def suma( operador1:Int, operador2:Int ): Int = {
   var suma: Int = 0
   suma = operador1 + operador2
   return suma
 }

En Scala, tenemos la posibilidad de definir funciones como variables, sin la necesidad de definir una función de forma específica; este caso, se conoce como first class function. Un ejemplo de first class function es el siguiente:

 val suma: (Int,Int) => Int = (a:Int, b:Int) => a + b
 println(s"suma(2,3)=${suma(2,3)}")
 println

El compilador Scala, realiza la transformación de la función suma como una clase de tipo Function, permitiendo la posibilidad de definir este tipo de variables función de forma sencilla.

Las funciones parciales son aquellas funciones en donde se define parte de la función. En el siguiente ejemplo, defino una función parcial suma en donde se define el primer operando y, el segundo operando, es pasado por parámetro.

val suma2: PartialFunction[Int, Int] = {
 case d:Int => 2 + d
 }
 println(s"suma2(2+3)=${suma2(3)}")
 println

Recursividad

La recursividad es aquella forma en la cual una función se define basada en su propia definición. Una función recursiva se define en función de dos pasos: el caso base, solución a la instancia mas sencilla; y, el caso de inducción, solución a los casos complejos.

Un ejemplo típico de recursividad es la función factorial, la cual se define en lenguaje Scala como sigue:

 def factorial(n: Int): Int = {
   if (n > 1)
     n * factorial(n-1)
   else
     1
   }
 println(s"factorial(3)=${factorial(3)}")
 println

Las funciones recursivas son utilizas en estructuras de datos recursivas, como por ejemplo, una lista o un árbol.

Existe dos tipos de recursividad: la recursividad por la cabecera y la recursividad por la cola.

La recursividad por la cabecera es aquella recursividad que se realiza aplicando el cálculo de la operación con la cabecera de la estructura y, una vez realizada, se realiza la llamada recursiva con la cola. Con esta recursividad, para el caso de una lista, se realiza el recorrido desde el inicio de la estructura (cabecera) hasta el final de la lista.

La recursividad por la cola es aquella recursividad que se realiza aplicando las llamadas recursivas a la función y, una vez llamadas, se realiza el cálculo; es decir, recorres la lista con las llamadas y, en el retorno de la recursividad, aplicas el cálculo. Con esta recursividad, para el caso de una lista, se realiza el recorrido desde el final de la estructura hasta el inicio.

ADT

Scala es un lenguaje tipado, como por ejemplo: tipo entero, Int; tipo alfanumérico, String; tipo lógico, Boolean;… Con la combinación de estos tipos, podemos definir tipos más complejos, mediante las operaciones suma y producto.

Definimos un ADT (Algebraic Data Type), tipo de datos algebraico, como aquel tipo conformado por tipos simples los cuales, mediante las operaciones de suma y producto, formamos tipos más complejos. La operación suma es aquella operación que corresponde con la herencia y, la operación producto, es aquella operación que corresponde con las definiciones de los parámetros de las case class.

Un ejemplo de un ADT que define un tipo Lista con tipos enteros, es el siguiente:

sealed trait MiLista
case class Null extends MiLista
case class Nodo(cabeza:Int, cola:MiLista)

De la definición anterior, podemos decir que tenemos la definición de una estructura de tipo lista de enteros de una forma matemática clara y sencilla; pero, ¿y si queremos definir una lista de otro tipo? La primera solución, es definir otro ADT para el tipo seleccionado; y, la segunda solución, consiste en definir un ADT con la capacidad de soportar polimosfirmo paramétrico. Un ejemplo de una lista con polimorfísmo paramétrico es el siguiente:

sealed trait MiLista[T]
case class Null[T]() extends MiLista[T]
case class Nodo[T](cabeza:[T], cola:MiLista[T])

Con la definición anterior, podemos definir una lista de cualquier tipo.

Funciones de Orden Superior (HOF – Hight Order Function)

Las funciones de orden superior son aquellas funciones que permiten definir como parámetro otra función, es decir, un parámetro de la función puede ser una función. Un ejemplo básico y sencillo de función HOF, puede ser el siguiente:

 def ejemploHOF(mensaje:String, f: (Int,Int) => Int, operador1:Int, operador2:Int ): Unit = {
   println(mensaje + "=" + f(operador1, operador2))
 }
 ejemploHOF( "Resultado de la suma(2,3)", suma, 2, 3 )

La salida por consola del ejemplo es el siguiente:

Resultado de la suma(2,3)=5

El ejemplo parece sencillo pero a partir de este concepto se construyen muchos patrones de la programación funcional que no son tan sencillos.

Catamorfismo

En los apartados anteriores describí conceptos como los ADT’s y funciones de orden superior; pero, llegado a este punto, hay que preguntarse: ¿cómo trabajo con los ADT’s?, ¿cómo realizo un cálculo sobre un ADT?, ¿con qué mecanismos los puedo manipular? La respuesta es sencilla, trabajamos los ADT’s con los catamorfismos.

Definimos catamorfismo como aquella formar de interpretar, manipular o consumir un ADT. Se puede definir con cualquier ADT y, a nivel práctico, se corresponde con la función fold.

El ADT es aquel tipo definido desde un punto de vista matemático y, la programación funcional, tiene un aspecto matemático; con lo cual, la forma de pensar debe de ser matemática.

Sea el ADT que define la estructura Lista del apartado anterior definido de la siguiente forma:

sealed trait MiLista[T] // Caso abstracto
case class Null[T]() extends MiLista[T] // Caso Base
case class Nodo[T](cabeza:[T], cola:MiLista[T]) extends MiLista[T]// Caso de Inducción

A nivel conceptual, una lista se puede representar con la anterior definición de la siguiente forma:

Lista(1,2,3) => val lista: MiLista[Int] = Nodo(1, Nodo(2, Nodo(3, Null) ) )

Dado el siguiente problema a resolver: cálculo de la suma de los elementos de una lista, es decir: dada un elemento de tipo MiLista[Int], se desea definir una función que calcule la suma de los elementos y retorne un número entero.

La definición del ADT está formada por tres elementos: caso base, caso de inducción y caso abstracto. Con estos tres casos definimos el catamorfismo de la siguiente forma:

  • Caso Base. Dado un elemento de tipo Null[Int], ¿qué tengo que realizar para retornar como resultado un entero(B)?.
Null[Int] ---(B)---> Int; Resultado: B=0

Dado el caso base, tenemos que retornar el valor 0. Para toda lista vacía, la suma de los elementos de una lista es cero.

  •  Caso de inducción. Dado un elemento de tipo Nodo[Int], ¿qué tengo que realizar para calcular la suma de sus elementos(B)?
Node[Int] -----(A:Int, B:MiLista[Int]) => B(Int)---> Int; Resultado: (A, B) => B

Un Node está formado por una cabeza(parte A) y un cola de tipo MiLista(parte B). La suma de la parte A y de la parte B tiene que tener como resultado un número B entero, es decir, todo elemento A mas B (resultado de la cola) tiene que tener como resultado B

  • Caso abstracto. El caso abstracto lo forma la unión del caso base y el caso de inducción; es decir, la operación suma y la operación producto. Así, tenemos:
    • caso base: Null[Int] => B
    • caso inducción: Nodo[T](cabeza:[T], cola:MiLista[T]); (A, B) => B
    • caso abstracto: caso base más caso inducción: Null[Int] U Nodo[T](cabeza:[T], cola:MiLista[T]) => (B, (A,B)=> B)

Así, la función fold queda definida de la siguiente forma:

 def fold[A,B](lista:MiLista[A])(base:B, f: (A, B) => B): B = lista match{
   case Null() => base
   case Nodo(h,t) => f(h, fold(t)(base,f))
 }

Fold, FoldRight

La función fold es idéntica a la función foldRight. Estas funciones están enfocadas a una recursividad de cabacera.

Así, la función foldRight queda definida como sigue:

 def foldRight[A, B](lista: MiLista[A])(zero: B,f: (A, B) => B): B = lista match {
   case Nil => zero
   case head::tail => f(head, foldRight(tail)(zero,f))
 }

Desde un punto de vista de las llamadas a realizar y qué elementos intervienen en cada una de las llamadas, se puede ver como sigue:

-> 1 + foldRight(–)
-> 1 + (2 + foldRight(–) )
-> 1 + (2 + (3 + foldRight(–) ) )
-> 1 + (2 + (3 + ( 4 + foldRight(–) ) ) )

Por cada llamada, se van insertando una entrada en la pila del sistema. Cuando se termina el espacio, la ejecución falla. Un ejemplo para llegar a esta situación es el siguiente:

 val listaEnteros = (1 to 325000).toList
 println( foldRight(listaEnteros)(0, (a:Int, b:Int) => a + b) )

La salida por consola es la siguiente:

Exception in thread "main" java.lang.StackOverflowError

FoldLeft

La función foldLeft tiene un parecido con las funciones fold y foldRight, la diferencia, reside en el orden de los parámetros de la función pasada por parámetro. El tipo de recursividad es por la cola, es decir, se realiza la llamada recursiva y, una vez llamada, se realiza el cálculo. La función foldLeft se define como sigue:

@annotation.tailrec
 def foldLeft[A,B](l:List[A])(zero:B, f:(B,A)=>B): B = l match {
 case Nil => zero
 case head::tail => foldLeft(tail)( f(zero,head), f )
 }

Este tipo de recursión en Scala es la más eficiente porque se llama a la función con el valor acumulado.

Para demostrar la eficiencia de esta función, realizamos el cálculo de la suma de la lista de ejemplo de foldRight.

 val listaEnteros = (1 to 325000).toList
 println( foldLeft(listaEnteros)(0, (a:Int, b:Int) => a + b) )

La salida por consola es la siguiente:

1273054948

Como he comentado al inicio de la entrada, no he querido ser formal, simplemente he querido mostrar pequeñas píldoras documentales para que, a la persona iniciada en la programación funcional, le permita comprender las piedras iniciales. Desde mi experiencia, pasadas estas primeras piedras, el proceso de comprensión del resto de conceptos es más sencillo.

Cliente Mosquitto: Paho-mqtt

En la anterior entrada con título “Mosquitto”, realicé la descripción del broker Mosquitto, explicando las tareas de instalación, estructuras de carpetas y uso del Broker desde la consola del sistema. En esta entrada, “Cliente Mosquitto: Paho-mqtt”, me centraré en cómo los dispositivos se pueden conectar al broker Mosquitto utilizando la librería Paho-mqtt.

Paho-mqtt es aquella librería cliente en lenguaje Python la cual implementa la versión 3.1 y 3.1.1 del protocolo MQTT.

Creación del proyecto

Para la realización de los ejemplos con el cliente Paho-mqtt, reallizaré la creación de un proyecto Python con el cliente Paho-mqtt. El proceso de creación consistirá en lo siguiente:

  1.  Creación de un proyecto Python en el IDE.
  2. Desde la carpeta del proyecto y en la consola del sistema, creación del entorno virtual del proyecto ejecutando el siguiente comando:
     python3 -m venv venv
  3. Desde la carpeta del proyecto y en la consola del sistema, instalación del cliente Paho-mqtt ejecutando el siguiente comando:
     pip install paho-mqtt

Una vez realizado los pasos anteriores, tenemos creado un proyecto Python con el cliente Paho-mqtt para desarrollar los publicadores y subscriptores del broker Mosquitto.

Broker Mosquitto

Para realizar las pruebas, necesitamos arrancar el broker Mosquitto, un publicador y un subscriptor para realizar las pruebas. Los pasos a seguir y la estructura de topics serán los mismos que en la entrada anterior. Los comandos a ejecutar son los siguientes:

  • Para el arranque del broker Mosquitto en el puerto por defecto en la línea de comandos, se realiza con el siguiente comando:
sudo /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf
  • Para el arranque de un publicador en la línea de comandos, se realiza con el siguiente comando:
mosquitto_pub -t "casa/habitaciones/hab1/luz" -m "ON"
  • Para el arranque de un subscriptor en la línea de comandos, se realiza con el siguiente comando:
mosquitto_sub -t "casa/habitaciones/hab1/luz" -v

Una vez realizados estos pasos, tenemos el broker levantado, las consolas del publicador y subscriptor abiertas para ver la actividad existente.

Publicadores

En el presente apartado, me centraré en presentar tres ejemplos de publicación de mensajes en un topic del broker Mosquitto. El publicador es aquel elemento que genera mensajes que son enviados al broker Mosquitto.

Ejemplo básico

El primer ejemplo, lo realizamos en un fichero Python con nombre publicador_ejem1.py, es aquel ejemplo básico de creación de un cliente, conexión al broker y publicación del mensaje “Ejemplo desde Python” en el topic del broker Mosquitto.

El snippet del código es el siguiente:

 import paho.mqtt.client as mqtt

 broker_address = "localhost"
 client = mqtt.Client('Publicador_ejem1') # Creación del cliente
 client.connect(broker_address)
 topic = "casa/habitaciones/hab1/luz"

 client.publish(topic, "Ejemplo desde Python")

Tras la ejecución de publicador_ejem1.py, la salida en la consola del subscriptor Mosquitto, abierta conforme lo descrito en el apartado anterior, es la siguiente:

casa/habitaciones/hab1/luz Ejemplo desde Python

Ejemplo publicador de un mensaje

El segundo ejemplo, lo realizamos en un fichero Python con nombre publicador_ejem2.py, es aquel ejemplo de creación de un publicador, definición del topic al cual informar, y envío del mensaje “Ejemplo de publicador simple desde Python” al broker Mosquitto mediante la función single del publicador. El snippet del código es el siguiente:

 import paho.mqtt.publish as publish

 broker_address = "localhost"
 topic = "casa/habitaciones/hab1/luz"
 publish.single(topic, "Ejemplo de publicador simple desde Python", hostname=broker_address,
 client_id='publicador_ejem2.py')

Tras la ejecución de publicador_ejem2.py, la salida en la consola del subscriptor Mosquitto, abierta conforme lo descrito en el apartado anterior, es la siguiente:

casa/habitaciones/hab1/luz Ejemplo de publicador simple desde Python

Ejemplo publicador de mensaje múltiple

El tercer ejemplo, lo realizamos en un fichero Python con nombre publicador_ejem3.py, es aquel que utiliza un publicador, se definen tres mensajes (mensaje1, mensaje2 y mensaje3) para ser enviados al broker Mosquitto mediante la función multiple del publicador. El snippet del código es el siguiente:

 import paho.mqtt.publish as publish

 broker_address = "localhost"
 topic = "casa/habitaciones/hab1/luz"
 mensaje1 = "Mensaje 1 de un envío multiple"
 mensaje2 = "Mensaje 2 de un envío multiple"
 mensaje3 = "Mensaje 3 de un envío multiple"

 msgs = [{'topic': topic, 'payload': mensaje1},
 (topic, mensaje2, 0, False),
 (topic, mensaje3, 0, False)]

 publish.multiple(msgs, hostname=broker_address)

Tras la ejecución de publicador_ejem3.py, la salida en la consola del subscriptor Mosquitto, abierta conforme lo descrito en el apartado anterior, es la siguiente:

casa/habitaciones/hab1/luz Mensaje 3 de un envío multiple
casa/habitaciones/hab1/luz Mensaje 2 de un envío multiple
casa/habitaciones/hab1/luz Mensaje 1 de un envío multiple

Subscriptores

En el presente apartado, me centraré en presentar tres ejemplos de subscriptores de mensajes en un topic del broker Mosquitto. El subscriptor es aquel elemento que recibe mensajes de un topic del broker Mosquitto originados en un publicador determinado.

Ejemplo básico de subscriptor

El primer ejemplo, lo realizamos en el fichero subscriptor_ejem1.py, es aquel que utiliza un cliente MQTT con las siguientes tareas: definición de un callback para la recepción de mensajes del topic en la función on_message; conexión al broker con la función connect;y, la subscripción al topic del broker de Mosquitto, mediante la función subscribe. El presente ejemplo realiza solo una iteración por las funciones loop_start() y loop_stop. El snippet del código es el siguiente:

import time
import paho.mqtt.client as mqtt # import the client1

broker_address = "localhost"
broker_port = 1883
topic = "casa/habitaciones/hab1/luz"

def on_message(client, userdata, message):
 print("Mensaje recibido=", str(message.payload.decode("utf-8")))
 print("Topic=", message.topic)
 print("Nivel de calidad [0|1|2]=", message.qos)
 print("Flag de retención o nuevo?=", message.retain)

client = mqtt.Client("Subscriptor_ejem1")
client.on_message = on_message 
client.connect(broker_address) 
client.loop_start() # Inicio del bucle
client.subscribe(topic)
time.sleep(10) # Paramos el hilo para recibir mensajes.
client.loop_stop() # Fin del bucle

Tras la ejecución del subscriptor_ejem1.py y tras la publicación de un mensaje en la consola Mosquitto, conforme a lo descrito en el apartado anterior, la salida es la siguiente:

Mensaje recibido= PRUEBA
Topic= casa/habitaciones/hab1/luz
Nivel de calidad [0|1|2]= 0
Flag de retención o nuevo?= 0

Ejemplo de subscriptor de N mensajes

El segundo ejemplo, lo realizamos en el fichero subscriptor_ejem2.py, es aquel ejemplo que espera infinitos mensajes de un topic del broker Mosquitto. Las operaciones son casi las mismas al apartado anterior, estando las diferencias en la definición de un bucle infinito con la función loop_forever(). El snippet del código es el siguiente:

import paho.mqtt.client as mqtt

broker_address = "localhost"
broker_port = 1883
topic = "casa/habitaciones/hab1/luz"

def on_message(client, userdata, message):
 print("Mensaje recibido=", str(message.payload.decode("utf-8")))
 print("Topic=", message.topic)
 print("Nivel de calidad [0|1|2]=", message.qos)
 print("Flag de retención =", message.retain)

client = mqtt.Client('Cliente1') 
client.on_message = on_message 
client.connect(broker_address, broker_port, 60) 
client.subscribe(topic) # Subscripción al topic
client.loop_forever()

Tras la ejecución del subscriptor_ejem2.py y tras la publicación de dos mensajes en el consola Mosquitto, conforme a lo descrito en el apartado anterior, la salida es la siguiente:

Mensaje recibido= PRUEBA1
Topic= casa/habitaciones/hab1/luz
Nivel de calidad [0|1|2]= 0
Flag de retención = 0
Mensaje recibido= PRUEBA2
Topic= casa/habitaciones/hab1/luz
Nivel de calidad [0|1|2]= 0
Flag de retención = 0

Ejemplo con control de conexión al topic

El tercer ejemplo, lo realizamos en el fichero subscriptor_ejem3.py, es aquel ejemplo que espera infinitos mensaje de un topic del broker Mosquitto pero se define un callback para realizar la conexión definida en la función on_connect; este callback, si se pierde la conexión, realiza la reconexión al topic. El snippet del código es el siguiente:

import paho.mqtt.client as mqtt
broker_address = "localhost"
broker_port = 1883
topic = "casa/habitaciones/hab1/luz"

def on_connect(client, userdata, flags, rc):
 print("Connected with result code " + str(rc))
 print("UserData= " + str(userdata))
 print("flags= " + str(flags))
 print("")
 client.subscribe(topic)

def on_message(client, userdata, message):
 print("Mensaje recibido=", str(message.payload.decode("utf-8")))
 print("Topic=", message.topic)
 print("Nivel de calidad [0|1|2]=", message.qos)
 print("Flag de retención=", message.retain)
 print("---------------------------------------------")
 print("")

client = mqtt.Client('Cliente1', userdata="UsuarioDePrueba") 
client.on_connect = on_connect 
client.on_message = on_message 
client.connect(broker_address, broker_port, 60) 
client.loop_forever()

Tras la ejecución del subscriptor_ejem3.py y tras la publicación de un mensaje en el consola Mosquitto, conforme a lo descrito en el apartado anterior, la salida es la siguiente:

Connected with result code 0
UserData= UsuarioDePrueba
flags= {'session present': 0}

Mensaje recibido= PRUEBA
Topic= casa/habitaciones/hab1/luz
Nivel de calidad [0|1|2]= 0
Flag de retención= 0
---------------------------------------------

La definición de componentes software con la librería Paho-mqtt para la comunicación con el broker Mosquitto, es una tarea sencilla teniendo claro el modelo productor/consumidor. En los ejemplos descritos, se centran en la comunicación con el broker faltando el desarrollo de captura de datos de los sensores o componentes los cuales han sido simulados desde la consola de Mosquitto.