Montaña Aconcagua, Provincia de Mendoza, Argentina

Scala, un camino de ida hacia la cima

Sergio Magnacco
Despegar Ingeniería
12 min readJun 1, 2021

--

¿Por qué programamos con Scala?

La pregunta me recuerda la historia de los budistas sobre qué es la sabiduría, donde un aprendiz le pregunta a su monje cómo adquirirla. El monje le explica que la sabiduría es como un durazno, y le pregunta si le apetece uno. El estudiante dice que sí; el maestro se lo muestra y le ofrece pelarlo. Él, respetuoso, se lo agradece. El maestro se ofrece a cortarlo, y finalmente se ofrece a darle las rodajas con un detalle: masticadas. A lo que el aprendiz le dice: no, maestro… gracias, ya entendí. La moraleja de la historia es que nadie puede vivir una experiencia por uno, hay que hacer para saber y entender. Así que: a escribir código.

Primero, vamos a necesitar un REPL de Scala. Ahora están de moda, pero en el 2014 no muchos lenguajes lo tenían para poder prototipar. Acá hay una gran opción Scastie, que es la que voy a usar para jugar en este post. Si no, siéntanse libres de bajar Scala y probar su REPL , y si se animan a más, prueben Ammonite. El primer link de Scala es un one click installer y se le puede pedir que ya te instale ammonite.

Scala corre sobre la JVM, con las ventajas de poder interactuar con un ecosistema sólido, tanto como en librerías como en herramientas (performance, reporting, etc). Aquellos que vienen de ese mundo verán que muchas de estas cosas que vamos a ir viendo que tiene Scala desde hace varios años, en los últimos tiempos se fueron agregando a Java. Scala pasó a ser un modelo a seguir. Por ejemplo, también lo sigue Rust. Si ven que Java aceleró sus releases, no es coincidencia. Scala lo viene haciendo desde hace años con mucho éxito. En algunos casos han roto la compatibilidad pero lo han hecho para mejorar el lenguaje. La comunidad, viendo el valor, lo adoptó y sumó el cambio rápidamente, por lo que nos permite siempre estar en la última versión gracias a un ecosistema dinámico y que sigue el paso. Scala 3 va a ser demoledor, hasta un migrador nos hizo y está invitando al público de python al adaptarse a su sintaxis.

Más allá de que Java se va actualizando con ideas de Scala, hay un tema conceptual que los diferencia. En este lenguaje se escriben expresiones y no declaraciones. Las declaraciones las genera el compilador por nosotros ahorrándonos mucho código imperativo (decorativo y tedioso). Escribamos un par de expresiones válidas para ir conociéndolo.

val language = "Scala"
val description = if (language == "Scala")
s"is an object oriented and fun programming lang"
else
"is an object oriented language."
println(s"$language $description")

Estas son declarativas y devuelven un valor inmutable e idempotente. Gracias a esto, se las puede combinar y su evaluación puede ser reordenada por el compilador (lazy evaluation). Se suele hablar de transparencia referencial: es lo mismo el valor 4 que 2 + 2, entonces este modelo de sustitución nos permite pensar como si fueran ecuaciones.

Los bloques (closures) son también expresiones, por lo que tanto las expresiones lambdas como el for pueden ser tratados de esa forma y compuesto con otras expresiones. Esta característica es lo que en mi opinión hace de Scala un lenguaje maleable como la plastilina.

El segundo killer feature, en mi opinión, es que combina dos poderosos paradigmas de programación: el funcional y el imperativo (orientado a objetos) y la teoría de Categorías y Cálculo Lambda. Esto nos permite organizar nuestro código por interés (concerns) o responsabilidades, como lo venimos haciendo hace tiempo en lenguajes orientados a objetos, y a nivel de código tenemos el poder de las funciones como ciudadanos de primer orden. La teoría de categorías nos da un marco con algunas abstracciones que son útiles y de alguna forma se asemejan a los patrones de diseño, con nombres extraños y sonantes.

Si querés ir hasta el fondo del rabbit hole, seguí este link con un video, o para los que prefieran leer (old school). No son necesarios para poder trabajar con el lenguaje, pero si sos curioso, mejor empezar por buenos links.

