Inyección de dependencias en programación funcional III. Mónada Reader

Llegamos a la la última entrada de la sería de inyección de dependencias con la entrada, Inyección de dependencias en programación funcional III. Mónada Reader. El objetivo de la misma es mostrar al lector cómo se realiza la inyección de dependencias con la mónada Reader en lenguaje Scala. Para el lector interesado, las entradas de la serie son las siguientes:

La diferencia conceptual respecto a las otras dos es el uso de la mónada Reader. La mónada Reader es aquella mónada la cual puede leer un determinado componente; en dicho  componente, es donde definimos los elementos con las referencias de las funciones a inyectar. Así, necesitamos definir un elemento, en nuestro caso una case class, con las referencias a las funciones las cuáles están definidas en los componentes. Por otro lado, el servicio de negocio lo definimos a partir de un trait con un constructor de tipos.

Desde un punto de vista gráfico, la vista estática de los componentes queda definida como sigue:

Los tipos utilizados son los siguientes:

import cats.data.Reader
import cats.syntax.either._
import scala.language.higherKinds
object typesEjem3{
  type MensajeError = String
  type GetComponent1 = (String) => Either[MensajeError, String]
  type GetComponent2 = (Int) => Either[MensajeError, Int]
  type ResponseService = Either[MensajeError, String]
  type ParameterString = String
  type ParameterInt = Int
  type ServiceOperation[A] = Reader[ServiceContext, A]
  case class ServiceContext( funcComponent1: GetComponent1, funcComponent2: GetComponent2 )
}

La definición de los componentes de negocio del ejemplo son los representados por el objeto Component1Ejem3 y Component2Ejem3. El snippet del código de los componentes es el siguiente:

object Component1Ejem3{
  import typesEjem3._
  val response1: MensajeError = "Error en Response1"
  val doSomething: GetComponent1 = (elem: String) => {
    elem.length match {
      case lengthElem: Int if lengthElem > 0 => (elem + " modificado").asRight
      case _ => response1.asLeft
    }
  }
}
object Component2Ejem3{
  import typesEjem3._
  val response2: MensajeError = "Error en Response2"
  val doSomething: GetComponent2 = (num: Int) => {
    num match {
      case elem: Int if elem > 0 => elem.asRight
      case _ => response2.asLeft
    }
  }
}

La definición del servicio de negocio se realiza con un type class empleando un trait Service3 y el objeto ServiceImpl. Para el lector interesado en conocer lo que es un Type Class en los siguientes enlaces describo cómo se define y describe dicho patrón. Los enlaces son los siguientes:

El snippet del código del servicio es el siguiente:

trait Service3[ F[_] ]{
  def doBusiness(msg: typesEjem3.ParameterString): F[ Either[typesEjem3.MensajeError, String] ]
}
object ServiceImpl extends Service3[typesEjem3.ServiceOperation]{
  override def doBusiness(msg: typesEjem3.ParameterString): typesEjem3.ServiceOperation[Either[typesEjem3.MensajeError, String]] = Reader{ ctx =>
    for{
      response1 <- ctx.funcComponent1(msg).right
      response2 <- ctx.funcComponent2(msg.length).right
    }yield{
      response1 + "-" + response2
    }
  }
}

Como se muestra en el snippet anterior la función doBusiness del objeto ServiceImpl define la funcionalidad del servicio y es donde se utiliza la mónada Reader. La Mónada Reader se define de la siguiente manera : Reader[ServiceContext, A]; siendo la entrada de tipo ServiceContext; y, como salida, el tipo A el cual en nuestro caso es de tipo Either. Analizando la función, el objeto de entrada es de tipo ServiceContext con las referencias a los componentes que se inyectan y, como resultado, se retorna un elemento de tipo Either.

La aplicación que usa los anteriores elementos es la siguiente:

object Ejem3DependencyInyector extends App{
  import typesEjem3._
  def ejemplo1(): Unit = {
    val context = ServiceContext(Component1Ejem3.doSomething, Component2Ejem3.doSomething)
    val message1 = "Mensaje de prueba"
    ServiceImpl.doBusiness(message1).run(context) match {
      case Right(msg) => println(s"Test1=${msg}")
      case Left(error) => println(error)
    }
    println
  }
  ejemplo1()
}

En la aplicación anterior, se muestra cómo usar un servicio con una mónada Reader: lo primero, es definir una clase ServiceContext con las funciones de los componentes; segunda, crear e invocar la clase con la mónada usando la función run; y, para finalizar, tratar el resultado con un pattern matching.

La salida por consola es la siguiente:

Test1=Mensaje de prueba modificado-17

La definición de los test del servicio de negocio descrito en el ejemplo es el siguiente:

import cats.syntax.all._
import es.ams.dependencyinyector.typesEjem3.{ GetComponent1, GetComponent2, ServiceContext} //
import org.scalatest.{Matchers, WordSpec}
class Ejem3DependecyInyectorTest extends WordSpec with Matchers {
  "Example Mock" should {
    "Example OK" in {
      val context = ServiceContext(Component1Ejem3.doSomething, Component2Ejem3.doSomething)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      result shouldBe(msg + " modificado-6")
    }
    "Example OK: mock component1" in {
      val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
      val context = ServiceContext(funcGetResponse1Mock, Component2Ejem3.doSomething)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
      assert(result.equals("mock-6"))
    }
    "Example OK: mock component2" in {
      val funcComponent2: GetComponent2 = (num: Int) => 0.asRight
      val context = ServiceContext(Component1Ejem3.doSomething,funcComponent2)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
    }
    "Example OK: mock component1 and mock component2" in {
      val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
      val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight
      val context = ServiceContext(funcGetResponse1Mock, funcGetResponse2Mock)
      val msg: String = "prueba"
      val result: String = ServiceImpl.doBusiness(msg).run(context) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      assert(result.length > 0)
      assert(result.equals("mock-0"))
    }
  }
}

La inyección de dependencias desde un punto de vista funcional sigue la misma filosofía que la inyección de dependencias de objetos. La primera consecuencia es la desaparición de la utilización de framework de Mock necesarios en otros paradigmas como el utilizado en los lenguajes Java o Python. La utilización del paradigma funcional permite la composición de elementos más intuitiva aunque, evidentemente, la curva de aprendizaje es mayor.

Inyección de dependencias en programación funcional II

