Scala: Code interpretation at runtime

With today’s post, we’ll dive into Scala code generation on the fly: at runtime. We have to be cautious of not mixing concepts with Scala macros, which generate code at compile-time. These make use of Scala type system, which is much safer than generating code at runtime.

House-of-cards-but-why

When is it useful to make use of this mechanism then? We’ll try to shed light on this by following a very simple example, getting abstract of real implementation (that you may find at Scalera’s Github).

The problem: “da” serializer

Let’s suppose a not-so-wild case, which consists on communicating two services via an event bus (we’ll get abstract of its implementation: it could be a message queue like Kafka, Akka streams, …).

The main idea is the following:

Sender-receiver-schema

The producer knows how to send and the consumer has an associated callback for message arrivals:

trait Producer{
  def produce(message: Any): Try[Unit]
}
trait Consumer{
  val consume: Any => Unit
}

Both producer and consumer services know the message types that may arrive. In our example, they could be one of these:

case class Foo(att1: Int, att2: String)
case class Bar(att1: String)

If the producer wants to send a message using the event bus, it will have to serialize it somehow (JSON, Byte array, XML, …) so, by the time it reaches the opposite end, the consumer will start the inverse process (deserialization) and will get the original message.

…nothing weird so far.

Whydoeseverythinghavetobesocomplicated

If we have a JSON serializer …

trait JsonSer[T] {
  def serialize(t: T): String
  def deserialize(json: String): T
}

and we serialize our message …

implicit val fooSerializer: JsonSer[Foo] = ???
val foo: Foo = ???
producer.send(implicitly[JsonSer[Foo]].serialize(foo))

How do we know which deserializer to use when the consumer gets the message?

Option 1: Try every possible serializer until one of them works

In our consumer, we’d have:

lazy val consumer = new Consumer {
  override val consume: Any => Unit = {
    case message: String =>
      Seq(barSerializer, fooSerializer).flatMap { ser =>
        Try(ser.deserialize(message)).toOption
      }.headOption.fold(ifEmpty = println("Couldn't deserialize")) {
        case bar: Bar => println("it's a bar!")
        case foo: Foo => println("it's a foo!")
        case _ => println("it's ... something!")
      }
  }
}

A lil’ bit coarse, right? If the proper serializer is the last of a 100 list, we would have tried and failed with 99 serializers before (Such a waste of CPU!).

4d8

Besides, we could also consider the case of having a different type deserializer, but it fits with the received message, so it would partially or wrongly deserialize the message.

Option 2: Add the message type to the message itself

We could add an extra layer wrapping the message for indicating the message type that it contains. This way, when receiving it, we could determine the serializer type we have to use.

MessageWrapper

For writing the type, we’ll make use of Scala’s TypeTags, getting info about the T contained message type.


//Wrapper for the message (and its serializer)

import scala.reflect.runtime.universe.{TypeTag, typeTag}

case class Message[T: TypeTag](content: T){
  val messageType: Message.Type = typeTag[T].tpe.toString
}
object Message {

  type Type = String

  def typeFrom(msg: String): Message.Type = ???

  implicit def messageSer[T:TypeTag:JsonSer]: JsonSer[Message[T]] = ???

}

//We'll make use of it for sending

producer.produce(messageSer[Foo].serialize(Message(foo)))

//And we redefine the consumer

lazy val consumer = new Consumer {
    override val consume: Any => Unit = {
      case message: String =>
        Message.typeFrom(message) match {

          case "org.scalera.reflect.runtime.Bar" =>
            println("it's a bar!")
            val value = messageSer[Bar].deserialize(message).content
            println(value.att1)

          case "org.scalera.reflect.runtime.Foo" =>
            val value = messageSer[Foo].deserialize(message).content
            println("it's a foo!")
            println(value.att2)

          case _ =>
            println("it's ... something!")
        }
    }
  }

As you can see, we don’t have to try every possible serializer anymore. Instead of that, from the message type we’ve extracted from the Message wrapper we have added, we’re able to use the proper deserializer.

But it is also true that we have to add an extra case for each string that represents the message type. Wouldn’t it be nice to deserialize somehow and to have the defined case only for the already deserialized objects? (Something similar what we first tried but without trying all posible serializers). Something like:

lazy val consumer = new Consumer {
  override val consume: Any => Unit = { msg =>
    genericDeserialize(msg) match {
      case f: Foo =>
      case b: Bar =>
      case _ =>
    }
  }
}

Option 2-cool: Serializers ‘under the hood’

For achieving something similar, we have to get focused on that genericDeserialize method: Which signature should it have? Initially, something like this:

def genericDeserialize(msg: String): Any

An Any? Seriously? My fellows, at runtime, we have no idea about the type we can get. We just know that, from a String, we’ll get ‘some….thing’. The match that applies to that Any will allow us to go from something totally abstract to more concrete types.

At this point is where both reflect library and Scala compiler appear.

reflect.Toolbox

The Toolbox API allows parsing strings and getting the resulting AST (abstract syntax tree). From that AST, it is able to evaluate the expression and returns an instance of an Any as well.

For instantiating a Toolbox and use type references, we hace to add as SBT dependencies the following:

libraryDependencies ++= Seq(
  "org.scala-lang" % "scala-compiler" % "2.11.8",
  "org.scala-lang" % "scala-reflect" % "2.11.8")

For example, if we wanted to parse the "2".toInt + 4 operation,

import scala.tools.reflect.ToolBox
import scala.reflect.runtime.{universe => ru}
import ru._

//  Scala compiler tool box
val tb = ru.runtimeMirror(
  this.getClass.getClassLoader).mkToolBox()

println(ru.showRaw(tb.parse("2".toInt + 4")))

we would get the abstract syntax tree generated as a String (by using showRaw):

Apply(Select(Select(Literal(Constant("2")), TermName("toInt")), TermName("$plus")), List(Literal(Constant(4))))

If we use the toolbox for evaluating the parsed expression,

println(tb.eval(tb.parse("2".toInt + 4")))

we’ll get an Any that represents the resulting value of the sum:

6

“Da” serializer

Once we’ve seen how it generally works, we apply the same principle to our serializer, so the expression we’re going to try to interpret is similar to:

{
  import scala.reflect._;
  import spray.json._;
  import org.scalera.reflect.runtime._;
  import MySprayJsonImplicits._;
  import MyJsonSerImplicits._;

  implicitly[JsonSer[Message[$messageType]]]
}

where MySprayJsonImplicits and MyJsonSerImplicits represent the objects that contain both the Spray implicits for JsonFormat and the JsonSer implicits that we have defined before.

$messageType represents the concrete type to deserialize that we would have got by using the TypeTag (as seen before).

If we adapt it to our code, we’ll get something similar to:

object GenSer {

  import scala.tools.reflect.ToolBox
  import scala.reflect.runtime.{universe => ru}
  import ru._

  //  Scala compiler tool box
  private val tb = ru.runtimeMirror(this.getClass.getClassLoader).mkToolBox()

  def genericDeserialize(msg: String)(serContainers: Seq[AnyRef]): Any = {

    val messageType = Message.typeFrom(msg)

    val serContainersImport = serContainers.map(container =>
      "import " + container.toString.split("\\$").head + "._").mkString(";\n")

    val expr =
      s"
         |{
         |  import scala.reflect._;
         |  import spray.json._;
         |  import org.scalera.reflect.runtime._;
         |  $serContainersImport;
         |
         |  implicitly[JsonSer[Message[$messageType]]]
         |}
        ".stripMargin

    tb.eval(tb.parse(expr))
      .asInstanceOf[JsonSer[Message[Any]]]
      .deserialize(msg).content
  }

}

If you take a look, we’ve empowered the generic deserialization method notation to hold a sequence of objects to import, so we won’t make explicit which object contains the Spray implicits and which one contains our JsonSer‘s.

val serContainersImport = serContainers.map(container =>
  "import " + container.toString.split("\\$").head + "._").mkString(";\n")

It is also noteworthy that, when deserializing, we get a Message[Any]; so we’ll have to get the ‘content’ field that represents the raw value of Any type that holds the deserialized message.

The result

So finally, we can now make use of our function for generically deserialize and let our consumer code be ‘swaggy’:

lazy val consumer = new Consumer {
  override val consume: Any => Unit = { 
    case msg: String =>
      genericDeserialize(msg)(Seq(case3,Message)) match {
        case bar: Bar => println("it's a bar!")
        case foo: Foo => println("it's a foo!")
        case _ => println("it's ... something!")
      }
  }
}

Conclusions

Being able to evaluate code at runtime let us do a lot of interesting things when we want to interpret String based types. However, these kind of techniques won’t care about the so worthy and safe Scala type system, so these are tools to be used with a lil’ bit of care.

It is also kind of expensive in time terms to evaluate these expressions. I would recommend to use a type cache in this case. Something really simple like a map:

type TypeName = String
var cache: Map[TypeName, JsonSer[Message[_]]]

And when invoking the method, we’ll check if the type evidence (JsonSer) we’re looking for is already stored in the map. If so, we’ll use it, otherwise, we’ll create and store it in the cache, using it as result.

post-28553-Steve-Jobs-mind-blown-gif-HD-T-pVbd

Easy peasy…
Peace out!

Scala: Interpretación de código en runtime

En el post de hoy vamos a ver cómo generar código de Scala al vuelo: en tiempo de runtime. Hemos de ser cautos de no confundirlo con las macros de Scala, las cuales generan código en tiempo de compilación. Estas últimas hacen uso del sistema de tipos de Scala, lo cual es infinitamente más seguro que generar código en tiempo de ejecución.

House-of-cards-but-why

¿Cúando es útil hacer uso de este mecanismo entonces? Trataremos de arrojar luz siguiendo un ejemplo muy sencillo, abstrayéndonos de la verdadera implementación, la cual podéis encontrar en el Github de Scalera

El problema: “el” serializador

Supongamos un caso (no tan descabellado) en el que queramos comunicar dos servicios vía un bus de eventos (nos abstraeremos de la implementación: podría ser una cola de mensajes tipo Kafka, un stream de Akka, …).

El punto de partida es el siguiente:

Sender-receiver-schema

El productor sabe enviar y el consumidor tiene asociado un callback para cuando llegue un mensaje:

trait Producer{
  def produce(message: Any): Try[Unit]
}
trait Consumer{
  val consume: Any => Unit
}

Tanto el servicio productor como el consumidor conocen los tipos de mensajes que pueden llegar, en nuestro ejemplo serán estos:

case class Foo(att1: Int, att2: String)
case class Bar(att1: String)

Si el servidor quiere enviar un mensaje usando el bus de eventos, deberá serializarlo de alguna forma (JSON, array de bytes, XML, …) para que, al llegar al otro extremo, el consumidor realice el proceso inverso (deserialización) y obtenga el mensaje original.

…hasta aquí nada extraño.

Whydoeseverythinghavetobesocomplicated

Si tenemos un serializador para JSONs …

trait JsonSer[T] {
  def serialize(t: T): String
  def deserialize(json: String): T
}

y serializamos nuestro mensaje…

implicit val fooSerializer: JsonSer[Foo] = ???
val foo: Foo = ???
producer.send(implicitly[JsonSer[Foo]].serialize(foo))

¿Cómo sabemos qué deserializador usar cuando el consumidor reciba el mensaje?

Opción 1: Probar con todos hasta que uno encaje

En nuestro consumidor tendríamos:

lazy val consumer = new Consumer {
  override val consume: Any => Unit = {
    case message: String =>
      Seq(barSerializer, fooSerializer).flatMap { ser =>
        Try(ser.deserialize(message)).toOption
      }.headOption.fold(ifEmpty = println("Couldn't deserialize")) {
        case bar: Bar => println("it's a bar!")
        case foo: Foo => println("it's a foo!")
        case _ => println("it's ... something!")
      }
  }
}

Un poco burdo, ¿no? Si el serializador correcto es el último de 100, estaríamos probando y fallando con 99 antes de dar con el correcto (¡qué desperdicio de CPU!).

4d8

Además, también podría darse el caso de que hubiera un deserializador para otro tipo, pero que encaja y es capaz de deserializar parcial o erróneamente el mensaje.

Opción 2: Añadir el tipo al mensaje

Podríamos añadir una capa por encima al mensaje e indicar el tipo de mensaje que es. Así, al recibirlo, podríamos determinar el tipo de serializador que necesitamos usar.

MessageWrapper

Para escribir el tipo, nos apoyaremos en los TypeTag de Scala, obteniendo información sobre el tipo de mensaje contenido T.


//Wrapper for the message (and its serializer)

import scala.reflect.runtime.universe.{TypeTag, typeTag}

case class Message[T: TypeTag](content: T){
  val messageType: Message.Type = typeTag[T].tpe.toString
}
object Message {

  type Type = String

  def typeFrom(msg: String): Message.Type = ???

  implicit def messageSer[T:TypeTag:JsonSer]: JsonSer[Message[T]] = ???

}

//We'll make use of it for sending

producer.produce(messageSer[Foo].serialize(Message(foo)))

//And we redefine the consumer

lazy val consumer = new Consumer {
    override val consume: Any => Unit = {
      case message: String =>
        Message.typeFrom(message) match {

          case "org.scalera.reflect.runtime.Bar" =>
            println("it's a bar!")
            val value = messageSer[Bar].deserialize(message).content
            println(value.att1)

          case "org.scalera.reflect.runtime.Foo" =>
            val value = messageSer[Foo].deserialize(message).content
            println("it's a foo!")
            println(value.att2)

          case _ =>
            println("it's ... something!")
        }
    }
  }

Cómo podéis ver, ya no tenemos que ir probando con todos los serializadores posibles hasta que demos con uno que funcione; sino que, a partir del tipo de mensaje (extraido del envoltorio de tipo Message que hemos añadido), somos capaces de usar el deserializador adecuado.

Pero también es cierto, que tenemos que añadir un case por cada string que representa el tipo. ¿No sería mejor poder deserializar (como sea) y que el case solo esté definido sobre los objetos ya deserializados (como antes pero sin probar a lo loco con mil serializadores)? Algo de este estilo:

lazy val consumer = new Consumer {
  override val consume: Any => Unit = { msg =>
    genericDeserialize(msg) match {
      case f: Foo =>
      case b: Bar =>
      case _ =>
    }
  }
}

Opción 2-guay: Serializadores ‘under the hood’

Para conseguir algo parecido, debemos de centrarnos en ese método genericDeserialize: ¿qué signatura debería tener? Inicialmente algo del siguiente estilo:

def genericDeserialize(msg: String): Any

¿Un Any? ¿En serio? Amigos, en tiempo de runtime, no tenemos ni idea del tipo que nos puede llegar. Solo sabemos que, a partir de un String, vamos a obtener un ‘algo’. El match que se aplica sobre dicho Any nos permitirá pasar de algo totamente abstracto a tipos más concretos.

Es en este punto, donde entra en juego la librería de reflect y el compilador de scala.

reflect.Toolbox

La api de Toolbox permite parsear cadenas de texto y obtener el AST (abstract syntax tree) resultante. Del mismo modo, a partir del AST es capaz de evaluar la expresión devolviendo una instancia de un tipo determinado.

Para poder instanciar un Toolbox y usar referencias a tipos, es preciso añadir como dependencias a nuestro proyecto las librerías de scala-compiler y scala-reflect:

libraryDependencies ++= Seq(
  "org.scala-lang" % "scala-compiler" % "2.11.8",
  "org.scala-lang" % "scala-reflect" % "2.11.8")

Si por ejemplo quisiéramos parsear la operación "2".toInt + 4,

import scala.tools.reflect.ToolBox
import scala.reflect.runtime.{universe => ru}
import ru._

//  Scala compiler tool box
val tb = ru.runtimeMirror(
  this.getClass.getClassLoader).mkToolBox()

println(ru.showRaw(tb.parse(""""2".toInt + 4""")))

nos devolverá el árbol sintáctico generado como un string (usando showRaw):

Apply(Select(Select(Literal(Constant("2")), TermName("toInt")), TermName("$plus")), List(Literal(Constant(4))))

Si a partir de la expresión parseada, usamos el toolbox para evaluarla,

println(tb.eval(tb.parse(""""2".toInt + 4""")))

obtendremos un Any que representa el valor resultante de la suma:

6

“El” serializador

Una vez visto el funcionamiento general, aplicamos el mismo principio a nuestro serializador, de manera que la expresión que vamos a intentar interpretar es algo similar a:

{
  import scala.reflect._;
  import spray.json._;
  import org.scalera.reflect.runtime._;
  import MySprayJsonImplicits._;
  import MyJsonSerImplicits._;

  implicitly[JsonSer[Message[$messageType]]]
}

donde MySprayJsonImplicits y MyJsonSerImplicits representan los objetos que contienen tanto los implícitos de Spray para JsonFormat que hemos definido, como los implícitos de la type class JsonSer que hemos definido nosotros.

$messageType representa el tipo concreto a deserializar que habremos obtenido previamente usando el TypeTag (como hemos visto antes).

Si lo adaptamos a nuestro código obtendremos algo similar a:

object GenSer {

  import scala.tools.reflect.ToolBox
  import scala.reflect.runtime.{universe => ru}
  import ru._

  //  Scala compiler tool box
  private val tb = ru.runtimeMirror(this.getClass.getClassLoader).mkToolBox()

  def genericDeserialize(msg: String)(serContainers: Seq[AnyRef]): Any = {

    val messageType = Message.typeFrom(msg)

    val serContainersImport = serContainers.map(container =>
      "import " + container.toString.split("\\$").head + "._").mkString(";\n")

    val expr =
      s"""
         |{
         |  import scala.reflect._;
         |  import spray.json._;
         |  import org.scalera.reflect.runtime._;
         |  $serContainersImport;
         |
         |  implicitly[JsonSer[Message[$messageType]]]
         |}
        """.stripMargin

    tb.eval(tb.parse(expr))
      .asInstanceOf[JsonSer[Message[Any]]]
      .deserialize(msg).content
  }

}

Si os fijáis, hemos permitido en la notación del método de deserialización genérica, que se le pase una secuencia de objetos a importar, de manera que no se explicita qué objeto es el que contiene los implícitos de Spray y qué otro contiene los de nuestro JsonSer.

val serContainersImport = serContainers.map(container =>
  "import " + container.toString.split("\\$").head + "._").mkString(";\n")

También cabe destacar que, al deserializar, obtenemos un Message[Any]; por lo que posteriormente hay que obtener el campo ‘content’ que representa el valor plano de tipo Any que representa el mensaje deserializado.

El resultado

Ahora sí, podemos usar nuestra función para deserializar de manera genérica y dejar el código de nuestro consumidor de manera ‘reshulona’:

lazy val consumer = new Consumer {
  override val consume: Any => Unit = { 
    case msg: String =>
      genericDeserialize(msg)(Seq(case3,Message)) match {
        case bar: Bar => println("it's a bar!")
        case foo: Foo => println("it's a foo!")
        case _ => println("it's ... something!")
      }
  }
}

Conclusiones

Poder interpretar código en tiempo de runtime da bastante juego cuando queremos interpretar tipos en base a String’s. Ahora bien, interpretar código en runtime se pasa por el forro el sistema de tipos que tanta seguridad aporta en Scala, por lo que son herramientas a usar con algo de cuidado.

También es algo costoso en tiempo la evaluación de estas expresiones. Os recomendaría que usarais una cache de tipos. Algo tan sencillo como un mapa:

type TypeName = String
var cache: Map[TypeName, JsonSer[Message[_]]]

Y al ejecutar el método, comprobar si existe ya un JsonSer almacenado en el mapa. De ser así, usamos ese y en caso contrario, lo creamos, lo almacenamos en la caché y lo devolvemos como resultado.

post-28553-Steve-Jobs-mind-blown-gif-HD-T-pVbd

Easy peasy…
¡Agur de limón!

Shapeless: Introduction and HLists (Part 1)

My good friends from Scalera asked me if I would want to write a post for them, and they made me an offer I couldn’t refuse, “We’ll get you a beer” they said… and here I am! I can’t say no to a good beer friend. This is the very first post I’ve ever written about Scala, so please bear with me.

What is Shapeless?

Shapeless is a library created by Miles Sabin that aims to improve generic programming in Scala.

post3_en

To achieve this it makes use of a bunch of macros and type level tricks, sometimes called whitchcraft, that push the language to its limits. There is a lot to talk about Shapeless, but in this first part we are going to focus on the basics, the most popular structure… yes, you’re right, we are talking about HList`s!!!

HLists

This is probably the most known feature of Shapeless, and the best start point (IMO) to tackle it, so let’s get started introducing what an HList is.

HList stands for Heterogeneous List, i.e. a list of elements of possibly different types. That’s it, the concept is real simple, the consecuences are far more interesting. This allows us to have a list containing a String and an Int. But you might be thinking… I can do that with a regular Scala List. Well, let’s take a look:

scala> List("scalera", 3)
res0: List[Any] = List(scalera, 3)

Although it seems this is a List with two elements of different type, the fact is that from a type level point of view these two elements have the same type, Any. You can’t do anything useful with this List, in order to do so you must do some ugly castings here and there.

How do HLists solve this problem? Well, an HList stores the type of every element in the list. This way we know the type of the first element, the type of the second element, and so forth. Let’s try the same example we saw before, this time using an HList.

The syntax for creating an HList is the same as for creating a List, we just have to replace the constructor name:

scala> HList("scalera", 3)
res0: shapeless.::[String,shapeless.::[Int,shapeless.HNil]] = scalera :: 3 :: HNil

The result type is a little bit confusing, but we can use infix notation to make it cleaner, the result type then would be String :: Int :: HNil. As you can see, we have all the types stored, we are not losing any info! Furthermore, we statically know the size of the list!

So we can say that HLists are more powerful than Lists, and here is an example to prove it:

val list: List[Any] = "John Doe" :: 26 :: Nil
val hlist: String :: Int :: HNil = "John Doe" :: 26 :: HNil

list.head.toUpperCase // error: value toUpperCase is not a member of Any
hlist.head.toUpperCase // success: "JOHN DOE"

Notice you can use two different syntax in order to build HLists, similar to what you can do for regular Lists:

HList("scalera", 3) <===> "scalera" :: 3 :: HNil
List(1, 2, 3) <===> 1 :: 2 :: 3 :: Nil

Also, you can tell from the example above we have similar methods to work with HLists

val hlist = 1 :: "two" :: 3 :: "four" :: HNil
hlist.head // 1
hlist.tail // "two" :: 3 :: "four" :: HNil
hlist take 2 // 1 :: "two" :: HNil

So far we’ve seen that HLists have similarities with regular Lists, being the formers more powerful, but besides Lists, there are other Scala structures more similar to HLists. These structures are tuples and case classes.

All these three data structures(hlists, tuples and case classes) have the same shape, they can store the same information, and you can go from one to another without losing any information. Let’s see an example:

case class User(name: String, age: Int)
type UserTuple = (String, Int)
type UserHList = String :: Int :: HNil

val user: User = User("John Doe", 26)
val userTuple: UserTuple = ("John Doe", 26)
val userHList: UserHList = "John Doe" :: 26 :: HNil

def fromUserToUserHList(user: User): UserHList = ???
def fromUserHListToUser(userHList: UserHList): User = ???
...

It would be straightforward to implement these functions and I encourage you to try to do so. It’s a good exercise to get used to HLists.

Why would I use HLists then?

So, if an HList is very similar to a case class or a tuple, what’s the big deal? What can you do with an HList you can’t with a case class or tuple? Well, the big difference is that HLists are a more powerful abstraction, it abstracts over tuple arity for instance. You can’t write a method over tuples, because Tuple2 and Tuple3 are not related to each other, but you can create a method for HLists and run it with HLists of arbitrary size. This is out of the scope of this post though.

Conclusions

We’ve seen that HLists are a powerful abstraction that provides us with the advantages of both lists and tuples. This is only the tip of the iceberg though, as its power really shines when we combined with other Shapeless type class, Generic. But we’ll leave it for another post…

Shapeless: Introducción y HLists (Parte 1)

Mis buenos amigos de Scalera me preguntaron si quería escribir un post para el blog, y me hicieron una oferta que no pude rechazar, “Te invitamos a una cerveza” dijeron… ¡y aquí estoy! No puedo decir que no a una buena cerveza un buen amigo. Este es el primer post que escribo relacionado con Scala, así que tened paciencia conmigo.

¿Qué es Shapeless?

Shapeless es una librería creada por Miles Sabin, cuyo propósito es mejorar la programación genérica en Scala.

post3_sp

Para conseguirlo utiliza una serie de macros y técnicas avanzadas a nivel del sistema de tipos, que muchos simplemente llaman whitchcraft (brujería), que llevan al lenguaje al límite. Hay muchos temas que tratar cuando hablamos de Shapeless, pero en esta primera parte nos vamos a centrar en los principios básicos, la estructura más popular… sí, estás en lo cierto, ¡estoy hablando de las HLists!

HLists

Esta es probablemente la característica más famosa de Shapeless, y el mejor punto de partida (en mi opinión) para empezar, así que comencemos introduciendo qué es una HList.

El nombre HList proviene de Heterogeneous List (listas heterogeneas), en otras palabras, una lista con elementos que pueden ser de distinto tipo. Eso es todo, el concepto es muy simple, pero las consecuencias son muy interesantes. Esto nos permite tener una lista formada por un String y un Int por ejemplo. Puede que estes pensando ahora mismo… Ya puedo hacer eso con una lista normal y corriente. Bueno, vamos a ver si es verdad:

scala> List("scalera", 3)
res0: List[Any] = List(scalera, 3)

Aunque pueda parecer que esta lista tiene dos elementos de distinto tipo, la verdad es que desde el punto de vista del sistema de tipos estos dos elementos son del mismo tipo, Any. No se puede hacer nada útil con esta lista, si quisieras hacerlo tendrías que hacer algún que otro casting, con los peligros que eso conlleva…

¿Cómo solucionan este problema las HList? Pues muy sencillo, una HList guarda el tipo de todos y cada uno de los elementos que la componen. De esta manera sabemos que tipo tiene el primer elemento, el segundo, etc. Vamos a probar con el ejemplo anterior, esta vez utilizando una HList.

La sintaxis para crear una HList es la misma que para crear una List, sólo cambiaremos el nombre del constructor:

scala> HList("scalera", 3)
res0: shapeless.::[String,shapeless.::[Int,shapeless.HNil]] = scalera :: 3 :: HNil

El tipo de retorno es un poco confuso, pero podemos utilizar notación infija para simplificarlo, el tipo de retorno quedaría String :: Int :: HNil. Como puedes ver, tenemos todos los tipos guardados, ¡No perdemos ninguna información! Es más, ¡Podemos saber estáticamente el tamaño de la lista!

Así pues, podemos decir que las HLists son más potentes con las Lists estandar de Scala, y aquí hay un ejemplo para probarlo:

val list: List[Any] = "John Doe" :: 26 :: Nil
val hlist: String :: Int :: HNil = "John Doe" :: 26 :: HNil
list.head.toUpperCase // error: value toUpperCase is not a member of Any
hlist.head.toUpperCase // success: "JOHN DOE"

Como habrás podido ver, se pueden utilizar distintas sintaxis para construir una HList, al igual que para construir Lists:

HList("scalera", 3) <===> "scalera" :: 3 :: HNil
List(1, 2, 3) <===> 1 :: 2 :: 3 :: Nil

Además, puedes comprobar que muchos de los metodos definidos para Lists se pueden utilizar también para HLists.

val hlist = 1 :: "two" :: 3 :: "four" :: HNil
hlist.head // 1
hlist.tail // "two" :: 3 :: "four" :: HNil
hlist take 2 // 1 :: "two" :: HNil

Hasta ahora hemos visto que HList tiene similaridades con List, siendo la primera más potente, pero a parte de List, hay otras estructuras en Scala que son más parecidas a HList. Estas estructuras son las tuplas y las case classes.

Estas tres estructuras (hlists, tuplas y case classes) tienen la misma forma, puden guardar la misma información, y se puede ir de una a la otra sin perdida de información. Veamos un ejemplo:

case class User(name: String, age: Int)
 
type UserTuple = (String, Int)
type UserHList = String :: Int :: HNil
 
val user: User = User("John Doe", 26)
val userTuple: UserTuple = ("John Doe", 26)
val userHList: UserHList = "John Doe" :: 26 :: HNil
 
def fromUserToUserHList(user: User): UserHList = ???
def fromUserHListToUser(userHList: UserHList): User = ???
 
//...

Sería bastante directo implementar estas funciones, y te animo a que lo intentes. Es un buen ejercicio para tomar contacto con las HLists.

¿Por qué querría usar HList entonces?

Si una HList es muy parecida a una case class o una tupla, ¿Por qué son importantes? ¿Qué puedes hacer con una HList que no puedas hacer con una case class o una tupla? Pues bien, la gran diferencia es que HList es una abstracción más potente, abstrae sobre la aridad de las tuplas por ejemplo. No puedes escribir una función sobre tuplas, por que Tuple2 y Tuple3 no están relacionadas, sin embargo, puedes crear una función sobre HList y ejecutarla con HLists de tamaño arbitrario. Aunque esto queda fuera del ámbito de este post.

Conclusiones

Hemos visto que HList es una abstracción muy potente que nos brinda las ventajas de las listas y las tuplas al mismo tiempo. Sin embargo, esto es solo la punta del iceberg, ya que su verdadero potencial sale a relucir cuando las combinamos con otra typeclass de Shapeless, Generic. Pero esto lo dejaremos para otro post…