Para los que no están familiarizados con el paradigma funcional y su poder, acá algunos conceptos:

Una función solo tiene una responsabilidad. Simple, como los objetos e.e, y se pueden definir como en el siguiente ejemplo un método línea 1 y 3, o como un valor línea 2. Ambas son válidas y se pueden pasar por parámetro. Si uno mira detras de la cortina del mago compilador, al def lo transforma a una función que es un objeto. Es decir, todo es un objeto y las funciones son del tipo Function con un método llamado apply, cuando pasamos un def, el compilador por detras hace un new Function, cuyo apply es el body del método.

def sumAsFunction(x: Int,y: Int) = x + y 
val sumAsValue: (Int, Int) => Int = (x, y) => x + y
def combineBoth(x: Int, y: Int) = sumAsFunction(x, y) + sumAsValue(x, y)

Una función no tiene efecto de lado o secundario. El efecto de lado se manifiesta por salir fuera del sistema (IO) o por modificar estados como se da en las variables mutables. Si declaran una operación que tiene efecto de lado, no es una función, es un procedimiento o método. En la literatura lo van a encontrar cómo “effects”. Saber diferenciar entre un este y una función es importante porque este lenguaje permite ambos. Siempre van a leer que es bueno separar los efectos sobre el sistema de la lógica de negocio. Es fácil detectar un “effect” porque generalmente la firma retorna un Unit, pero también puede ser identificado como una llamada que haga IO y que retorne algún valor resultante.

Una función tiene transparencia referencial, ya mencioné sus ventajas. Si necesitan profundizar en transparencia referencial y el modelo de substitución, hay muchos videos y blogs que lo explican al detalle, pero no hay como un ejemplo ameno donde f y g son los nombres de la función, y la notación

<nombre>: <tipo_I> => <tipo_O> = <parametro> => <declaración>

Donde tipo_I es el tipo de entrada, el tipo_O es el tipo de salida. El lenguaje soporta más de un parametro de entrada o salida y se lo debe anotar como si fuera una tupla como se va a ver más abajo. El simbolo => representa una flecha como en la relación de conjuntos, vamos de un conjunto a otro (arrow). Finalmente, parametro es el nombre que necesito para poderlo usar en la declaración de la función. Con el ejemplo va a quedar más claro

val f: (Int, Int) => Int = (x, y) => x + yval g: (Int, Int) => Int = (x, y) => y + x

Una función se puede combinar con otra, es decir, podría pasar como parámetro una función a otra función. Las que pueden recibir funciones se las llaman ‘Higher Order Functions’.

val z: (Int, Int) => Boolean =
(x, y) => x + y == g(x, y) && f(x, y) == x + y

Notar que si quiero pasar una función por parámetro, Scala me exige que utilice la definición de valor e.g: f: (Int) => Int

def applyFunction(x Int, f: (Int) => Int): Int = f(x)

Otra carácteristica son poder declarar funciones parciales, se les puede ir pasando los parametros en distintos momentos hasta que se completen todos y se realice finalmente la computación del mismo (lazy evaluation). Por ejemplo este truquito lo usan mucho para armar DSLs (por ahora no preocuparse), con el tiempo pueden ir probando el sabor de estos chiches.

val partialFunction = z(1, _)

Finalmente, Inmutabilidad… pero este merece su propio espacio así que no me voy a explayar ahora.

La ventaja que tienen las funciones es que nos permiten entender qué pasa en el código sin sorpresas (least surprise principle). Tanto no tener efecto de lado como saber que una función hace una y solo una cosa, reduce la complejidad de pensar que esta pasando. Fundamental usar buenos nombres, significativos. ¿Por que es tan importante? Hay que evitar lo que para mi fue un mal ejemplo en Haskell, y que, IMHO, fue fundamental para tener una baja adopción, esto fue el uso/abuso de chirimbolitos. Para mi esto asustó al pueblo dev (no entiendo nada), y quedó reducido a un público academico (gente que no mantiene código de negocio). Poder saber qué hace sin tener que leer la especificación, es un gol. También el poder combinar funciones nos permite modelar casos de usos complejos con unas simples líneas: el viejo ideal de la reutilización al mango.

