Bienvenidos a esta emocionante entrada donde exploraremos las últimas novedades en el mundo de Scala. Scala, un lenguaje de programación funcional y orientado a objetos, continúa evolucionando y sorprendiendo a la comunidad de desarrolladores con sus nuevas características y mejoras. En esta ocasión, descubriremos las actualizaciones más recientes que han llegado a Scala, desde las mejoras en la sintaxis y el rendimiento hasta las nuevas herramientas que han surgido. ¡Prepárate para sumergirte en el fascinante universo de Scala y descubrir las últimas innovaciones que lo hacen aún más poderoso y versátil!
En caso de que quieras formarte en el uso de Scala te recomendamos visitar nuestro curso de Scala.
Los tipos de intersección y unión son ligeramente diferentes:
Empecemos por los tipos de unión. Un tipo de unión, expresado como A | B, representa una unión "verdadera", a diferencia de una unión disjunta, la cual separa entre “izquierda” y “derecha”. En este caso no hay izquierda y derecha, por lo que literalmente podemos hacer esto:
val a: A | B = new A()
val b: A | B = new B()
Como son commutativas, A | B es lo mismo que B | A.
Los tipos de intersección (expresados cómo A y B) representan un concepto dual para los tipos de unión, y al igual que con los tipos de unión, ya hay construcciones similares presentes en Scala. Ya podemos combinar los rasgos B y C en alguna clase / rasgo / objeto A, lo que resulta en el tipo de intersección "A con B con C".
La principal diferencia entre estas combinaciones y los tipos de intersección que estamos obteniendo en Scala 3 es la conmutatividad: A con B no es lo mismo que B con A, al menos desde la perspectiva del sistema de tipos, mientras que A y B y B y A son lo mismo y se pueden usar indistintamente.
Como sabemos los trait no pueden tener parámetros, sino que usamos clases abstractas para ello. Sin embargo, las clases abstractas no se pueden mezclar en diferentes partes de la jerarquía de clases.
Ahora los traits también obtienen parámetros, así que podemos usar una sintaxis como esta:
trait Foo(val s: String) {
...
}
Esto realmente no compilaría ya que tanto Foo ("a") como Foo ("b") se han mezclado en algún momento. Para que esto funcione hay que seguir las siguientes reglas:
Hay dos grandes cambios respecto a los tipos de funciones:
En las versiones anteriores ya contábamos con métodos dependientes, pero ahora tenemos la posibilidad de convertir dicho método en una función, lo cual es común en Scala, pero era imposible para los métodos cuyo tipo de retorno era una ruta dependiente del tipo de entrada. Ahora podemos hacer esto:
def fooMethod(a: A): a.Foo = a.key
val fooFunction: (a: A) => a.Foo = fooMethod
La segunda característica más importante, son los tipos de funciones implícitas. Por ejemplo:
type MyFunction[B] = implicit A => B
Al igual que en otros escenarios con implicaciones, esto significa que si se encuentra un valor implícito de tipo A en el ámbito, se pasará; si no hay una A implícita en el ámbito, debe pasarse explícitamente o la compilación fallará.
Así que dado este método método:
def foo(f: Foo)(implicit a: A): B = ???
Podemos reescribirlo de la siguiente forma:
type MyFunction = implicit A => B
def foo(f: Foo): MyFunction
Esto nos permite definir foo cómo un valor de función. Sin tipos de funciones implícitas, solo podemos definirlo como un método, ya que en Scala 2 tener parámetros implícitos en un método automáticamente significa que no se puede expresar como una función.
Las tuplas ya no se implementan a través de los traits de TupleN que terminan (bastante arbitrariamente) en Tuple22. En su lugar, se implementan de manera similar a las Listas, con su estructura recursiva, lo que significa que básicamente se están convirtiendo en HLists sin forma.
Aunque esto significa que el límite de 22 se ha ido, esa no es la principal ventaja. La mejora principal se da en la forma en que tratamos las tuplas en sí mismas, porque las tuplas anidadas ahora pueden tratarse como planas; (a, b, c) serán exactamente las mismas que (a, (b, (c, ()))). Esto permite una buena programación genérica similar a lo que podemos hacer con HLists, como mapear sobre ellas con funciones monomórficas o incluso polimórficas.
Es similar a un tipo alias, pero en lugar de ser un alias solo para el programador, también es un alias para el compilador, lo que significa que el compilador realmente diferencia los dos.
type Nombre = String
Esto nos da la posibilidad de usar "Nombre" como un tipo, lo que puede hacer que el código sea más comprensible. Sin embargo, nada nos impide pasar realmente una cadena "normal" donde se requiere un nombre. Peor aún, si tenemos otro alias de tipo, por ejemplo: Apellido = String, nada nos impide pasar accidentalmente un Apellido donde se espera Nombre y viceversa, ya que desde el punto de vista del compilador, todos son solo Strings.
Si queremos lograr el comportamiento en el que el compilador evite dicho uso, debemos recurrir a clases de valor. Esto significa escribir algo como esto:
class FirstName(val underlying: String) extends AnyVal
class LastName(val underlying: String) extends AnyVal
Esto es un poco repetitivo y tiene un leve impacto en el rendimiento. Si añadimos la extensión AnyVal, ella se encarga de parte de esa penalización de rendimiento, pero no en todos los casos de uso y además nuestro código queda un poco feo..
Hay una biblioteca que permite definirlos de una manera un poco más elegante:
@newtype case class FirstName(underlying: String)
pero es una sintaxis bastante repetitiva. Además, estos nuevos tipos deben definirse en un objeto u objeto de paquete.
Scala 3 ha introducido nuevos tipos opacos.
opaque type FirstName = String
El concepto newType y opaque type con prácticamente el mismo. Nombre es un tipo en sí mismo, y pasar un valor de tipo Apellido donde se espera que Nombre no se compile. Puede haber cierta resistencia a introducir una nueva palabra clave, en cuyo caso la propuesta es usar: nuevo tipo Nombre = String.
Scala 3 obtiene soporte de lenguaje completo para lambdas sin tener que recurrir a feeds o bibliotecas externas.
Supongamos que necesitamos F [_], pero queremos pasar un constructor que necesita dos tipos (como Mapa o O), o en otras palabras, queremos pasar (﹡ → ﹡) → ﹡ donde se necesita ﹡ → ﹡.
En lugar de tener que definir un lambda como este:
({ type T[A] = Map[Int, A] })#T
Lo haremos de una forma más simple:
[A] => Map[Int, A]
Los parámetros de tipo lambdas soportan variaciones y límites, por ejemplo:
[+A, B <: C] => Whatever[A, B, C]
Hay situaciones en las que solo necesitamos algunos parámetros en la firma, por ejemplo para la evidencia en restricciones de tipo generalizadas, y nunca se utilizan en el propio cuerpo. Se sigue generando un código innecesario para dichos parámetros, que se puede evitar con la palabra clave "erased".
Por ejemplo:
def foo[S, T](s: S, t: T)(implicit ev: S =:= T)
Se usa como:
def foo[S, T](s: S, t: T)(implicit erased ev: S =:= T)
Por lo tanto, siempre que tengamos uno o más parámetros que se usen solo para la verificación de tipos, el uso de la palabra clave "erased" hará que el código sea más eficaz.
Una de las construcciones más torpes de Scala tiene un rediseño completo, lo que significa reemplazar el código de esta manera:
Con este código:
Pueden contener miembros personalizados y algunos métodos prácticos ya predefinidos.
Ejemplo:
El compilador ahora coincidirá con los tipos al comparar valores y fallará en la compilación si hay una falta de coincidencia.
Ya podemos modelar algo como “foo” == 123 mediante clases (en realidad se realiza en varias bibliotecas), pero con Scala 3 obtenemos el soporte de idioma nativo.
El compilador ahora requerirá una importación de características de idioma no solo cuando se define una conversión implícita sino también cuando se aplica.
Esto se basa en la función de tipos de unión, que nos permite definir el tipo de resultado de operaciones que pueden generar una excepción de puntero nulo (útil para la interoperabilidad conJava) como Foo | nulo.
Tal y cómo hemos comentado al inicio de la entrada, si deseas seguir aprendiendo este lenguaje de programación, puedes consultar nuestro curso de Scala donde encontrarás numerosos recursos que te permitirán impulsar tu carrera. Además, si deseas introducirte en este mundo puedes consultar nuestro tutorial de primeros pasos con Scala o nuestro tutorial más avanzado sobre el Uso de las Clases Case y Pattern Matching en Scala.