En la entrada anterior, Inyección de dependencias en programación funcional I, realicé la descripción de cómo se realizaba la inyección de funciones en programación funcional en lenguaje Scala; en la presente entrada, Inyección de dependencias en programación funcional II, modularizaré el código existente en la primera entrada organizando el código con una perspectiva orientada a objetos sin perder el aspecto funcional.

La vista estática del problema es la definida en el diagrama de clases de la siguiente imagen:

 

Los tipos utilizados en el ejemplo son los siguientes:

import cats.syntax.either._
object typesEjem2{
  type MensajeError = String
  type GetComponent1 = (String) => Either[MensajeError, String]
  type GetComponent2 = (Int) => Either[MensajeError, Int]
  type ResponseService = Either[MensajeError, String]
  type ParameterString = String
  type ParameterInt = Int
  type BusinessService = (GetComponent1, GetComponent2) => ParameterString => ResponseService
}

La definición de los componentes de negocio del ejemplo son los representados por los objetos Component1 y Component2. Respecto al ejemplo de la entrada anterior, se han definido las funciones dentro de un objeto con lo cual modularizamos la funcionalidad. El snippet del código de los componentes es el siguiente:

object Component1{
  import typesEjem2._
  val response1: MensajeError = "Error en Response1"
  val doSomething: GetComponent1 = (elem: String) => {
    elem.length match {
      case lengthElem: Int if lengthElem > 0 => (elem + " modificado").asRight
      case _ => response1.asLeft
    }
  }
}
object Component2{
  import typesEjem2._
  val response2: MensajeError = "Error en Response2"
  val doSomething: GetComponent2 = (num: Int) => {
    num match {
      case elem: Int if elem > 0 => elem.asRight
      case _ => response2.asLeft
    }
  }
}

La definición del servicio de negocio del ejemplo es el definido por el objeto Service. La estrategia de modularización es la misma que con los componentes. El snippet del código del servicio es el siguiente:

object Service{
  import typesEjem2._
  val doBusinessActivity: BusinessService = (objComp1, objComp2) => (msg) => {
    for {
      respon1 <- objComp1 (msg)
      respon2 <- objComp2(msg.length)
    } yield {
      respon1 + "-" + respon2
    }
  }
}

La aplicación de ejemplo que usa los anteriores elementos es la siguiente:

object Ejem2DependencyInyectorApp extends App {
  def ejemplo1(): Unit = {
    val message1 = "Mensaje de prueba"
    Service.doBusinessActivity(Component1.doSomething, Component2.doSomething)(message1) match {
      case Right(msg) => println(s"Test1=${msg}")
      case Left(error) => println(error)
    }
    val message2 = ""
    Service.doBusinessActivity(Component1.doSomething, Component2.doSomething)(message2) match {
      case Right(msg) => println(s"Test2=${msg}")
      case Left(error) => println(error)
    }
  }
  ejemplo1()
}

La salida por consola es la siguiente:

Test1=Mensaje de prueba modificado-17
Error en Response1

La definición de los test del servicio de negocio descrito en el ejemplo es el siguiente:

import org.scalatest.{Matchers, WordSpec}
import es.ams.dependencyinyector.typesEjem2.{GetComponent1, GetComponent2}
import cats.syntax.all._
class Ejem2DependecyInyectorTest extends WordSpec with Matchers {
  "Example Mock" should {
    "Example OK" in {
      val msg: String = "prueba"
      val result: String = Service.doBusinessActivity(Component1.doSomething, Component2.doSomething)(msg) match {
        case Right(msg) => { println(msg); msg}
        case Left(error) => error
      }
      result shouldBe(msg + " modificado-6")
    }
  "Example OK: mock component1" in {
    val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
    val msg: String = "prueba"
    val result: String = Service.doBusinessActivity(funcGetResponse1Mock, Component2.doSomething)(msg) match {
      case Right(msg) => { println(msg); msg}
      case Left(error) => error
    }
    assert(result.length > 0)
    assert(result.equals("mock-6"))
  }
  "Example OK: mock component2" in {
    val funcComponent2: GetComponent2 = (num: Int) => 0.asRight
    val msg: String = "prueba"
    val result: String = Service.doBusinessActivity(Component1.doSomething, funcComponent2)(msg) match {
      case Right(msg) => { println(msg); msg}
      case Left(error) => error
    }
    assert(result.length > 0)
  }
  "Example OK: mock component1 and mock component2" in {
    val funcGetResponse1Mock: GetComponent1 = (num: String) => "mock".asRight
    val funcGetResponse2Mock: GetComponent2 = (num: Int) => 0.asRight
    val msg: String = "prueba"
    val result: String =Service.doBusinessActivity(funcGetResponse1Mock, funcGetResponse2Mock)(msg) match {
      case Right(msg) => { println(msg); msg}
      case Left(error) => error
    }
    assert(result.length > 0)
    assert(result.equals("mock-0"))
    }
  }
}

En esta entrada he realizado la modularización del código definido en la entrada, Inyección de dependencias en programación funcional I; en la siguiente entrada, subiré el nivel de abstracción y describiré el mismo problema utilizando la mónada Reader.

Scala Future con Ejemplos, continuación

En la entrada anterior, “Scala Future con Ejemplos”, realicé una descripción de cómo utilizar la entidad Future en Scala con ejemplos. En la presente entrada, “Scala Future con Ejemplos, continuación”, realizaré una ampliación de Future y, además, describiré ejemplos de utilización de Future con Actores.

El modelo de actores, segun wikepedia, es un modelo matemático de computación simultánea que trata a los actores como los primitivos universales de la computación concurrete. La implementación que utilizaremos es la que proporciona Akka y voy a suponer que el lector tiene unos conocimientos mínimos del modelo de actores y de Akka. La definición de dependencia de la librería Akka en sbt es la siguiente:

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % akkaVersion,

Para la realización de los siguientes ejemplos, es necesario definir un actor con una mínima funcionalidad. La funcionalidad es la siguiente: si el actor recibe un mensaje de tipo String (msg), el actor responde con la concatenación del contenido de msg con el texto “Recibido en Actor”; si el actor recibe cualquier otro mensaje, el actor responde con el mensaje “Hola Mundo.” La implementación del actor es la siguiente:

import akka.actor.{Actor, Props}
import scala.concurrent.Future
object ActorEjemplo {
  def props() = Props(new ActorEjemplo())
}
class ActorEjemplo extends Actor {
  import context.dispatcher
  def receive = {
    case msg:String =>{
      val respuesta = msg + " Recibido en Actor"
      println(respuesta)
      sender() ! respuesta
    }
    case _ => {
      val respuesta = "Hola Mundo"
      println(respuesta)
      sender() ! respuesta
    }
  }
}

La lista de ejemplos que se muestran son los siguientes:

  • Ejemplo 1: ejemplo de Futures con tratamiento funcional
  • Ejemplo 2: ejemplo de Futures con tratamiento funcional
  • Ejemplo 3: ejemplo de Futures con tratamiento funcional.
  • Ejemplo 4: ejecución de un Actor con tiempo de espera.
  • Ejemplo 5: ejecución de un Actor sin tiempo de espera.
  • Ejemplo 6: composición de Futures.
  • Ejemplo 7: composición de Furures con for comprehension.
  • Ejemplo 8: conversión de List[Future[A]] -> Future[List[A]]
  • Ejemplo 9: conversión de Future[List[A]] -> List[Future[A]]
  • Ejemplo 10: morfismos con Future, función fold.
  • Ejemplo 11: morfismos con Future, función reduce
  • Ejemplo 12: el primero que termine, se ejecuta; función firstCompletedOf

Ejemplo 1: ejemplo de Futures con tratamiento funcional

Sea un Future future1 cuyo resultado sea el texto “Hello world!”; sea el Future future2 cuyo resultado es el valor entero 3; y, por último, sea el Future future3 que define una secuencia de operaciones con futuro1 y future2 para realizar un cálculo numérico a partir de la ejecución secuencial de future1 y future2.

El snippet del código con las ejecución de las tres futuros  la solución es la siguiente:

def ejemplo(): Unit = {
  val future1 = Future {
    "Hello world!"
  }
  val future2 = Future.successful(3)
  val future3 = future1 map { elemf1 =>
    future2 map { elemf2 =>
      elemf1.length * elemf2
    }
  }
  future3 onComplete {
    case Success(resultado) => println(s"resultado ejemplo3=${resultado}")
    case Failure(error) => println(s"error ejemplo3=${error}")
  }
}

La salida por consola de la ejecución del código anterior es la siguiente:

resultado ejemplo3=Success(36)

Ejemplo 2: ejemplo de Futures con tratamiento funcional

El siguiente ejemplo es el mismo que el caso anterior pero empleando la función foreach.

def ejemplo(): Unit = {
  val future1 = Future {
    "Hello world!"
  }
  val future2 = Future.successful(3)
  val future3 = future1 flatMap { elemf1 =>
    future2 map { elemf2 =>
      elemf1.length * elemf2
    }
  }
  future3 foreach { elem => println(s"Resultado ejemplo4=${elem}") }
}

La salida por consola de la ejecución del código anterior es la siguiente:

Resultado ejemplo4=36

Ejemplo 3: ejemplo de Futures con tratamiento funcional

Sean tres Futures cuyo resultado individual son tres operaciones matemáticas simples y, el último Future, define un filtro. El resultado será la multiplicación del segundo por el tercero.

El snippet del código con la solución es la siguiente:

def ejemplo5(): Unit = {
  val resultado = for {
    a <- Future(10 / 2)
    b <- Future(a + 1)
    c <- Future(a - 1)
    if c > 3
  } yield {
    b * c
  }
  resultado foreach { elem => println(s"Resultado ejemplo5=${elem}") }
}

La salida por consola de la ejecución del código anterior es la siguiente:

Resultado ejemplo5=24

Ejemplo 4: ejecución de un Actor con tiempo de espera

El ejemplo siguiente realizará la creación de un actor, envío de un mensaje y el tratamiento de la respuesta del actor con un tiempo de espera; para finalizar, se realizará la eliminación del actor del ejemplo enviándole el mensaje “PoisonPill”. La definición del actor es la definida al principio de la entrada y el sistema de actores es el identificado con el identificador “Ejem”.

El código del ejemplo es el siguiente:

import scala.concurrent._
import ExecutionContext.Implicits.global
implicit val system = ActorSystem("Ejem")
implicit val timeout = Timeout(2 seconds)
def ejemplo1(): Unit = {
  val actorEjemplo1 = system.actorOf(ActorEjemplo.props(), "ActorEjemplo1")
  val future: Future[Any] = actorEjemplo1 ? "Mensaje"
  val result: String = Await.result(future, timeout.duration).asInstanceOf[String]
  println(s"\nresultado ejemplo1=${result}")
  actorEjemplo1 ! PoisonPill
}

La salida por consola de la ejecución del código anterior es la siguiente:

Mensaje Recibido en Actor
resultado ejemplo1=Mensaje Recibido en Actor

La creación del actor se realiza con la función actorOf del sistema de actores identificado  como system; el envío de un mensaje a un actor se utiliza la función “?”; y, su resultado, es gestionado por el componente Await el cual opera con la respuesta de tipos Future que retorna el actor.

Ejemplo 5: ejecución de un Actor sin tiempo de espera

El siguiente ejemplo es idéntico que el anterior pero se espera la respuesta del actor de tipo Future sin tiempo de espera. La lógica es la misma que el ejemplo anterior pero se emplea la función onComplete para gestionar el resultado de ejecución. El código del ejemplo es el siguiente:

def ejemplo2(): Unit = {
  val actorEjemplo2 = system.actorOf(ActorEjemplo.props(), "ActorEjemplo2")
  // La función mapTo retorna un nuevo Future con la respuesta si es Success; en otro caso, ClassCastException.
  val future: Future[String] = ask(actorEjemplo2, "mensaje").mapTo[String]
  future onComplete {
    case Success(resultado) => println(s"\nresultado ejemplo2=${resultado}\n")
    case Failure(error) => println(s"\nerror ejemplo2=${error}\n")
  }
  actorEjemplo2 ! PoisonPill
}

Ejemplo 6: composición de Futures.

Supongamos que necesitamos ejecutar dos futuros y, su resultado, es la entrada de un tercer futuro. Una posible solución con un tiempo de espera determinado es la siguiente:

def ejemplo6(): Unit = {
  val actorEjemplo6_1 = system.actorOf(ActorEjemplo.props(), "ActorEjemplo6-1")
  val actorEjemplo6_2 = system.actorOf(ActorEjemplo.props(), "ActorEjemplo6-2")
  val actorEjemplo6_3 = system.actorOf(ActorEjemplo.props(), "ActorEjemplo6-3")

  val future1 = actorEjemplo6_1 ask ("Mensaje a Actor6_1")
  val future2 = actorEjemplo6_2 ask ("Mensaje a Actor6_2")

  val respuestaFuture1 = Await.result(future1, 3 seconds).asInstanceOf[String]
  val respuestaFuture2 = Await.result(future2, 3 seconds).asInstanceOf[String]
  val future3 = actorEjemplo6_3 ask ("##" + respuestaFuture1 + "&" + respuestaFuture2 + "##")
  val respuestaFuture3 = Await.result(future3, 3 seconds).asInstanceOf[String]
  println(s"resultado ejemplo6=${respuestaFuture3}")
}

La salida por consola de la ejecución del código anterior es la siguiente:

Mensaje a Actor6_1 Recibido en Actor
Mensaje a Actor6_2 Recibido en Actor
##Mensaje a Actor6_1 Recibido en Actor&Mensaje a Actor6_2 Recibido en Actor## Recibido en Actor
resultado ejemplo6=##Mensaje a Actor6_1 Recibido en Actor&Mensaje a Actor6_2 Recibido en Actor## Recibido en Actor

Ejemplo 7: composición de Furures con for comprehension

El siguiente ejemplo es el mismo que el caso anterior pero se emplea for comprehensión.

def ejemplo7(): Unit = {
  val actorEjemplo7_1 = system.actorOf(ActorEjemplo.props(), "ActorEjemplo7-1")
  val actorEjemplo7_2 = system.actorOf(ActorEjemplo.props(), "ActorEjemplo7-2")
  val actorEjemplo7_3 = system.actorOf(ActorEjemplo.props(), "ActorEjemplo7-3")
  val future1 = actorEjemplo7_1 ask ("Mensaje a Actor7_1")
  val future2 = actorEjemplo7_2 ask ("Mensaje a Actor7_2")
  val future3 = for {
    f1 <- future1.mapTo[String]
    f2 <- future2.mapTo[String]
    c <- ask(actorEjemplo7_3, "##" + f1 + "&" + f2 + "##").mapTo[String]
  } yield {
    c
  }
  future3 foreach { resultado => println(s"Resultado ejemplo7=${resultado}") }
}

La salida por consola de la ejecución del código anterior es la siguiente:

Mensaje a Actor7_2 Recibido en Actor
Mensaje a Actor7_1 Recibido en Actor
##Mensaje a Actor7_1 Recibido en Actor&Mensaje a Actor7_2 Recibido en Actor## Recibido en Actor
Resultado ejemplo7=##Mensaje a Actor7_1 Recibido en Actor&Mensaje a Actor7_2 Recibido en Actor## Recibido en Actor

Ejemplo 8: conversión de List[Future[A]] -> Future[List[A]]

Supongamos que tenemos una lista de operaciones Futuras del mismo tipo, en nuestro caso, Futuros cuya respuesta son números enteros; y, su procesamiento, puede ser transformado un un único Future. La solución consiste en emplear la función sequence la cual es la encargada de transformar muchos Future en uno único. Así, la solución se define de la siguiente forma:

def ejemplo8(): Unit = {
  val listaFutures: List[Future[Int]] = List(Future(1), Future(3), Future(5), Future(7))
  // Convertimos un List[Future[Int]] en Future[List[Int]] para trabajar con el Future.
  // Así, podemos trabajar con la List[Int]
  val futureList: Future[List[Int]] = Future.sequence(listaFutures)
  println(s"futureList=${futureList}")
  val suma: Future[Int] = futureList.map(_.sum)
  suma foreach{elem => println(s"Resultado ejemplo8=${suma}")}
}

La salida por consola de la ejecución del código anterior es la siguiente:

Resultado ejemplo8=Success(16)

Ejemplo 9: conversión de Future[List[A]] -> List[Future[A]]

Supongamos que tenemos un único Future y una lista de datos de un determinado tipo  que procesar y, su procesamiento, puede ser ejecutado por N Futuros. La solución consiste en emplear la función traverse. El código del ejemplo es el siguiente:

def ejemplo9(): Unit = {
  // Convertimos un Future[List[Int]] en List[Future[Int]] para trabajar con el Future.
  val futureList1 = Future.traverse((1 to 10).toList)(x => Future(x * 2 - 1))
  val resultadoFutureList1 = futureList1.map(_.sum)
  resultadoFutureList1 foreach {elem => println(s"Resultado1 de ejemplo9=${elem}")}
  val futureList2 = Future.traverse(List(1,3,5))(x => Future(x * 2 - 1))
  val resultadoFutureList2 = futureList2.map(_.sum)
  resultadoFutureList2 foreach {elem => println(s"Resultado2 de ejemplo9=${elem}")}
}

La salida por consola de la ejecución del código anterior es la siguiente:

Resultado2 de ejemplo9=15
Resultado1 de ejemplo9=100

Ejemplo 10: morfismos con Future, función fold

Supongamos que tenemos un ADT de tipo lista de futuros no bloqueantes y queremos realizar un morfismo sobre dicho ADT; el mecanismos para realizarlo, es utilizar la función fold. El siguiente snippet muestra un ejemplo representativo:

def ejemplo10(): Unit = {
  val listaFutures: List[Future[Int]] = List(Future(1), Future(3), Future(5), Future(7))
  val sumaListaFutures = Future.fold(listaFutures)(0)(_+_)
  sumaListaFutures onComplete{
    case Success(resultado) => println(s"Resultado ejemplo10=${resultado}")
    case Failure(error) => println(s"Error ejemplo10=${error}")
  }
}

Ejemplo 11: morfismos con Future, función reduce

La función reduce es como la función fold pero sin valor inicial.

def ejemplo11(): Unit = {
  val listaFutures: List[Future[Int]] = List(Future(1), Future(3), Future(5), Future(7))
  val futureSum: Future[Int] = Future.reduce(listaFutures)(_ + _)
  futureSum foreach { elem => println(s"Resultado ejemplo11=${futureSum}") }
}

La salida por consola de la ejecución del código anterior es la siguiente:

Resultado ejemplo11=Success(16)

Ejemplo 12: el primero que termine, se ejecuta; función firstCompletedOf

Supongamos que tenemos una lista de futuros en un ADT de tipo lista; y, a nivel de negocio, solo nos interesa el resultado del primer future que termina; en estos casos,  se emplea la función firstCompletedOf. El siguiente ejemplo muestra un ejemplo representativo.