La diferencia entre variable y valor es bastante fuerte. Un valor no cambia en el tiempo, lo cual nos permite predecir su estado futuro una vez seteado. Ahora, una variable, en un programa imperativo como es Java, en un momento puede tener un valor y en otro momento otro valor. Eso hace que sea más difícil pensar qué pasa con ella en el tiempo, nos impide pensar como una ecuación y jugar con las sustituciones para resolver el problema.

Por supuesto, los que nos dedicamos a hacer código que resuelve problemas de negocio entendemos que tiene que haber un sano equilibrio entre ambos mundos. La recomendación siempre cuando entran al mundo de Scala es que en el primer proyecto no se tiren de cabeza con el mundo funcional, sino que traten de codear como siempre y luego, con el dominio del lenguaje, empezar a escalar y probar cómo resolver problemas de otra manera, haciéndole caso a Einstein.

El tratamiento de errores es otra gran diferencia que vale la pena mencionar. Los errores tradicionalmente se utilizan para poder manejar estados no esperados en el camino feliz, y los programadores, cuando no saben cómo tratarlos, la arrojan para arriba para que algún programador con más experiencia trate de atajarla y ver qué hacer con ella XD. En Scala, la idea es manejar los errores como valores inmutables. Es decir, como negocio entiendo que hay un posible resultado que es el no esperado y tengo que manejarlo donde corresponda (hasta ahí estamos en la misma), pero al ser un valor, puedo jugar con él como con cualquier otro valor o expresión (transparencia referencial y el modelo de substitución).

Más adelante vamos a ir viendo que hay ciertos features que nos permiten combinar o extraer información para hacer cosas. El lenguaje por defecto nos ofrece un tipo Try con dos posibles valores (constructores de tipos): “Success”, que wrapea al resultado; o Failure, que wrapea al error.

import scala.util.Trydef dividir(numerador: Int, denominador: Int): Try[Double] =    Try {     numerador / denominador    }dividir(10, 2)dividir(10, 0)

Para no reinventar la rueda, Scala nos provee una utilidad que la vas a querer, tiene opciones para devolver Try (allCatch.toTry), Either u Option.

import scala.util.control.Exception._import scala.util.Eitherdef dividir(numerador: Int, denominador: Int): Either[Throwable, Double] = {     allCatch.either( numerador / denominador )}

Cuando no nos interesa tratar el error y solo nos importa si hubo o no un valor resultante, se suele utilizar Option, donde el valor esperado es Some, y el error es None.

En concurrencia, tenemos una abstracción Future que también va a tener un resultado a futuro Success o Failure como el Try. Si usted está viendo aquí un patrón repetido, está en lo cierto: esto es un patrón de diseño funcional, en realidad pertenece a la teoría de las categorías. En los blogs van a ver que se llama Monada.

Ah, es un wrapper de un valor y tiene algunos métodos copados para hacer cosas como transformar el valor que wrapea utilizando una función, pedir su valor y, si justo no está definido, devolver un default. Quizá hasta te sorprenda empezar a ver este patrón en otros lados, como por ejemplo, una lista de elementos o lista vacía. En Scala, un List vacío se representa como Nil, y si tiene elementos, como List (uno o más elementos). En esta teoría también aparecen definiciones como Functor, Monoide, que tienen nombres locos, pero no deberían asustarnos porque realmente los van a usar cuando hagan cosas con las listas, pero saber que se llaman así en sus primeros años con Scala no les va a mover el amperímetro.

Ahora, si quieren tomar la Pastilla Roja (o Azul) e.e, les recomiendo ver estos videos en ese orden:

  1. Mondas for beginners
  2. Functors
  3. Monads are Monoids in the Category of Endofunctors

