Abstract alge… what? Functors and cucumbers

Category mania! Are we paid for speaking about it? I’m afraid we don’t. Would we like to? Very likely. In previous posts we spoke about monoids and monads, but now it’s functor’s time. Functors are everywhere and we can prove it. Before reviewing its different kinds, let’s focus on key concepts.

What’s a morphism?

It’s just a transformation between two structures, a change between spaces, a mutation.
In Scala? A simple function:

val f: A => B

What’s a functor?

Quick and simple answer: the behavior that defines, for a certain F[A] type constructor, the map method.

trait Functor[F[_]]{
  def map[A, B](fA: F[A])(f: A => B): F[B]
}

There are functors for List, Option, Future, Try, …

object ListFunctor extends Functor[List]{
  def map[A, B](fA: List[A])(f: A => B): List[B] =
    fA match {
      case Nil => Nil
      case (head :: rest) =>
        f(head) +: this.map(rest)(f)
    }
}
object OptionFunctor extends Functor[Option]{
  def map[A, B](fA: Option[A])(f: A => B): Option[B] =
    fA match {
      case None => None
      case Option(a) => Option(f(a))
    }
}
//...

Actually, apart from the famous map method, they should fit into a couple of properties:

  • Identity morphism: F[Id[A]] = Id[F[A]]. For example, being identity the identity function defined in Scala, Option(identity(1)) == identity(Option(1))
  • Morphism composition: If we have two morphisms f: A => B and g: B => C, it must be checked that F[f o g] = F[f] o F[g]. It’s not as complicated as it seems:
    val f: Int => String = _.toString
    val g: String => Boolean = _.length > 1
    val l: List[Int] = List(1,20,3)
    l.map(f).map(g) ==
      l.map(f andThen g) ==
      l.map(n => g(f(n)))
    //List(false, true, false)
    

Even though, we can describe functors (in the context of a F[_] production chain) as the instructions for transforming some given A input value into some B output value within the same F[_] production chain, by using a morphism (a transformation function) A => B.

Functor types

Ordinary functors are also known as co-variant functors (in order to differentiate itself from contravariant and invariant functors). Let’s se what makes the difference between these different kinds of functor:

Contravariant Functor

Formal definition says that a F[_] functor is contravariant if, instead of having the map method, it has a contramap method defined:

trait Contravariant[F[_]]{
  def contramap[A, B](fA: F[A])(f: B => A): F[B]
}

That’s it: if it exists a function B => A, the contramap method defines F[A] => F[B].

…ok, let’s stop being so hardcore. We’ll illustrate it with an example.

Imagine a type class that knows how to compare certain type elements:

type Comparison = Int
val Greater = 1
val Equal = 0
val Lower = -1

trait Comparator[T]{
  def compare(t1: T, t2: T): Comparison
}

Imagine as well, that I know how to compare integer numbers (and here comes the tricky tip):

if I know how to compare integers and I know how to convert ‘cucumbers’ into integer numbers, I do know how to compare ‘cucumbers’

object ComparatorF extends Contravariant[Comparator]{
  def contramap[A, B]
    (fA: Comparator[A])
    (f: B => A): Comparator[B] =
    new Comparator[B]{
      def compare(t1: B, t2: B): Comparison =
        fA.compare(f(t1), f(t2))
    }
}

And now, the so-expected example about how to generate a contravariant functor for cucumbers:

trait Cucumber
val intC: Comparator[Int] = ???
val cucumberToInt: Cucumber => Int = ???
val cucumberC: Comparator[Cucumber] =
  ComparatorF.contramap(intC)(cucumberToInt)

cucumberC.compare(new Cucumber{}, new Cucumber{})

…sublime…

Invariant functors

Invariant functors for F[_] are determined by the imap method:

trait Invariant[F[_]] {
  def imap[A, B](fA: F[A])(f: A => B)(g: B => A): F[B]
}

…once again the doubt is killing you: I know and I beg you for some time to explain properly. Let’s see some example.

Imagine a type class that knows how to store stuff in some database (MongoDB, for example):