def ejemplo12(): Unit = {
  val listaFutures: List[Future[Int]] = List(Future(1), Future(3), Future(5), Future(7))
  val futureFirst: Future[Int] = Future.firstCompletedOf(listaFutures)
  futureFirst foreach { elem => println(s"Resultado ejemplo12=${futureFirst} elem=${elem}") }
}

Una de las posibles salidas por consola de la ejecución del código anterior es la siguiente:

Resultado ejemplo12=Success(3) elem=3

Estos son los ejemplos que presento, si al lector interesado se le ocurre plantear otro ejemplo, o bien, plantear cualquier otra alternativa, estaré encantado de compartirlo.

Scala: Future con Ejemplos

En todo proyecto o aplicación informática es habitual realizar alguna operación asíncrona, es decir, ejecutar una operación en donde se lanza un mensaje de una operación sin quedarte bloqueado a la espera de su resultado. En la entrada de hoy, “Scala Future con ejemplos”, voy a presentar unos ejemplos de utilización de Future desde un punto de vista practico.

Sin ser exhaustivo, podemos definir Future como aquel objeto que contiene un valor el cual estará disponible en algún instante.

La estructura de los ejemplos es incremental en dificultad y los ejemplos que presento son ejemplos que en nuevas versiones del lenguaje pueden presentar diferencias. Los ejemplos son los siguientes:

  1. Ejemplo 1 básico desde consola
  2. Ejemplo 2 básico desde consola.
  3. Ejemplo 3 básico desde consola.
  4. Ejemplo 4: Future y tratamiento de errores con recover.
  5. Ejemplo 5: Future y tratamiento de errores con recoverWith.
  6. Ejemplo 6: Future y ejecución paralela con función fallbackTo.
  7. Ejemplo 7: Future y ejecución paralela con función zip.
  8. Ejemplo 8: Future y ejecución paralela con for comprehension.
  9. Ejemplo 9: Tratamiento de tareas Future para aquella que acabe primero.

Ejemplo 1 básico desde consola

El ejemplo más básico es ejecutar un código en la consola Scala; para ello, arrancamos la consola; insertamos el comando “:paste” y, posteriormente, copiamos el siguiente snippet de código finalizando con Ctrl- D.

El ejemplo define un Future en el cual se lanza una excepción; una vez recibida el resultado, se escribe por la salida estándar.

import scala.concurrent._
import ExecutionContext.Implicits.global
val futureFail = Future { throw new Exception("Error!") }
futureFail.foreach( value => println("->" + value) )

La salida de la ejecución es la siguiente:

import scala.concurrent._
import ExecutionContext.Implicits.global
futureFail: scala.concurrent.Future[Nothing] = Future(Failure(java.lang.Exception: Error!))

Ejemplo 2 básico desde consola

Continuamos con la consola y, en este segundo ejemplo, el snippet del código se centra
en la gestión del resultado del Future con la función onComplete y los objetos Success
y Failure. El código es el siguiente:

import scala.util._
import scala.concurrent._
import ExecutionContext.Implicits.global
val futureFail = Future {
  throw new Exception("Error!")
}
futureFail.onComplete {
  case Success(value) => println("Success:" + value)
  case Failure(e) => println("Respuesta Failure:" + e)
}

La salida de la ejecución es la siguiente:

import scala.util._
import scala.concurrent._
import ExecutionContext.Implicits.global
futureFail: scala.concurrent.Future[Nothing] = Future(<not completed>)
Respuesta Failure:java.lang.Exception: Error!

Ejemplo 3 básico desde consola

La funcionalidad de un Future puede ser una función completa y, en su definición funcional, podemos utilizar funciones, o bien, definir Future en funciones.

En el presente snippet, se definen dos funciones que ejecutan Future: getEvent y getTraffic; además, se define una secuencia de ejecución de Future empleando las funciones anteriores: futureStep1 y futureStep2; el resultado de la ejecución de la secuencia, lo realiza futureStep2 el cual controla el resultado empleando objetos Success y Failure.

import scala.util._
import scala.concurrent._
import ExecutionContext.Implicits.global
def getEvent(parametro: String): Future[String] = {
  val resultadoGetEvent = Future{
    val resultado = "getEvent: " + parametro
    resultado
  }
  resultadoGetEvent
}
def getTraffic(parametro: String): Future[String] = {
  val resultadoGetTraffic = Future {
    val resultado = "getTraffic: '" + parametro + "'"
    resultado
  }
  resultadoGetTraffic
}
val futureStep1: Future[String] = getEvent("PruebaEvent")
val futureStep2: Future[String] = {
  futureStep1.flatMap { response =>
    getTraffic(response)
  }
}
futureStep2.onComplete {
  case Success(value) => println("futureStep2 Success:" + value)
  case Failure(e) => println("futureStep2 Failure:" + e)
}

La salida de la ejecución es la siguiente:

import scala.util._
import scala.concurrent._
import ExecutionContext.Implicits.global
getEvent: (parametro: String)scala.concurrent.Future[String]
getTraffic: (parametro: String)scala.concurrent.Future[String]
futureStep1: scala.concurrent.Future[String] = Future(Success(getEvent: PruebaEvent))
futureStep2: scala.concurrent.Future[String] = Future(<not completed>)

Ejemplo 4: Future y tratamiento de errores con recover

Supongamos que en las funciones getEvent y getTraffic se producen errores; dichos errores, tenemos que controlarlos y, en el caso que se produzcan, tenemos que retornar un valor determinado; para estos casos, empleamos la función recover.

import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent._
import ExecutionContext.Implicits.global
implicit val timeout = Timeout(2 seconds)
case class Resultado(evento: String, traffic: String)
def ejemplo1(): Unit = {
  def getEvent(parametro: String): Future[String] = {
    val resultadoGetEvent = Future {
    val resultado = "getEvent: " + parametro
    println(resultado)
    resultado
  }.recover {
    case e: Exception => "Valor getEvent por defecto"
  }
  resultadoGetEvent
}
def getTraffic(parametro: String): Future[String] = {
  val resultadoGetTraffic = Future {
    val resultado = "getTraffic: '" + parametro + "'"
    println(resultado)
    resultado
  }.recover {
    case e: Exception => "Valor getTraffic por defecto"
  }
  resultadoGetTraffic
}
val resultadoFutures = for {
  event <- getEvent("Parametro")
  traffic <- getTraffic(event)
} yield {
  Resultado(event, traffic)
}
val result = Await.result(resultadoFutures, timeout.duration)
println(s"->${result}")
}