Otro feature, que para mí es muy valioso porque simplificó diseñar con código soluciones, son los famosos value objects inmutables, que en Scala se llaman case classes. En la teoría se llaman Algebraic Data Types. Estos tipos de datos respetan todas las reglas matemáticas que nos sirven para comparar sin tener que estar haciendo cosas como se hacen en Java con el equals y el hashcode. Antes de ir a un ejemplo, solo les voy a dar mi opinión: mentalmente me es más fácil poner primero un nombre y después pensar en su tipo que al revés, como en Java. El ejemplo habla por sí solo, solo notar que == en Scala es sinónimo de .equals() en Java, ah, y no más setters y getters haciendo click click click en tu ide favorito:

case class Persona(nombre: String, apellido: String)val sergioFromToday = Persona("Sergio", "Magnacco")val sergioFromTheFuture = Persona("Sergio", "Magnacco")sergioFromToday == sergioFromTheFuture  #true: Boolean

Como mencioné, Java aprende de los buenos ejemplos, y hoy pueden ver algo similar llamado Records. Algo que aún no está disponible en Java es el pattern matching, aunque lo tiene previsto para Java 16. ¿Qué es el pattern matching? Ivan Topolnak (coautor de Kamon.io junto a Diego Parra ), ambos ex Despegar devs, lo nombró switch con esteroides. Nada mejor que un ejemplo, donde definimos un hotel cuyo id es 300400 y su review del lobby es bueno, queremos poder devolver un Success si el review es mayor a 8 o un Failure si es menor, indicando el hotel id, su review y su puntaje. En este ejemplo uso dos pattern match para extraer los valores (unapply magia oscura para obtener el valor de un campo) de los cases classes y del resultado del Try. Todo esto no es magia: se logra con un buen compilador, que puede escribir código repetitivo por nosotros y nos facilita la vida (sintactic sugar, un dulce Odersky). Este es otro ejemplo de la ergonomía de un lenguaje.

import scala.util.Try
import scala.util.Success
import scala.util.Failure
case class Review(id: String, points: Double)case class Hotel(id: String, review: Review)case class LowReviewException(hotel_id: String, review_id: String, points: Double) extends Throwableval good_review_300400 = Review(id = "Lobby", points = 9.82)val hotel_300400 = Hotel(id = "300400", review = good_review_300400)val good_hotel_review: Try[String] = hotel_300400 match {
case Hotel(id, Review(_ , points)) if points >= 8 =>
Success(id)
case Hotel(hotel_id, Review(review_id, points)) =>
Failure( LowReviewException(hotel_id, review_id, points))
}
good_hotel_review match {
case Success(id) =>
println(s"Hotel with good review: $id")
case Failure(LowReviewException(hotel_id, review_id, points)) =>
println(s"The $hotel_id for $review_id has $points")
case Failure(e) =>
println("Unexpected failure $e")
}

Para programar en Scala, como toda escalera, es bueno empezar por lo simple, paradigma de objetos, y luego ir subiendo peldaños y agregando funcionalidades. Entender en detalle la teoría de categorías o lambda calculus no es un must. Es bueno saber que existen, siempre se puede profundizar cuando se tenga la necesidad o el interés, pero no es requisito saberlos para que puedas hacer programas de negocio de manera simple, legible, flexible y robusta.

Sí tiene un detalle, que es que requiere pensar mejor, pero como resultado obtenemos un código más pequeño y, como solemos leer más que escribir, se paga sola la inversión. Después de todo, pensar nos hace entender mejor el dominio.

También hay que sumar que la inmutabilidad nos simplifica la vida. No tener efecto de lado te da paz mental… siempre y cuando no abras la puerta del mal que todo lenguaje ofrece para los que tienen un espíritu indómito.

Si llegaron hasta acá, queda un par de cosas para ver, pero lo más importante para escribir código de negocio está arriba. En los próximos post vamos a hablar de colecciones, diseño (cómo pensar distinto si venís del mundo mutable y declarativo de Java), concurrencia y cómo las funciones y la inmutabilidad nos dan una gran mano para paralelizar nuestra lógica, y un bonus track de asincronismo a la Scala.

Scala es una montaña, lo bueno está en el camino.

No dejes que los 5 obstaculos te impidan llegar arriba.

Nos vemos en la cima

Mantenlo estúpidamente simple

--

--