Tutorial - Novedades de Scala 3: Conoce todo lo que nos trae esta nueva versión

Duración:
30'
Nivel:
intermedio
Curso relacionado:

Intersecciones y uniones

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.

RecomendadoCurso de Scala

Parámetros Trait

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:

  • Solo las clases pueden pasar argumentos a sus traits principales; Los traits no pueden pasar argumentos a los rasgos.
  • Cuando una clase C extiende un trait T con parámetros, debe proporcionar los argumentos a T, excepto en el caso de que C tenga una superclase que también extienda de T; en ese caso, es la superclase la que debe proporcionar los argumentos y no C.

Tipos de funciones

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.

Generando tuplas

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.

Tipos opacos

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.

Tipo lambdas

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]

Parámetros borrados

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.

Enumeraciones

Una de las construcciones más torpes de Scala tiene un rediseño completo, lo que significa reemplazar el código de esta manera:

object WeekDay extends Enumeration {

type WeekDay = Value

val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value

}

Con este código:

enum WeekDay {

case Mon, Tue, Wed, Thu, Fri, Sat, Sun

}

Pueden contener miembros personalizados y algunos métodos prácticos ya predefinidos.

Ejemplo:

enum Weekday(val index: Int) {

private def next(i: Int) = (i + 1) % 7

private def prev(i: Int) = (i + 7) - 1 % 7

def nextDay = Weekday.enumValue(next(index))

def prevDay = Weekday.enumValue(prev(index))

case Mon   extends Weekday(0)

case Tue   extends Weekday(1)

...

}

Igualdad multiversal

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.

Restricción de conversiones implícitas

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.

Seguridad nula

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 con Java) como Foo | nulo.

Solicita información sobre Scala

En Imagina llevamos más de 11 años ofreciendo formación para empresas, estamos especializados en el área técnica y de ofimática, adaptando nuestras formaciones a vuestras necesidades. Déjanos tus datos, y nos pondremos en contacto contigo para informarte sobre el curso que mejor se ajuste a lo que buscas. Cuéntanos tus necesidades y podremos asesorarte sobre la modalidad que mejor se adapte: En directo, En directo a Medida u Online.

España