case class ObjectId(hash: String)

trait DataStorage[T]{
  def store(t: T): ObjectId
  def get(id: ObjectId): T
}

If we forget about possible effects (exceptions, timeouts and so) for not messing up the example, we could define an invariant functor for DataStorage that allows storing any-kind elements:

object DataStorageF extends Invariant[DataStorage]{
  def invariant[A, B]
    (fA: DataStorage[A])
    (f: A => B)
    (g: B => A): DataStorage[B] = {

    new DataStorage[B]{
      def store(b: B): Option[ObjectId] =
        fA.store(g(b))
      def get(id: ObjectId): B =
        f(fA.get(id))
    }
  }
}

So…

If I know how to store integers and I know how to convert integers into cucumbers (and the other way round), I know how to store cucumbers!

val intDS: DataStorage[Int] = ???
val cucumberToInt: Cucumber => Int = ???
val intToCucumber: Int => Cucumber = ???
val cucumberDS: DataStorage[Cucumber] =
  DataStorageF
    .imap(intDS)(intToCucumber)(cucumberToInt)

val id = cucumberDS.store(new Cucumber{})
val originalCucumber = cucumberDS.get(id)

We couldn’t say this is all, but it may be a good introduction to the wonderful functor world. See you in the next poxt. ‘Like’ and share if you like storing cucumbers.
Peace out!

Sources:
[1] Advanced Scala with Cats – Noel Welsh & Dave Gurnell
[2] Variance and functors – Typelevel blog

Teoría de Cate-movidas: Functores y pepinos

Que manía con las categorías. ¿Nos pagan por hablar de ello? No. ¿Nos gustaría que lo hiciesen? Es muy probable. En otras ocasiones hablábamos de monoides y mónadas, pero esta vez le toca el turno a los functores. Los functores están por todas partes y no son los padres: podemos demostrarlo. Antes de detallar sus variantes, centrémonos en los conceptos clave.

¿Qué es un morfismo?

Una transformación entre dos espacios, un cambio, una mutación.
¿En Scala? Una función:

val f: A => B

¿Qué es un functor?

Respuesta corta y simple: el comportamiento que define, para un constructor de tipos F[A], el método ‘map’:

trait Functor[F[_]]{
  def map[A, B](fA: F[A])(f: A => B): F[B]
}

Existen functores para List, Option, Future, Try, …

object ListFunctor extends Functor[List]{
  def map[A, B](fA: List[A])(f: A => B): List[B] =
    fA match {
      case Nil => Nil
      case (head :: rest) =>
        f(head) +: this.map(rest)(f)
    }
}
object OptionFunctor extends Functor[Option]{
  def map[A, B](fA: Option[A])(f: A => B): Option[B] =
    fA match {
      case None => None
      case Option(a) => Option(f(a))
    }
}
//...

En realidad, a parte del famoso método map, deben cumplir un par de propiedades más:

  • Morfismo identidad: F[Id[A]] = Id[F[A]]. Por poner un ejemplo, siendo identity la función identidad definida en Scala, Option(identity(1)) == identity(Option(1))
  • Composición de morfismos: Si tenemos dos morfismos f: A => B y g: B => C, se debe cumplir que F[f o g] = F[f] o F[g]. Que no es tan complicado si lo vemos con
    val f: Int => String = _.toString
    val g: String => Boolean = _.length > 1
    val l: List[Int] = List(1,20,3)
    l.map(f).map(g) ==
      l.map(f andThen g) ==
      l.map(n => g(f(n)))
    //List(false, true, false)
    

Pero a grandes rasgos, podemos pensar en los functores ordinarios como la descripción de como, en una cadena de producción o montaje F[_], se permite realizar transformaciones de manera que, para argumento un A, y usando un morfismo (una función de transformación) A => B, obtenemos un B dentro de la misma cadena de producción F[_]

Clasificación de functores

Los functores ordinarios también son denóminados co-variantes (para ser diferenciados de los contravariantes y de los invariantes). Veamos a continuación qué caracteriza a estos otros tipos de functor:

Functor contravariante

La definición formal dice que un functor para F[_] es contravariante si, en vez el método map, tiene definida la operación contramap:

trait Contravariant[F[_]]{
  def contramap[A, B](fA: F[A])(f: B => A): F[B]
}

Esto es, si existe una función B => A, el functor define F[A] => F[B].

…venga va, sin ser hardcore, ponemos un ejemplo.

Imagina una type class que sabe comparar elementos de un cierto tipo:

type Comparison = Int
val Greater = 1
val Equal = 0
val Lower = -1

trait Comparator[T]{
  def compare(t1: T, t2: T): Comparison
}

Imagina del mismo modo que yo dispongo de un comparador de números enteros (y aquí viene el quid de la cuestión):

si yo se comparar enteros y se como transformar ‘pepinos’ a enteros, ya se como comparar ‘pepinos’

object ComparatorF extends Contravariant[Comparator]{
  def contramap[A, B]
    (fA: Comparator[A])
    (f: B => A): Comparator[B] =
    new Comparator[B]{
      def compare(t1: B, t2: B): Comparison =
        fA.compare(f(t1), f(t2))
    }
}

Y ahora el archi-esperado ejemplo de generar un functor contravariante para pepinos:

trait Cucumber
val intC: Comparator[Int] = ???
val cucumberToInt: Cucumber => Int = ???
val cucumberC: Comparator[Cucumber] =
  ComparatorF.contramap(intC)(cucumberToInt)

cucumberC.compare(new Cucumber{}, new Cucumber{})

…sublime…

Functor invariante

Los functores invariantes para F[_] se caracterizan por tener un método denominado imap como sigue:

trait Invariant[F[_]] {
  def imap[A, B](fA: F[A])(f: A => B)(g: B => A): F[B]
}

…otra vez en el valle de la duda después de esta definición, lo se y pido paciencia. Se ve mucho mejor con otro ejemplo.

Imagina una type class que sabe almacenar objetos en una base de datos (MongoDB, por ejemplo):

case class ObjectId(hash: String)

trait DataStorage[T]{
  def store(t: T): ObjectId
  def get(id: ObjectId): T
}

Olvidándonos de los posibles efectos (excepciones, timeouts, etc) para no liar el ejemplo, podemos definir un functor invariante para DataStorage que permita almacenar cualquier elemento:

object DataStorageF extends Invariant[DataStorage]{
  def invariant[A, B]
    (fA: DataStorage[A])
    (f: A => B)
    (g: B => A): DataStorage[B] = {

    new DataStorage[B]{
      def store(b: B): Option[ObjectId] =
        fA.store(g(b))
      def get(id: ObjectId): B =
        f(fA.get(id))
    }
  }
}

Por lo tanto…

Si yo se como almacenar enteros y se transformar pepinos a enteros (y viceversa),
¡se cómo almacenar pepinos!

val intDS: DataStorage[Int] = ???
val cucumberToInt: Cucumber => Int = ???
val intToCucumber: Int => Cucumber = ???
val cucumberDS: DataStorage[Cucumber] =
  DataStorageF
    .imap(intDS)(intToCucumber)(cucumberToInt)

val id = cucumberDS.store(new Cucumber{})
val originalCucumber = cucumberDS.get(id)

No podemos decir que esto sea todo, pero sí puede ser una buena introducción al maravilloso mundo de los functores. Nos vemos en el próximo post. ‘Like’ y comparte si te gusta almacenar pepinos.
¡Agur de limón!

Fuentes:
[1] Advanced Scala with Cats – Noel Welsh & Dave Gurnell
[2] Variance and functors – Typelevel blog

Transforming the Future

A few weeks ago we talked about the type Future and its use to create asynchronous calls.

We saw how to work with blocking calls to obtain the value of the future. We also used callbacks in order to obtain the result of the future asynchronously. However, there are some issues that were left unsaid. And by that I’m referring to transforming the Future without blocking the execution.

Future transformations

In order to transform futures, as with other Scala basic types, mainly two methods are used: map and flatmap.

Map method

Map method allows us to change the content of a future by applying a function. For instance, if we have a method to get the first million prime numbers but we want to transform it to return just the first hundred ones, we can apply the map method in the following way:

def getFirstMillionOfPrimes(): Future[List[Int]] = ???

getFirstMillionOfPrimes().map(
  (list: List[Int]) => list.take(100)
)

This way we will be transforming the inside of the future without breaking the asynchrony.

pi2band2bi

FlatMap method

On the other hand, the flatMap method allows us to apply a function to the content of the future and returning a future in turn. After that, a flatten operation is applied to convert the Future[Future[A]] into a simple Future[A]. What the f…? Better explained with an example.

Imagine we want to concatenate the first million prime numbers in a string. To do so, we’ll use a new method:

def concatenate(l: List[Int]): Future[String] = ???

and now we perform a  flatMap

getFirstMillionOfPrimes().flatMap(
  (list: List[Int]) => concatenate(list)
) //Future[String]

And how can we do all this in a more simple way?

Easy question. For comprehension to the rescue! With a spoonful of syntactic sugar we can write a much more readable code.

for {
  primes <- getFirstMillionOfPrimes()
  primesString <- concatenate(primes)
} yield primesString

This way, the concatenation operation won’t be applied until the prime numbers are obtained with the method getFirstMillionPrimes.

This allows us to keep an order when composing asynchronous calls. Besides, if the first asynchronous call fails, the second won’t be conducted.

And that’s all for today. Now you know how to change the future. What a shame not to be able to change the past 😦

doesnt-go-into-girls-shower

See you soon!

Transformando el futuro

Hace ya unas cuantas semanas estuvimos hablando sobre el tipo Future para crear llamadas asíncronas.
Vimos como trabajar con llamadas bloqueantes para obtener el valor del futuro. También utilizamos callbacks para obtener el resultado del futuro de forma asíncrona. Sin embargo se quedaron algunos puntos en el tintero. Me refiero a transformar los Future sin bloquear la ejecución.

Transformaciones de Futuros

Para transformar los futuros, al igual que con otros tipos básicos de Scala, se usa principalmente los métodos map y flatmap.

El método map

El método map nos permite cambiar el contenido de un futuro aplicando una función. Por ejemplo, si tenemos un método que nos permite obtener el primer millón de números primos, pero queremos transformarlo para que solo nos devuelva los cien primeros, podemos aplicar el método map de la siguiente manera:

def getFirstMillionOfPrimes(): Future[List[Int]] = ???

getFirstMillionOfPrimes().map(
  (list: List[Int]) => list.take(100)
)

De esta forma estamos transformando el interior del Futuro sin romper la asincronía.

pi2band2bi

El método flatMap

Por otro lado, el método flatMap nos permite aplicar una función al contenido del futuro, que devuelve un Futuro a su vez. Después, aplica una operación de flatten para convertir el Future[Future[A]] en un simple Future[A]. What the f…? Se entiende mejor con un ejemplo.

Imaginemos que queremos concatenar en una cadena de texto el primer millón de números primos. Para ello utilizamos un nuevo método:

def concatenate(l: List[Int]): Future[String] = ???

y ahora realizamos un flatMap

getFirstMillionOfPrimes().flatMap(
  (list: List[Int]) => concatenate(list)
) //Future[String]

¿Y como podemos componer hacer todo esto de una forma más sencilla?

Pues muy sencillo. ¡For comprehenssion al rescate! Aplicando un poco de syntactic sugar podemos tener un código mucho más legible.
Basta con hacer lo siguiente:

for {
  primes <- getFirstMillionOfPrimes()
  primesString <- concatenate(primes)
} yield primesString

De esta manera, no se aplicará la operación de concatenación hasta que no se hayan obtenido los números primos mediante el método getFirstMillionPrimes.
Esta permite guardar un cierto orden a la hora de hacer composición de llamadas asíncronas. Además, si la primera llamada asíncrona falla, no se efectuará la segunda.

