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

  • Por defecto todos los atributos de una clase son privados, pero si se antecede su nombre con var o val entonces se vuelven públicos (y mutables e inmutables respectivamente):
  • class Persona(var tipoId: String, val numeroId: Int, ciudad: String)
  • Las palabras reservadas private  y protected tienen el mismo significado que en Java
  • Objetos

  • Los objetos son "singletons". Son instancias nombradas que pueden tener valores o funciones. Sirven para definir módulos:
  • object SimpleObject{
        val param1 : Int = 10
        var param2 : String = "Yes"
        def method1 = "Method 1"
        def sum(a:Int, b:Int) = a + b
    }
    
  • O para definir instancias nombradas o universales:
  • object EmptySet extends IntSet {
        def contains(x: Int): Boolean = false
        def incl(x: Int): IntSet = new NonEmptySet(x, EmptySet, EmptySet)
    }
    

    Case Classes

  • Los Case Classes son clases con atributos inmutables y otras facilidades. Pueden servir para implementar value objects (de Domain Driven Design):
  • case class Persona(tipoId: String, numeroId: Int, ciudad: String, edad: Int)
  • Implementan una función toString por defecto que resulta útil:
  • val p1 = new Persona("CC",123456,"Bogotá", 26)
    p1.toString == "Persona(CC,123456,Bogotá,26)"
  • Se pueden inicializar omitiendo el keyword new:
  • 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

  • Permiten hacer pattern matching, que es como evaluar condiciones y extraer datos al mismo tiempo (mejor explicación en el curso de Scala de Coursera):
  • 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 :: Nil1 :: 2 :: 3 :: List()
    • La expresión  x :: xs sirve para construir una lista cuyo primer elemento es x y su cola es xs . Los elementos de xs deben de ser del mismo tipo que para que toda la lista resultante sea del mismo tipo.
    • Nil es la lista vacía. Es lo mismo que escribir List()

    Listas

    • map, filter, forall y exists:
    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 y foldRight. 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 de map , la función que se le pasa a flatMap debe devolver otra lista:
    def todosLosDigitos(nums: List[Int]): List[Int] = {
        nums.flatMap(darDigitos)
    }
    

    options

  • Son valores "opcionales". Por ejemplo
  • 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 un None 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 son map y flatMap 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 un Some entonces es transformado
    • Si el Option es un None 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 el Option es un Some 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últiples Options. 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 a flatMap debe devolver otro Option.
    • 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 tipo Option y e una expresión que usa p0:
    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 valor p1, e3 puede utilizar p1 y p2, etc ...
    • La expresión e puede utilizar todos los valores p1, p2 ... pn

    Option

    • Ventajas:
      • map y flatMap 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 de Null 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 un Option[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

  • "A    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."
  • Los  futures  sirven para componer acciones asíncronas y paralelas de forma fácil y razonable.

    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 del Future. Cuando el Future sobre el que se llama map 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 varios Futures 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 y getImageUrls?
    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 y getImageUrls?
    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 y getImageUrls?
    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]] = { ... }
    }
    
  • (En realidad no es específico a listas sino a cualquier cosa que se pueda recorrer: secuencias, streams, vectores, etc...) 

  • COMPONIENDO UN NÚMERO (VARIABLE) DE ACCIONES ASÍNCRONAS

    • Future.sequence reune los resultados de múltiples Futures (es decir un List[Future[A]]) en un solo Future que dispone de todos los resultados (es decir un Future[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 los Futures 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 de map y flatMap en Futures 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:
    1. Utilizar una función rarísima pero que ahorra trabajo y sirve
    2. Utilizar una solución mundana que todo el equipo conoce como por ejemplo  pattern-matching  
  • La elección correcta es la que uno crea que TODO el equipo va a entender, sea la 1 o la 2
  • Ahora que sabemos algunos patrones de más podemos inclinarlos por la 1 (pero siempre podemos elegir la 2).
  • Referencias/Links utiles

    scala de rápidez

    By Miguel Vilá

    scala de rápidez

    • 2,440