La salida de la ejecución es la siguiente:

getEvent: Parametro
getTraffic: 'getEvent: Parametro'
->Resultado(getEvent: Parametro,getTraffic: 'getEvent: Parametro')

Ejemplo 5: Future y tratamiento de errores con recoverWith

El ejemplo anterior controla los errores pero, ¿qué hacemos cuando una excepción puede ser un resultado esperado?, o bien, ¿qué hacemos cuando se pueden producir muchos tipos de excepciones y queremos controlar el resultado para cada una de ellas?. En estos casos utilizamos la función recoverWith.

case class Resultado(evento: String, traffic: String)
def ejemplo3(): Unit = {
  def getEvent(parametro: String): Future[String] = {
    val resultadoGetEvent = Future {
    val resultado = "getEvent: " + parametro
    println(resultado)
    resultado
    throw new IllegalArgumentException(s"Error en parametro ${parametro}!")
  }.recoverWith {
    case ex: IllegalArgumentException => Future.successful(ex.getMessage)
    case e: Exception => Future.failed[String](new Exception("Error generico en getEvent"))
  }
  resultadoGetEvent
}
def getTraffic(parametro: String): Future[String] = {
  val resultadoGetTraffic = Future {
    val resultado = "getTraffic: '" + parametro + "'"
    println(resultado)
    resultado
  }.recoverWith {
    case ex: IllegalArgumentException => Future.successful(ex.getMessage)
    case e: Exception => Future.failed[String](new Exception("Error generico en getEvent"))
  }
  resultadoGetTraffic
}
val resultadoFutures = for {
  event <- getEvent("Parametro")
  traffic <- getTraffic(event)
  } yield {
    Resultado(event, traffic)
  }
  val result = Await.result(resultadoFutures, timeout.duration)
  println(s"->${result}")
}

El tratamiento del resultado de la función, se realiza empleando un for comprehension de forma secuencial y la función result de Await espera por la terminación de las dos funciones.

Otra posible opción para el control del resultado es utilizando algo como sigue:

resultadoFutures.onComplete {
  case Success(value) => println("Success: #" + value + "#")
  case Failure(e) => println("Failure:" + e)
}

La salida de la ejecución es la siguiente:

getEvent: Parametro
getTraffic: 'Error en parametro Parametro!'
->Resultado(Error en parametro Parametro!,getTraffic: 'Error en parametro Parametro!')

Ejemplo 6: Future y ejecución paralela con función fallbackTo

En ciertos momentos necesitamos que dos Future se ejecuten de forma paralela. En estos casos, utilizamos la función fallbackTo.

import akka.util.Timeout
import scala.concurrent.duration._
import scala.concurrent._
import ExecutionContext.Implicits.global
implicit val timeout = Timeout(2 seconds)
def ejemplo2(): Unit = {
  def getEventforma2(parametro: String): Future[String] = {
    val resultadoGetEvent = Future {
    val resultado = "getEvent: " + parametro
    Thread.sleep(2000)
    println("->" + resultado)
    resultado
  }
  resultadoGetEvent
}
def getTrafficforma2(parametro: String): Future[String] = {
  val resultadoGetTraffic = Future {
    val resultado = "getTraffic: '" + parametro + "'"
    println("=>" + resultado)
    resultado
  }
  resultadoGetTraffic
}
// Se ejecuta en paralelo el future getEventforma2 y getTrafficforma2
// El resultado será el resultado del primer future que termine.
// El Await espera a que terminen los dos Future.
val futureResultado = getEventforma2("PruebaEvent") fallbackTo getTrafficforma2("PruebaTraffic")
val resultado = Await.result(futureResultado, timeout.duration)
println(s"->$resultado")
}

Un posible solución puede ser la siguiente pero, en función del tiempo de ejecución, se puede producir una excepción de tipo TimeoutException.

=>getTraffic: 'PruebaTraffic'
->getEvent: PruebaEvent
->getEvent: PruebaEvent

Ejemplo 7: Future y ejecución paralela con función zip

Otra forma de ejecutar Future en paralelo es utilizando la función zip y, con esta función, al terminar cada una de las funciones, realizar el tratamiento. El siguiente  ejemplo muestra un ejemplo de uso.

def ejemplo1(): Unit = {
case class Resultado(aEvent:String, aTraffic:String)
def getEvent(parametro: String): Future[String] = {
  val resultadoGetEvent = Future {
    val resultado = "getEvent: " + parametro
    println(s"getEvent=${resultado}")
    Thread.sleep(3000)
    resultado
  }
  resultadoGetEvent
}
def getTraffic(parametro: String): Future[String] = {
  val resultadoGetTraffic = Future {
    val resultado = "getTraffic: '" + parametro + "'"
    println(s"getTraffic=${resultado}")
    resultado
  }
  resultadoGetTraffic
}
val resultado = (getEvent("param1") zip getTraffic("param2")) map {
  case (event, traffic) => {
    println("#event=" + event + " #traffic=" + traffic)
    Resultado(aEvent=event, aTraffic=traffic)
  }
}
val result = Await.result(resultado, timeout.duration)
println("resultado forma1=" + result)
}

La salida de la ejecución del código es la siguiente:

getTraffic=getTraffic: 'param2'
getEvent=getEvent: param1
#event=getEvent: param1 #traffic=getTraffic: 'param2'
resultado forma1=Resultado(getEvent: param1,getTraffic: 'param2')

Ejemplo 8: Future y ejecución paralela con for comprehension

El objeto Future es de  tipo monádico con lo cual podemos emplear for comprehension de la siguiente forma:

def ejemplo2(): Unit = {
case class ResultadoMonada(tarea1: String, tarea2: String)
def getTareaAsincrona1(): String = {
  val resultadoTarea = "Hacemos una tarea asíncrona1"
  Thread.sleep(2000)
  resultadoTarea
}
def getTareaAsincrona2(): String = {
  val resultadoTarea = "Hacemos una tarea asíncrona2"
  resultadoTarea
}
val getTareaAsincrona1Future = Future {
  getTareaAsincrona1()
}
val getTareaAsincrona2Future = Future {
  getTareaAsincrona2()
}
val resultMonada = for {
  resultado1 <- getTareaAsincrona1Future
  resultado2 <- getTareaAsincrona2Future
} yield {
  ResultadoMonada(tarea1 = resultado1, tarea2 = resultado2)
}
val result = Await.result(resultMonada, timeout.duration)
println("resultado Monada=" + result)
}

