scala de rápidez
- Una introducción rápida de sintáxis
- Algunas estructuras comunes y sus usos: List, Option y Future
- Unos pocos ejemplos de refactors de código que usan la librería estándar para escribir ménos código
- Nada del "dogma" de programación funcional
- Algunas cosas aquí mencionadas tienen mejores explicaciones en otros lados.
cosas de sintáxis
Funciones
- Definir una función:
def dobleLongitud(x: String): Int = {
2 * x.length
}
- Especificar el tipo de retorno en una función es opcional, pero por mantenibilidad se sugiere ponerlo.
- La última expresión de una función es su retorno
Valores y Variables
- Definir una variable:
val x = 7
- Cuando algo se define como un valor no se puede modificar (reasignar) después. Por lo tanto lo siguiente no compila:
val x = 7
x = 3 // <- No Compila
- En cambio si algo se define como una variable entonces se puede reasignar libremente:
var x = 7
x = x + 3
Valores y Variables
- Nota importante: que algo se haya definido como un valor no quiere decir que no se pueda modificar su estado INTERNO. Por ejemplo:
val p = new Persona(ciudad = "Bogotá")
p.ciudad = "Medellín" // <- esto SI compila
p = new Persona(ciudad = "Cali" ) // <- esto NO compila
Lambdas
- Una forma de definir funciones anónimas:
List(1,2,3).map( x => x*2) == List(2,4,6)
List(1,2,3).map(_*2) == List(2,4,6)
- Cuando el parámetro es una tupla la función puede "desestructurarla":
List((1,2),(3,4),(5,6)).map( t => t._1 + t._2 )
List((1,2),(3,4),(5,6)).map{ t =>
val a = t._1
val b = t._2
a + b
}
List((1,2),(3,4),(5,6)).map{ case (a,b) => a+b }
// <- Los corchetes importan cuando se usa 'case'
List((1,2),(3,4),(5,6)).map( case (a,b) => a+b )
// <- No compila
Clases
- Definir una clase es muy parecido a Java, pero se puede definir una clase sin métodos (solo datos):
class Persona(tipoId: String, numeroId: Int, ciudad: String)
- Al definir los campos de una clase se define también su constructor por defecto, pero también se pueden definir otros constructores (que son extensiones del que es por defecto):
class Persona(tipoId: String, numeroId: Int, ciudad: String) {
def this(numeroId: Int) = {
this("CC", numeroId, "Bogotá")
}
}
Clases
class Persona(var tipoId: String, val numeroId: Int, ciudad: String)
Objetos
object SimpleObject{
val param1 : Int = 10
var param2 : String = "Yes"
def method1 = "Method 1"
def sum(a:Int, b:Int) = a + b
}
object EmptySet extends IntSet {
def contains(x: Int): Boolean = false
def incl(x: Int): IntSet = new NonEmptySet(x, EmptySet, EmptySet)
}
Case Classes
case class Persona(tipoId: String, numeroId: Int, ciudad: String, edad: Int)
val p1 = new Persona("CC",123456,"Bogotá", 26)
p1.toString == "Persona(CC,123456,Bogotá,26)"
val p1 = new Persona("CC",123456,"Bogotá", 26) // <- Esto compila normalmente
val p2 = Persona("CC",6543221,"Cali",22) // <- Esto también compila
val p2 = p1.copy(ciudad = "Medellín") //Deja p1 intacto, crea p2
Case Classes
val p = Persona("CC",6543221,"Cali",29)
def puedeConsumirAlcohol(p: Persona) = {
p match {
case Persona("CC", numeroIdentificacion, _, edad) if edad >= 18 && edad <= 21 => "Si puede tomar en la mayoría de paises"
case Persona(_, numeroIdentificacion, _, edad) if edad >= 21 => "Definitivamente puede tomar"
case _ => "NOOOOOOO"
}
}
Traits
- Son parecidas a las interfaces en Java
- Pueden tener funciones sin implementar y otras implementadas.
- Sirven para reutilizar comportamiento (funciones), a diferencia de la extensión de clases que sirven para reutilizar implementación.
trait Ordered[A] {
def compare(that: A): Int
def < (that: A): Boolean = (this compare that) < 0
def > (that: A): Boolean = (this compare that) > 0
def <= (that: A): Boolean = (this compare that) <= 0
def >= (that: A): Boolean = (this compare that) >= 0
def compareTo(that: A): Int = compare(that)
}
Listas
- Varias formas de construirlas:
List(1,2,3) 1 :: List(2,3)
1 :: ( 2 :: ( 3 :: Nil ) ) 1 :: 2 :: 3 :: Nil
1 :: 2 :: 3 :: List()
-
La expresión
x :: xs
sirve para construir una lista cuyo primer elemento esx
y su cola esxs
. Los elementos dexs
deben de ser del mismo tipo quex
para que toda la lista resultante sea del mismo tipo. -
Nil
es la lista vacía. Es lo mismo que escribirList()
.
Listas
-
map
,filter
,forall
yexists
:
List(1,2,3).map { x => 2*x } == List(2,4,6)
List("Mónica", "Andres", "Luis", "Andrea", "Maria", "Eduardo", "Armando", "Erica", "Luisa").filter( nombre => nombre.startsWith("A") ) == List("Andres", "Andrea", "Armando")
List(3,10,-23,653,-1,-565,43,-87).forall( _ > 0 ) == false
List(5,3,2,6,8,23).exists( _ > 15 ) == true
List(5,3,2,6,8,23).foldRight(0)( (a:Int,acc:Int) => a+acc ) == 47
-
foldLeft
yfoldRight
. Una explicación decente la encuentran en el curso de Coursera.
Aplanando listas
- ¿Como sacar todos los digitos de una lista de números?:
scala> todosLosDigitos(List(5,10,26,34534))
res0: List[Int] = List(5, 1, 0, 2, 6, 3, 4, 5, 3, 4)
-
flatten
sirve para "aplanar" una lista de listas:
def darDigitos(n: Int): List[Int] = {
n.toString.map( c => Integer.parseInt(c+"") ).toList
}
def todosLosDigitos(nums: List[Int]): List[Int] = {
val ds : List[List[Int]] = nums.map(darDigitos)
val respuesta: List[Int] = ds.flatten
respuesta
}
Aplanando listas
-
Otra forma de hacerlo:
flatMap
sirve para mapear cada elemento de una lista a otra lista y concatenarlas al mismo tiempo. A diferencia demap
, la función que se le pasa aflatMap
debe devolver otra lista:
def todosLosDigitos(nums: List[Int]): List[Int] = {
nums.flatMap(darDigitos)
}
options
case class Persona(primerNombre: String, segundoNombre: Option[String])
- Un
Option
es un trait que tiene dos subclases:
sealed trait Option[T]
case class Some[T](t: T) extends Option[T]
case object None extends Option[Nothing]
Options
- Es decir o bien es un
Some
y alberga un valor o es unNone
y no tiene nada por dentro. - Un
Option
es como una caja negra que puede o no contener un valor:
Option
- Las dos funciones mas importantes de
Option
sonmap
yflatMap
que permiten manipular el valor interno como si existiese. -
map
recibe una función que transforma el valor interno. -
La firma de la función
map es algo como:
trait Option[A] {
def map[B](f: A => B): Option[B] = { ... }
}
Option
- Si el
Option
es unSome
entonces es transformado - Si el
Option
es unNone
entonces la función no tiene ningún efecto:

Option
- Ejemplo de uso de map:
val maria = Persona("Maria", Some("Alejandra")) val nomMay1: Option[String] = maria.segundoNombre.map( segNom => segNom.toUpperCase )
// <- Some("ALEJANDRA")
val miguel = Persona("Miguel", None) val nomMay2: Option[String] = miguel.segundoNombre.map( segNom => segNom.toUpperCase )
// <- None
Option
- ¿Qué pasa si se quiere "extraer" el valor del Option?
-
getOrElse
es una función que recibe un valor por defecto. Si elOption
es unSome
y contiene un valor entonces lo devuelve, de lo contrario devuelve el valor por defecto:
def nombreCompleto(p: Persona) {
val textoSegundoNombre = p.segundoNombre.map(s=> " "+s)
p.primerNombre + textoSegundoNombre.getOrElse("")
}
nombreCompleto(maria) // <- "Maria Alejandra"
nombreCompleto(miguel) // <- "Miguel"
Option
- ¿Como implementar la función
sumarDivisiones
?
def dividir(a: Double, b: Double): Option[Double] = {
if( b == 0.0 ) {
None
} else {
Some( a / b )
}
}
def sumarDivisiones(a: Double, b: Double, c: Double, d: Double): Option[Double] = {
val division1: Option[Double] = dividir(a,b)
val division2: Option[Double] = dividir(c,d)
???
}
Intento 1
- Hacer pattern matching en cada uno de los casos:
def sumarDivisiones(a: Double, b: Double, c: Double, d: Double): Option[Double] = {
val division1: Option[Double] = dividir(a,b)
division1 match {
case Some(aDivB) =>
val division2: Option[Double] = dividir(c,d)
division2 match {
case Some(cDivD) => Some( aDivB + cDivD )
case None => None
}
case None => None
}
}
- Funciona, pero hay mejores formas
intento 2: MAP
def dividir(a: Double, b: Double): Option[Double] = { ... } def sumarDivisiones(a: Double, b: Double, c: Double, d: Double): Option[Double] = { val division1: Option[Double] = dividir(a,b) val division2: Option[Double] = dividir(c,d) val suma = division1.map { aDivB => division2.map { cDivD => aDivB + cDivD } } suma // <- NO COMPILA!!!!!:
}
error: type mismatch:
found: Option[Option[Double]]
required: Option[Double]
flatMap
-
La función
flatMap
permite combinar los valores de múltiplesOption
s. La firma de esta función es algo como:
trait Option[A] { def flatMap[B](f: A => Option[B]):Option[B] ={
...
} }
- A diferencia de
map
, la función que uno le tiene que pasar aflatMap
debe devolver otroOption
. - Esta es la raíz de muchos errores de compilación
intento 3
-
Usando
flatMap
:
def sumarDivisiones(a: Double, b: Double, c: Double, d: Double): Option[Double] = {
val division1: Option[Double] = dividir(a,b)
val division2: Option[Double] = dividir(c,d)
val suma = division1.flatMap { aDivB =>
division2.map { cDivD =>
aDivB + cDivD
}
}
suma
}
- Sintáxis especial (for-comprehensions):
def sumarDivisiones(a: Double, b: Double, c: Double, d: Double): Option[Double] = {
val suma = for {
aDivB <- dividir(a,b)
cDivD <- dividir(c,d)
} yield aDivB + cDivD
suma
}
For-comprehensions
- Siendo
e0
una expresión que es de tipoOption
ye
una expresión que usap0
:
e0.map { p0 => e }
- Es lo mismo que:
for {
p0 <- e0
} yield e
For-comprehensions
e0.flatMap { p0 => e1.flatMap { p1 => e2.flatMap { p2 => ... en.map { pn => e }
}
} }
- Es lo mismo que:
for {
p0 <- e0
p1 <- e1
...
pn <- en
} yield e
-
La expresión
e2
puede utilizar el valorp1
,e3
puede utilizarp1
yp2
, etc ... -
La expresión
e
puede utilizar todos los valoresp1
,p2
...pn
Option
- Ventajas:
-
map
yflatMap
permiten expresar el camino ideal / felíz del código sin que uno se tenga que preocupar por el manejo del caso del error. -
Option[T]
expresa mediante su tipo explícitamente la posibilidad de que el valor no exista. Evita la posibilidad deNull Pointer Exceptions
. -> Typesafety!!! - Por ejemplo en Slick: Toda columna que en la base de datos sea nullable se traduce en un
Option[X]
dentro de las clases autogeneradas de Slick. - Desventaja: Puede llegar a ser muy engorroso cuando se combina con otras cosas. (Pero hay soluciones!!!)
refactor de código
- A partir de un
Option[(Ciudad, Departamento)]
queremos obtener unOption[String]
con el nombre de la ciudad. En vez de:
if(ciudadDepartamento.isDefined) Some(ciudadDepartamento.get._1.nombre) else None
- Simplemente:
ciudadDepartamento.map { case (ciudad,_) => ciudad.nombre }
ciudadDepartamento.map(_._1.nombre) // ¿menos legible?
Eithers
- Representa uno de dos posibles valores: uno a la "izquierda" y otro a la "derecha"
- También sirven para representar errores como Option pero además también sirven para decir QUÉ error se produjo
- Esta implementado mas o menos así:
sealed trait Either[+E, +A]
case class Left[+E](value: E) extends Either[E, Nothing]
case class Right[+A](value: A) extends Either[Nothing, A]
- Tiene un valor a la izquierda, que por lo general representa el error y otro valor a la derecha que representa el dato deseado
eithers
- ¿Como construir una Persona?
case class Person(name: Name, age: Age) sealed class Name(val value: String) sealed class Age(val value: Int)
def mkName(name: String): Either[String, Name] = if (name == "" || name == null) Left("Name is empty.") else Right(new Name(name))
def mkAge(age: Int): Either[String, Age] = if (age < 0) Left("Age is out of range.") else Right(new Age(age))
eithers
- Con map y flatMap:
def create(name: String, age: Int): Either[String, Person] = {
mkName(name).flatMap { name =>
mkAge(age).map { age =>
new Person(name, age)
}
}
}
- O con un for-comprehension:
def create(name: String, age: Int): Either[String, Person] = {
for {
name <- mkName(name)
age <- mkAge(age)
} yield new Person(name, age)
}
Futures
Future[T]
represents a T
-typed result, which may or may not be delivered at some time in the future, or which may fail altogether. Like real-world asynchronous operations, a future is always in one of 3 states: incomplete, completed successfully, or completed with a failure."
Futures
- Para poder ejecutar un
Future
se necesita tener en el scope un contexto de ejecución, que es mas o ménos un pool de threads. - Scala provee un contexto de ejecución global (que se utiliza haciendo
import scala.concurrent.ExecutionContext.Implicits.global)
, pero para aplicaciones de verdad se sugiere tener varios aislados. - Para ejecutar un bloque de código dentro de un
Future
basta con hacer: Future { miCodigo() }
Intuición
- ¿Qué se imprime primero?
implicit val ec: ExecutionContext = ...
def ejecutarFuturo(): Future[Int] = Future {
Thread.sleep(2000)
println("Future completed!")
4+5
}
ejecutarFuturo()
println("Completed!")
Map
-
map
transforma el resultado delFuture
. Cuando elFuture
sobre el que se llamamap
termine se ejecuta la función. - Es una mejor forma de mandar a ejecutar una función después de una acción asíncrona que usando callbacks.
- La firma de
map
es algo como:
class Future[A] {
def map[B](f: A => B)(implicit ec: ExecutionContext): Future[B] = {
...
}
}
MAP

Componiendo acciones asíncronas
- ¿Qué pasa si uno quiere hacer algo asíncrono pero necesita el resultado de otra acción que es asíncrona?
- Por ejemplo:
def userTrackIds(userId: UserId): Future[List[TrackId]] = { ... }
def tracks(trackIds: List[TrackId]): Future[List[Track]] = { ... }
- ¿Como obtener los tracks de un usuario?
Componiendo acciones asíncronas
- hmmm ¿
map
?
def userTracks(userId: UserId): Future[List[Track]] = {
userTrackIds(userId).map { trackIds =>
tracks(trackIds)
} // <- NO COMPILA!!!!!!!!!!
}
error: type mismatch:
found: Future[Future[List[Track]]]
required: Future[List[Track]]
- hmmm ¿
Future[Future[List[Track]]]
? - ¿Una acción asíncrona que devuelve otra acción asíncrona?
- No tiene mucho sentido
- ¿Entonces?
flatmap (por 3ra vez)
- La firma de
flatMap
es es algo como:
class Future[A] {
def flatMap[B](f: A => Future[B])(implicit ec: ExecutionContext): Future[B] = {
...
}
}
- Es decir, a flatMap se le debe pasar una función que devuelva un resultado asíncrono: otro Future
- Una vez mas: esta última restricción es la fuente de muchos errores de compilación
flatMap
def userTracks(userId: UserId): Future[List[Track]] = {
userTrackIds(userId).flatMap { trackIds =>
tracks(trackIds)
} // <- AHORA SI COMPILA!!!!!!!!!!
}
- O usando un for-comprehension:
def userTracks(userId: UserId): Future[List[Track]] = {
for {
trackIds <- userTrackIds(userId)
userTracks <- tracks(trackIds)
} yield userTracks
}
para paralelismo
-
flatMap
también se puede usar para reunir resultados de variosFutures
que se ejecutan en paralelo:
val f1: Future[Int] = ejecutarF1() // <- Se empieza a ejecutar
val f2: Future[String] = ejecutarF2() // <- Se empieza a ejecutar
val f3: Future[(Int,String)] = for {
n <- f1 // <- Se está ejecutando en paralelo a f2
s <- f2 // <- Se está ejecutando en paralelo a f1
} yield (n+1, "Hola,"+s) // Solo se ejecuta cuando f1 y f2 acaben
- En cambio lo siguiente no se ejecutaría en paralelo:
val f3: Future[(Int,String)] = for {
n <- ejecutarF1() // <- Se empieza a ejecutar
s <- ejecutarF2() // <- Solo se empieza a ejecutar cuando se haya resuelto "n"
} yield (n+1, "Hola,"+s) // Solo se ejecuta cuando f1 y f2 acaben
Componiendo un número (variable) de acciones asíncronas
- ¿Como descargar todas las imagenes de un HTML usando las funciones
downloadImage
ygetImageUrls
?
def downloadImage(url: Url): Future[Image] = { ... } def getImagesUrls(html: HTML): List[Url] = { ... }
def downloadImages(html: HTML) = { getImagesUrls(html).map(downloadImage) // <- en paralelo }
- ¿Qué tipo devuelve
downloadImages
?
Componiendo un número (variable) de acciones asíncronas
- ¿Como descargar todas las imagenes de un HTML usando las funciones
downloadImage
ygetImageUrls
?
def downloadImage(url: Url): Future[Image] = { ... } def getImagesUrls(html: HTML): List[Url] = { ... }
def downloadImages(html: HTML) = { getImagesUrls(html).map(downloadImage) // <- en paralelo }
- ¿Qué tipo devuelve
downloadImages
? -
List[Future[Image]]
Componiendo un número (variable) de acciones asíncronas
- ¿Como descargar todas las imagenes de un HTML usando las funciones
downloadImage
ygetImageUrls
?
def downloadImage(url: Url): Future[Image] = { ... } def getImagesUrls(html: HTML): List[Url] = { ... }
def downloadImages(html: HTML) = { getImagesUrls(html).map(downloadImage) // <- en paralelo }
- ¿Qué tipo devuelve
downloadImages
? -
List[Future[Image]]
- ¿Qué pasa si uno quiere hacer algo con todas las imagenes?
- Por ejemplo almacenarlas con una función como:
def storeImages(images: List[Image]) = {...}
COMPONIENDO UN NÚMERO (VARIABLE) DE ACCIONES ASÍNCRONAS
- Si pudiera convertir List[Future[Image]] en un Future[List[Image]] entonces podría pasarle el contenido de ese Future a la función storeImages.
-
La librería estándar de Scala tiene una función que precisamente permite hacer esto. Su firma simplificada es algo como:
object Future {
def sequence[A](list: List[Future[A]])(implicit executor: ExecutionContext): Future[List[A]] = { ... }
}
COMPONIENDO UN NÚMERO (VARIABLE) DE ACCIONES ASÍNCRONAS
-
Future.sequence
reune los resultados de múltiplesFutures
(es decir unList[Future[A]]
) en un soloFuture
que dispone de todos los resultados (es decir unFuture[List[A]]
). - La cosa quedaría así:
def downloadImage(url: Url): Future[Image] = { ... }
def storeImages(images: List[Image]) = { ... }
def getImagesUrls(html: HTML): List[Url] = { ... }
def downloadAndStoreImages(html: HTML) = {
val imagesFuture: List[Future[Image]] = getImagesUrls(html).map(downloadImage)
val futureImages: Future[List[Image]] = Future.sequence(imagesFuture)
futureImages.map { images =>
storeImages(images)
}
}
¿Como se podría implementar Future.sequence?
- ¡Con flatMap!
object Future {
def sequence[A](list: List[Future[A]])(implicit ec: ExecutionContext): Future[List[A]] =
{
list match {
case Nil => Future.successful(List[A]()) // <- Envuelve una lista vacía
case h :: tail =>
val tailSequence: Future[List[A]] = sequence(tail)
h.flatMap { a =>
tailSequence.map { as => a :: as }
}
}
}
}
¿Como se podría implementar Future.sequence?
- Se puede ver mas claro con un for-comprehension:
object Future {
def sequence[A](list: List[Future[A]])(implicit ec: ExecutionContext): Future[List[A]] =
{
list match {
case Nil => Future.successful(List[A]())
case h :: tail =>
val tailSequence = sequence(tail)
for {
a <- h
as <- tailSequence
} yield a :: as
}
}
}
¿Como se podría implementar Future.sequence?
- Lo mismo con
foldLeft
object Future {
def sequence[A](list: List[Future[A]])(implicit ec: ExecutionContext): Future[List[A]] =
{
list.foldLeft(Future.successful(List[A]())) { (tailSeq, h) =>
val tailSequence = tailSeq
for {
a <- h
tail <- tailSequence
} yield a :: tail
}
}
}
(nota aparte: promesas en js)
-
En Javascript varios frameworks/librerías (Angular.js o ember.js por ejemplo) usan
Promises
, que son muuuy parecidas a losFutures
en Scala -
Angular.js utiliza la implementación de promesas de un módulo llamado
$q
-
Ember.js utiliza la implementación de promesas de un módulo llamado
RSVP.js
-
Ambas tienen APIs muy similares porque ambas cumplen el estándar Promises/A+
-
Las Promesas tienen un método
.then
que recibe una función que mapea el valor de la promesa y otra función.catch
que sirve para manejar fallas:
promise // <- la promesa puede ser el consumo de un servicio web del servidor
.then(function success(data) {
console.log(data);
})
.catch(function error(msg) {
console.error(msg);
});
(nota aparte: promesas en js)
-
.then
hace las veces demap
yflatMap
enFutures
de Scala. Es decir se le pueden pasar funciones normales o funciones que devuelven otro valor asíncrono, es decir otra promesa.
promise
.then(doSomething) // <- 'doSomething' puede retornar un valor plano
.then(doSomethingElse) // O bien 'doSomethingElse' puede retornar una promesa
.then(doSomethingMore)
.catch(logError);
- ¡Incluso en JS existe el equivalente de
Future.sequence!
:
$q.all([promiseOne, promiseTwo, promiseThree])
.then(function(results) {
console.log(results[0], results[1], results[2]);
});
conclusiones
- La librería estándar tiene muchas funciones que facilitan la vida. Solamente que son técnicas que no nos son familiares y que no existen todavía en java.
-
Al principio es difícil darse cuenta cuando se puede usar flatMap pero con el tiempo uno identifica en que situaciones se puede hacer. Uno empieza a pensar en terminos de los tipos.
- Una vez uno se acostumbre a hacer flatMaps seguidos uno facilmente puede reemplazarlos por un for-comprehension y darle un look imperativo al codigo.
conclusiones
- Algo mantenible no es lo mismo que algo familiar. Entonces no está bien decir que algo que no me es familiar no es mantenible.
- Pero igual es mejor favorecer la familiaridad que tiene TODO el equipo con la solución que uno elija. Si uno tiene que elegir entre:
-
Utilizar una función rarísima pero que ahorra trabajo y sirve
-
Utilizar una solución mundana que todo el equipo conoce como por ejemplo
pattern-matching
Referencias/Links utiles
- Learning Functional Programming without Growing a Neckbeard
- La idea de los dibujos de option viene de aquí
- Documentación oficial de Futures en Scala
- Futures on Scala 2.10
- Programming with futures: patterns and anti-patterns
- Porque es importante aislar contextos de ejecución: Ver la sección 'Lessons learned' de NYT
- Asincronía sin tener que escribir maps o flatMaps (usando macros)
- Como elegir un pool de threads adecuado para una aplicación
- Promesas en Angular.js
-
Estándar de promesas en JS
- Algo que faltó: Eithers
scala de rápidez
By Miguel Vilá
scala de rápidez
- 2,440