Y esto es todo por hoy. Ahora ya sabes como cambiar el Futuro. Una lástima no poder cambiar el pasado 😦

doesnt-go-into-girls-shower

¡Hasta la próxima!

Spark: RDD basic operations

After more than a year publishing Scalera posts, I think the time for scratching one of the most important tools in Scala ecosystem has arrived. Of course, I’m talking about Spark.

Spark is a framework that allows parallelizing collections and their process. This process might be batch map-reduce tasks over different data sources (HDFS, Cassandra, ElasticSearch, …), streaming process of certain data flows (Kafka, Flume, …); or perform different data sources queries in some unified way with a query language like SQL. But that’s too much rock&roll. We’ll cover today the basic data type that is used in Spark: RDD.

What’s an RDD?

The RDD type (Resilient Distributed Dataset) looks like many other Scala collections. However, it’s important to get to know some of its main features:

  • It’s a distributed collection. This means that it’s partitioned among the different Spark nodes (known as workers).
  • They’re immutable: when you apply a transformation over an RDD, we’re actually creating a new one.
  • It’s lazily evaluated. With RDD’s, we’re just defining the information flow, but it won’t be evaluated at its definition moment, but at the moment when you apply an action over the RDD.funny-meme-super-lazy1

Besides, it’s good to know that you can perform two different type of operations on an RDD: transformations and actions.

Great, but how do I create them?

There are several ways to do so:

  • Parallelizing an in-memory collection, like a list of values. For doing so, we’ll use the parallelize method of the SparkContext.
    val newRDD: RDD[Int] = 
      sc.parallelize(List(1, 2, 3, 4))
    
  • From some data source using, for example, the textFile function of the SparkContext.
    val newRDD: RDD[Int] = 
      sc.textFile("myValues.txt")
    
  • Transforming an RDD by applying a transformation in order to create a new RDD from another one.
    val newRDD: RDD[String] = 
      intValues.map(_.toString)
    

What about that transformations stuff …?

Transformations define how the information flow will change by generating a new RDD. Some of these transformations are:

  • map: applies a function for transforming each collection element:
    intValues.map(_.toString) // RDD[String]
    
  • filter: select the subset of elements that match certaing boolean expression:
    intValues.filter(_.isOdd)// RDD[Int]
    
  • flatMap: apart from applying the map function, it flattens the returning collection:
    textFile.map(_.split(" ")) //RDD[Array[String]] but ...
    textFile.flatMap(_.split(" ")) //RDD[String]
    

You spoke about actions, didn’t you?

Actions will allow us to evaluate the RDD and return some result. This way, the whole defined data flow that represents the RDD is evaluated. Any example? Some of them:

  • count: it returns the total amount of elements:
    sc.parallelize(List(1, 2, 3, 4)).count //4
    
  • collect: it returns the WHOLE collection inside an in-memory array:
    sc.parallelize(List(1, 2, 3, 4)).collect // Array(1, 2, 3, 4)
    

    So, beware! If the RDD size doesn’t fit into the driver’s assigned memory, the program will crash.

  • saveAsTextFile: it pipes the collection to some text file:
    intValues.saveAsTextFile("results.txt")
    

 

So how can I apply all of these with Spark? We’ll find out the way in a week, with a practical case that will join both Twitter and Spark Streaming functionalities for performing some basic analytics in an easy, simple, g-rated way. So we’ll be able to make profit of it and feel both powerful and geek at the same time.

aad69873-6bcc-4a0d-84eb-abe375f34c6c

For comprehension and importance of beauty

There are many existential questions in the modern world we live in. But there is one question that surpasses them all. What does a for comprehension really do?

The answer to this intricate matter is… nothing new. For comprehension structures are just syntactic sugar. Underneath, and depending on how the block is built, there will be calls to map, flatMap, withFilter or foreach.

For comprehension structures will have two key parts:

  • The for block, by which nested queries are performed
  • The yield block, where previous queries are grouped and treated

In order to understand these concepts, let’s take a look at a small example:

for {
  x <- List(1, 2, 3)
  y <- List(true, false) 
} yield (x, y)

The result of this expression is the combination in tuples of the first list with the second:

List(
  (1, true),
  (1, false),
  (2, true),
  (2, false),
  (3, true),
  (3, false)
)

What this for comprehension is really doing is performing the following operations:

List(1, 2, 3).flatMap(x =>
  List(true, false).map( y => (x, y))
)

As can be seen, with for blocks everything becomes much more readable. All that needs to be done is to join several flatMaps to conclude with a map. Let’s see another example:

for {
  x <- List(1, 2)
  y <- List(true, false)
  z <- List("a", "b") 
} yield (x, y, z)

which is equivalent to:

List(1, 2, 3).flatMap(x =>
  List(true, false).flatMap( y =>
    List("a", "b").map(z => (x, y, z))
  )
)

and the result produced is:

List(
  (1, true, "a"),
  (1, true, "b"),
  (1, false, "a"),
  (1, false, "b"),
  (2, true, "a"),
  (2, true, "b"),
  (2, false, "a"),
  (2, false, "b")
)

Moreover, we can also apply filters by using embedded if statements:

for {
  x <- List(1, 2, 3, 4, 5, 6, 7)
  if x < 3
  y <- List("a", "b") 
} yield (x, y)

//result: List((1,a), (1,b), (2,a), (2,b))

whose equivalent is:

List(1, 2, 3, 4, 5, 6, 7)
  .withFilter(_ < 3)
  .flatMap( x =>
    List("a", "b").map(
      y => (x, y)
    )
  )

It may happen that not only we want to create a new collection, but also we want some operations to be applied to each one of the elements. In such a situation, we will not use the yield block. This will change the translation as we don’t want to make changes to a collection and thus, instead of being a map method, it will be the foreach method. This makes perfect sense since, in this case, we only want to perform actions for each generated event and the type of the final result will be Unit.

for {
  x <- List(1, 2, 3)
  y <- List(true, false)
} println(s"($x , $y)") 

List(1, 2, 3).foreach(x =>
  List(true, false).foreach( y =>
    println(s"($x , $y)")
  )
)

After all these examples, we hope that you use the for comprehension to make your code more readable whenever possible. Although we all know that the magic lies within.

35dunb

For comprehension y la importancia de la belleza

Existen multitud de dudas existenciales en el mundo moderno en el que vivimos. Pero hay una duda que supera a todas ellas. ¿Qué hace realmente una for comprehension?

La respuesta a esta intrincada cuestión es….nada nuevo. Las estructuras for comprehension son simplemente syntactic sugar. Por debajo, en función de como construyamos el bloque, habrá llamadas a map, flatmap, withFilter o foreach.

Las for comprehension contarán con dos partes clave:

  • El bloque for mediante el cual se realizan consultas anidadas
  • El bloque yield donde se agrupan y tratan las consultas realizadas anteriormente

Para entender estos conceptos vamos a ver un pequeño ejemplo:

for {
  x <- List(1, 2, 3)
  y <- List(true, false) 
} yield (x, y)

El resultado de esta expresión será la combinación mediante tuplas de la primera lista con la segunda:

List(
  (1, true),
  (1, false),
  (2, true),
  (2, false),
  (3, true),
  (3, false)
)

Esta for comprehension realmente realizará las siguientes acciones:

List(1, 2, 3).flatMap(x =>
  List(true, false).map( y => (x, y))
)

Como se puede observar, mediante los bloques for todo se vuelve mucho más legible. Lo único que hacemos es hilar varios flatmap para culminar con un map. Vamos a ver otro ejemplo:

for {
  x <- List(1, 2)
  y <- List(true, false)
  z <- List("a", "b") 
} yield (x, y, z)

cuyo equivalente será:

List(1, 2).flatMap(x =>
  List(true, false).flatMap( y =>
    List("a", "b").map(z => (x, y, z))
  )
)

y el resultado producido será:

List(
  (1, true, "a"),
  (1, true, "b"),
  (1, false, "a"),
  (1, false, "b"),
  (2, true, "a"),
  (2, true, "b"),
  (2, false, "a"),
  (2, false, "b")
)

Además, también podemos utilizar filtros mediante sentencias if embebidas:

for {
  x <- List(1, 2, 3, 4, 5, 6, 7)
  if x < 3
  y <- List("a", "b") 
} yield (x, y)

//result: List((1,a), (1,b), (2,a), (2,b))

cuyo equivalente es:

List(1, 2, 3, 4, 5, 6, 7).withFilter(_ < 3).flatMap( x => 
  List("a", "b").map(
    y => (x, y)
  )
)

Existe la posibilidad de que no solo queramos crear una nueva colección, sino realizar una acción con cada uno de los elementos. En ese caso no utilizaremos el bloque yield. Esto provocará que la traducción, al no querer realizar transformaciones a una colección, en vez de ser un método map, sea mediante el método foreach. Tiene bastante sentido ya que en este caso solo queremos realizar acciones por cada evento generado y el resultado final será de tipo Unit.

for {
  x <- List(1, 2, 3)
  y <- List(true, false) } println(s"($x , $y)") List(1, 2, 3).foreach(x =>
  List(true, false).foreach( y => 
    println(s"($x , $y)")
  )
)

Después de todos estos ejemplos, esperamos que utilicéis las for comprehension para hacer vuestro código más legible siempre que sea posible. Aunque todos sepamos que la magia se crea en el interior.

35dunb

Traversable ops – Map + Flatten = Flatmap *

One of the most frequently asked questions and that usually leads to confusion among those that are new to Scala is:

What’s the difference between map and flatMap?

Map

Map function is the prime converter function. It transforms the elements that compose the Traversable into other elements of the same or different type. Its notation is, for a given T[A]:

def map[B](f: A => B):T[B]

If we put an example,

val myList: List[Int] = List(1,2,3)
val anotherList: List[String] =
  myList.map((n:Int) => n.toString) //List("1","2","3")

we have that, given a list of integers, the transformation function (n:Int) => n.toString is applied to every member in the list, generating a new list whose type is the one that results from the transformation function, that is, String type.

Though this is not unique for map function, we should remember that there are shorter ways to define the transformation function:

val f: Int => String = (n: Int) => n.toString
val f: Int => String = n => n.toString
val f: Int => String = _.toString

flatMap

Well, map is now ok, understood, but then… what the hell is flatMap? In order to understand it, we need to make a short detour and get to know our helping function flatten.

flatten

This operation, with the following signature:

def flatten[B](implicit asTraversable: (A) => GenTraversableOnce[B]): Traversable[B]

allows us to flatten a traversable of traversables. That is to say, we flatten the collections that compose a collection. For instance:

val l: List[List[Int]] = List(List(1,2,3),List(),List(4),List(5))
require(l.flatten==List(1,2,3,4,5)

The contained type doesn’t have to be the same as the container’s List[List[Int]]). It also applies to some other really interesting traversables, such as Option[T]:

val l: List[Option[Int]] = List(Option(1),None,None,Option(2),Option(3))
require(l.flatten==List(1,2,3)

flatMap (now seriously…)

Then, what is a flatMap? By now you might have gotten an idea 😉
It’s just that, the application of a map operation followed by a flatten.

Let’s put a practical example:

We have a function that gets a JSON object and a function that deserializes it, converting it into a map. It is possible that some fields in the JSON object have null value. How can we return a list with the non-null values?

A first approach could be…

type KeyValueMap = Map[String,String]
type ValueList = List[String]
def fieldValues(obj: String, fieldDeser: String => KeyValueMap): ValueList = {
  fieldDeser(obj).filter{
    case (key,value) => value != null
  }.values.toList
}

But, as David said, using nulls is not an option, and instead of that, we could write

type KeyValueMap = Map[String,String]
type ValueList = List[String]
def fieldValues(obj: String, fieldDeser: String => KeyValueMap): ValueList = {
  fieldDeser(obj).values.flatMap(Option(_)).toList
}

What has happened here? In detail, if we do the same operation but in two steps and simulate the input, we have tha

val it: List[String] =
  List("value1",null,"value3")//Simulating 'fieldDeser(ob).values'
val mapped: Iterable[Option[String]] =
  it.map(Option(_)) //Remember Option(null)==None
require(mapped==List(Option("value1"),None,Option("value3")))
require(mapped.flatten==List("value1","value3"))

Now, I think we’ve finally answered to the million-dollar question.

1zltyts

Traversable ops – Map + Flatten = Flatmap

Una de las dudas más comunes, y que, por lo general, suele llevar a confusión a la gente que se inicia en Scala es:

¿Qué diferencia hay entre map y flatMap?

Map

La función map es el conversor por excelencia. Se encarga de transformar los elementos que componen el Traversable en otros elementos del mismo o distinto tipo.
Su notación es, para un T[A] :

def map[B](f: A => B):T[B]

Si lo vemos con un ejemplo,

val myList: List[Int] = List(1,2,3)
val anotherList: List[String] =
  myList.map((n:Int) => n.toString) //List("1","2","3")

tenemos que dada una lista de enteros, se aplica la función de transformación (n:Int) => n.toString a cada uno de los miembros de la lista, generando una nueva lista pero del tipo resultante de la función de transformación, es decir, de tipo String.

Aunque no atañe exclusivamente a la función map, cabe recordar que hay otras formas abreviadas para definir la función de transformación:

val f: Int => String = (n: Int) => n.toString
val f: Int => String = n => n.toString
val f: Int => String = _.toString

flatMap

Vale, el map es asumible, se puede entender, pero entonces….¿qué demonios es un flatMap? Para entenderlo es necesario que nos desviemos un tanto para conocer a nuestra función amiga flatten.

flatten

Esta operación, con la siguiente signatura:

def flatten[B](implicit asTraversable: (A) => GenTraversableOnce[B]): Traversable[B]

nos permite aplanar un traversable de traversables. Es decir, aplanamos las colecciones que componen esta colección. Por ejemplo:

val l: List[List[Int]] = List(List(1,2,3),List(),List(4),List(5))
require(l.flatten==List(1,2,3,4,5)

El tipo contenido no tiene por qué ser del mismo que el tipo contenedor (List[List[Int]]), también aplica sobre otros traversables muy interesantes, como el tipo Option[T]:

val l: List[Option[Int]] = List(Option(1),None,None,Option(2),Option(3))
require(l.flatten==List(1,2,3)

flatMap (ahora enserio…)

Entonces, ¿qué es un flatMap? Ahora os podréis hacer una idea 😉
Es justamente eso, aplicar una operación de map y posteriormente una de flatten.

Pongamos un ejemplo práctico:

Tenemos una función que recibe un cierto objeto en JSON y una función que lo deserializa convirtiéndolo en un mapa. Es posible, que en el objeto JSON vengan algunos campos con valor null. ¿Como devolvemos una lista con los valores obviando los que son nulos?

Una primera aproximación, podría ser…

type KeyValueMap = Map[String,String]
type ValueList = List[String]
def fieldValues(obj: String, fieldDeser: String => KeyValueMap): ValueList = {
  fieldDeser(obj).filter{
    case (key,value) => value != null
  }.values.toList
}

Pero como dijo David, usar nulls no es una Option, de manera que podríamos cambiarlo por

type KeyValueMap = Map[String,String]
type ValueList = List[String]
def fieldValues(obj: String, fieldDeser: String => KeyValueMap): ValueList = {
  fieldDeser(obj).values.flatMap(Option(_)).toList
}

¿Qué ha pasado aquí? Para verlo en detalle, si la misma operación la realizamos en dos pasos simulando el input, tenemos que:

val it: List[String] =
  List("value1",null,"value3")//Simulating 'fieldDeser(ob).values'
val mapped: Iterable[Option[String]] =
  it.map(Option(_)) //Remember Option(null)==None
require(mapped==List(Option("value1"),None,Option("value3")))
require(mapped.flatten==List("value1","value3"))

Ahora sí, creo que respondimos a la pregunta del millón

1zltyts