La salida de la ejecución del código es la siguiente:

resultado Monada=ResultadoMonada(Hacemos una tarea asíncrona1,Hacemos una tarea asíncrona2)

Ejemplo 9: Tratamiento de tareas Future para aquella que acabe primero

Hay necesidades funcionales en las cuáles necesitamos lanzar varias tareas y tratar aquel Future cuya ejecución termine el primero, despreciando al resto. En estos casos, empleamos la función firstCompletedOf. En el siguiente ejemplo, tomando las funciones del apartado anterior, el tratamiento del primer Future en terminar sería el siguiente:

// Arranca la tareaProgramada después de 200 milisegundos
val tareaProgramada1 = after(200 millis, using=system.scheduler)(getTareaAsincrona1Future)
val result1 = Future firstCompletedOf(Seq(tareaProgramada1, getTareaAsincrona2Future))
println(s"Resultado Prueba1:${result1}")

Una de las salidas de la ejecución del código anterior es el siguiente:

Success(Hacemos una tarea asíncrona2)

Otra posible codificación puede ser la siguiente:

[...]
// Tratamiento "quien acabe primero": resultado Exception porque future2 tarda mas en terminar.
val tareaProgramada2 = after(200 millis, using=system.scheduler)(Future.failed(new IllegalStateException("error!")))
val future2 = Future { Thread.sleep(1000); "foo" }
val result2 = Future firstCompletedOf(Seq(tareaProgramada2, future2))
result2 onComplete{
  case Success(resultado) => println(s"resultado2=${resultado}")
  case Failure(error) => println(s"error2=${error}")
}

Al lanzar la tareaProgramada2 una excepción, la salida de la ejecución del código anterior es la siguiente:

error2=java.lang.IllegalStateException: error!

Para finalizar el tipo de ejemplo, otra ejecución puede ser la siguiente:

val tareaProgramada3 = after(200 millis, using=system.scheduler)(Future.failed(new IllegalStateException("error!")))
val future3 = Future { "foo" }
val result3 = Future firstCompletedOf(Seq(tareaProgramada3, future3))
result3 onComplete{
  case Success(resultado) => println(s"resultado3=${resultado}")
  case Failure(error) => println(s"error3=${error}")
}

La salida del anterior snippet de código es la siguiente:

resultado3=foo

Estos son los ejemplos que presento, si al lector interesado se le ocurre plantear otro ejemplo, o bien, plantear cualquier otra alternativa, estaré encantado de compartirlo.

Validated: control de errores

En toda aplicación software es necesario realizar tareas de validación de campos, validación de formularios,o bien, verificación de una función;y, una vez realizadas las validaciones individuales, es necesario realizar la validación del conjunto de todas ellas. En la entrada de hoy, Validated: control de errores, me centraré en describir y realizar un ejemplo de validación de los campos de un formulario representado en una estructura de tipo Map.

Supongamos que estamos desarrollando una aplicación en la cual tenemos un formulario de dos elementos: el primero, el campo nombre; y, el segundo, el campo edad. Las validaciones que tenemos que realizar son: validación del campo nombre, validación del campo edad y evaluación de la validación del conjunto del formulario.

El campo nombre debe de ser un campo que no sea vacío. El campo edad debe de ser un
campo no vacío, entero y mayor que cero.

Las validaciones se realizan mediante el tipo Validated el cual no está definido como un tipo estándar de Scala, está definido en la librería Cats. La importación del tipo Validated y el resto de importaciones necesarias para el ejemplo son las siguientes:

import cats.data.Validated
import cats.instances.list._
import cats.syntax.all._

El primer paso a realizar es realizar la definición de los alias de todas las estructuras sobre las que trabajaremos. Los alias son los siguientes:

  • Definición del formulario. El formulario es una estructura de tipo Map.
type Form = Map[String, String]
  • Definición de la estructura de control de error sencillas. Las verificaciones
    sencillas se realizarán con un elemento de tipo Either
type ControlErrorFast[A] = Either[List[String], A]
  • Definición de la estructura de control de error compleja o validación. Las verificaciones complejas se realizarán con un elemento de tipo Validated.
type ValidatedForm[A] = Validated[List[String], A]

La primera operación a realizar es la obtención de un elemento del formulario. Para
realizar dicha operación, definiremos la función getValue de la siguiente manera:

def getValue(form: Form)(campo: String) : ControlErrorFast[String] = 
  form.get(campo)
    .toRight(List(s"El valor de $campo no está especificado"))

La segunda operación es realizar la verificación del campo nombre. Para realizar dicha operación, definimos la función readName y sus funciones auxiliares. El código es el siguiente:

def nonBlank(nombre:String)(dato:String): ControlErrorFast[String] =
   Right(dato)
    .ensure(List(s"El campo $nombre no debe de ser vacío."))
     (_.nonEmpty)

def readName(form: Form): ControlErrorFast[String] =
  getValue(form)("name")
   .flatMap( elem => nonBlank("name")(elem))

def nonBlank(nombre:String)(dato:String): ControlErrorFast[String] =
  Right(dato) 
   .ensure(List(s"El campo $nombre no debe de ser vacío."))
    (_.nonEmpty)

La tercera operación es realizar la verificación del campo edad. Para realizar dicha
operación, definiremos la función readAge y sus funciones auxiliares. El código es el
siguiente:

def readAge(form: Form): ControlErrorFast[Int] =
  getValue(form)("age")
   .flatMap(nonBlank("age"))
   .flatMap(parseInt("age"))
   .flatMap(nonNegative("age"))

def parseInt(nombre:String)(age:String): ControlErrorFast[Int] =
  Either.catchOnly[NumberFormatException](age.toInt)
   .leftMap(_ => List(s"El campo $nombre debe de ser numérico"))

def nonNegative(nombre:String)(dato:Int): ControlErrorFast[Int] =
  Right(dato)
   .ensure(List(s"El campo $nombre no es válido"))
    (_ >= 0 )

Para finalizar, una vez realizadas las verificaciones de los campos, es necesario realizar la validación del formulario para obtener el listado de los posibles errores presentes en el formulario, o bien, retornar el resultado final.

La validación del formulario se realiza utilizando elementos de tipo Validated y un elemento que trabaje con los contextos de validación; dicho elemento, es un Semigroupal el cual genera una tupla de elementos del mismo contexto.

val formHtml: Form = Map("name" -> "Pepito", "age" -> "40")
val valid1_1:ValidatedForm[String] = Validated.fromEither(readName(formHtml))
val valid1_2:ValidatedForm[Int] = Validated.fromEither(readAge(formHtml))
val resultado1 = (valid1_1, valid1_2).tupled
println(s"resultado1=${resultado1}")

La salida por consola es la siguiente:

resultado1=Valid((Pepito,40))

Para el supuesto de trabajar con un formulario con datos erróneos, el código sería el siguiente:

val formHtmlKO3: Form = Map("name" -> "", "age" -> "-1")
val valid4_1:ValidatedForm[String] = Validated.fromEither(readName(formHtmlKO3))
val valid4_2:ValidatedForm[Int] = Validated.fromEither(readAge(formHtmlKO3))
val resultado4 = (valid4_1, valid4_2).tupled
println(s"resultado4=${resultado4}")
println

La salida por consola es la siguiente:

resultado4=Invalid(List(El campo name no debe de ser vacío., El campo age no es válido))

Como conclusión final, una de las opciones para el control de errores es utilizar el componente Either, pudiendo controlar los valores y las excepciones. Cuando todos los elementos Either son agrupados para su evaluación con Validated y un Semigropal podemos obtener el resultado, o bien, el conjunto de los mensajes de las no validaciones que se han producido en el mismo instante. A diferencia de otros procesos de validación, la ventaja reside en que conocemos todos los fallos producidos en el mismo tiempo.

 

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.

Circe IV: ópticas

Finalizo la serie de Circe con la presente entrada, Circe IV: ópticas, en la cual me centraré en cómo Circe emplea componentes ópticos para facilitar su funcionalidad. Circe no implementa ópticas, sino que se ayuda de la librería Monocle para implementar esta funcionalidad.

 

El sketchnote de la presente entrada, queda descrito en la siguiente imagen:

Para el lector interesado en la librería óptica Monocle, puede acceder a los siguientes enlaces de la librería Monocle que tengo publicados:

El primer paso a realizar es definir una estructura JSON de prueba con lo cual realizar la comparativa entre el modo de trabajo sin ópticas y con ópticas. El JSON de pruebas es el siguiente:

 import cats.syntax.either._
 import io.circe._
 import io.circe.parser._
 val json: Json = parse(
 """
 {
   "order": {
   "customer": {
   "name": "Custy McCustomer",
   "contactDetails": {
     "address": "1 Fake Street, London, England",
     "phone": "0123-456-789"
   }
 },
 "items": [{
   "id": 123,
   "description": "banana",
   "quantity": 10
  }, {
      "id": 456,
      "description": "apple",
      "quantity": 20
    }],
    "total": 123.45
  }
 }
 """).getOrElse(Json.Null)

Acceso a datos en Circe sin ópticas

Como he descrito en las entradas anteriores al tema, el acceso a los datos se realiza con un cursor, la función downField y get entre otros. Para refrescar los conceptos, en el siguiente snippet se definen dos ejemplos: el primero, acceso al campo Phone del JSON de prueba; y, el segundo, acceso a los valores de un array del JSON de prueba. El código es el siguiente:

 println(s"[*] Número de teléfono del cliente(NO ÓPTICA): 
     ${json.hcursor.downField("order")
        .downField("customer")
        .downField("contactDetails")
        .get[String]("phone").toOption}")
 val items: Vector[Json] = json.hcursor.downField("order").downField("items").
 focus.flatMap(_.asArray).
 getOrElse(Vector.empty)
 val quantities: Vector[Int] = items.flatMap( _.hcursor.get[Int]("quantity").toOption )
 println(s"[*] Obtención de un Array del JSON=${quantities} ")

La salida por consola es la siguiente:

 [*] Número de teléfono del cliente(NO ÓPTICA): Some(0123-456-789)
 [*] Obtención de un Array del JSON=Vector(10, 20)

Acceso a datos en Circe con ópticas

La utilización de ópticas supone la definición de un regla de acceso para cada campo. Así, tenemos que definir para cada campo una óptica. Para los ejemplos del apartado anterior, definimos las ópticas para el campo phone y el array quantity de la siguiente forma:

 import io.circe.optics.JsonPath._
 val _phoneNum = root.order.customer.contactDetails.phone.string
 println(s"Número de teléfono del cliente (ÓPTICA): ${_phoneNum.getOption(json)}")
 val items: List[Int] = root.order.items.each.quantity.int.getAll(json)
 println(s"Número de teléfono del cliente (ÓPTICA): ${items}")

La salida por consola es la siguiente:

 Número de teléfono del cliente (ÓPTICA): Some(0123-456-789)
 Número de teléfono del cliente (ÓPTICA): List(10, 20)

Modificación de datos en Circe con ópticas

Para realizar la moficación de un campo, se emplea la función modify de la óptica de aquel campo a modificar. Para realizar la modificación del campo quantity, se realiza de la siguiente forma:

 import io.circe.optics.JsonPath._
 import io.circe._
 val doubleQuantities: Json => Json = root.order.items.each.quantity.int.modify(_ * 2)
 println(s"Multiplicación del campo quantity x2= ${doubleQuantities(json)} ")

La salida por consola es la siguiente:

 Multiplicación del campo quantity x2= {
   "order" : {
   "customer" : {
   "name" : "Custy McCustomer",
   "contactDetails" : {
     "address" : "1 Fake Street, London, England",
     "phone" : "0123-456-789"
   }
 },
  "items" : [
   {
     "id" : 123,
     "description" : "banana",
     "quantity" : 20
   },
   {
     "id" : 456,
     "description" : "apple",
     "quantity" : 40
   }
  ],
   "total" : 123.45
  }
 }

Llegado a este punto, podemos llegar a la conclusión final que la operativa con estructuras JSON con la librería Circe es una tarea sencilla y de fácil aprendizaje: el acceso, modificación y transformación de JSON a una case class o viceversa, son tareas con una pequeña complejidad. Además, si se conoce el funcionamiento de las librerías ópticas, el acceso y manipulación de JSON es más sencillo aún.

Para el lector interesado, el conjunto de las entradas de la librería Circe son las siguientes: