Scala ( / ˈ s k ɑː l ə / SKAH -lah ) [8] es un potente lenguaje de programación de propósito general de alto nivel y tipado estático que admite tanto la programación orientada a objetos como la programación funcional . Diseñado para ser conciso, [9] muchas de las decisiones de diseño de Scala tienen como objetivo abordar las críticas a Java . [7]
El código fuente de Scala se puede compilar en código de bytes de Java y ejecutar en una máquina virtual Java (JVM). Scala también se puede compilar en JavaScript para ejecutarlo en un navegador o directamente en un ejecutable nativo. En JVM, Scala proporciona interoperabilidad de lenguaje con Java , de modo que se puede hacer referencia directamente a las bibliotecas escritas en cualquiera de los idiomas en código Scala o Java. [ 10] Al igual que Java, Scala está orientado a objetos y utiliza una sintaxis denominada llave que es similar al lenguaje C. Desde Scala 3, también existe una opción para utilizar la regla fuera de juego (sangría) para estructurar bloques , y se recomienda su uso. Martin Odersky ha dicho que este resultó ser el cambio más productivo introducido en Scala 3. [11]
A diferencia de Java, Scala tiene muchas características de los lenguajes de programación funcionales (como Scheme , Standard ML y Haskell ), incluido el curry , la inmutabilidad , la evaluación diferida y la coincidencia de patrones . También tiene un sistema de tipos avanzado que admite tipos de datos algebraicos , covarianza y contravarianza , tipos de orden superior (pero no tipos de rango superior ), tipos anónimos , sobrecarga de operadores , parámetros opcionales , parámetros con nombre , cadenas sin formato y una excepción experimental. Versión de efectos algebraicos que puede verse como una versión más potente de las excepciones comprobadas de Java . [12]
El nombre Scala es un acrónimo de escalable y lenguaje , lo que significa que está diseñado para crecer con las demandas de sus usuarios. [13]
El diseño de Scala se inició en 2001 en la École Polytechnique Fédérale de Lausanne (EPFL) (en Lausana , Suiza ) por Martin Odersky . Fue una continuación del trabajo en Funnel, un lenguaje de programación que combina ideas de programación funcional y redes de Petri . [14] Odersky trabajó anteriormente en Generic Java y javac , el compilador Java de Sun. [14]
Después de un lanzamiento interno a finales de 2003, Scala se lanzó públicamente a principios de 2004 en la plataforma Java , [15] [7] [14] [16] Le siguió una segunda versión (v2.0) en marzo de 2006. [7]
El 17 de enero de 2011, el equipo de Scala ganó una subvención de investigación de cinco años de más de 2,3 millones de euros del Consejo Europeo de Investigación . [17] El 12 de mayo de 2011, Odersky y sus colaboradores lanzaron Typesafe Inc. (más tarde rebautizada como Lightbend Inc. ), una empresa para brindar soporte comercial, capacitación y servicios para Scala. Typesafe recibió una inversión de 3 millones de dólares en 2011 de Greylock Partners . [18] [19] [20] [21]
Scala se ejecuta en la plataforma Java ( máquina virtual Java ) y es compatible con los programas Java existentes . [15] Como las aplicaciones de Android generalmente se escriben en Java y se traducen del código de bytes de Java al código de bytes de Dalvik (que puede traducirse posteriormente a código de máquina nativo durante la instalación) cuando se empaquetan, la compatibilidad de Java de Scala lo hace muy adecuado para el desarrollo de Android, especialmente cuando se prefiere un enfoque funcional. [22]
La distribución de software Scala de referencia, incluidos el compilador y las bibliotecas, se publica bajo la licencia Apache . [23]
Scala.js es un compilador de Scala que compila en JavaScript, lo que permite escribir programas Scala que se pueden ejecutar en navegadores web o Node.js. [24] El compilador, en desarrollo desde 2013, se anunció como ya no experimental en 2015 (v0.6). La versión v1.0.0-M1 se lanzó en junio de 2018 y la versión 1.1.1 en septiembre de 2020. [25]
Scala Native es un compilador de Scala que apunta a la infraestructura del compilador LLVM para crear código ejecutable que utiliza un tiempo de ejecución administrado liviano, que utiliza el recolector de basura Boehm . El proyecto está dirigido por Denys Shabalin y tuvo su primera versión, 0.1, el 14 de marzo de 2017. El desarrollo de Scala Native comenzó en 2015 con el objetivo de ser más rápido que la compilación justo a tiempo para JVM al eliminar la compilación en tiempo de ejecución inicial de código y también proporciona la capacidad de llamar a rutinas nativas directamente. [26] [27]
En junio de 2004 se lanzó un compilador de Scala de referencia dirigido a .NET Framework y su Common Language Runtime , [14] pero se eliminó oficialmente en 2012. [28]
El programa Hello World escrito en Scala 3 tiene esta forma:
@main def main () = println ( "¡Hola mundo!" )
A diferencia de la aplicación independiente Hello World para Java , no hay declaración de clase y nada se declara estático.
Cuando el programa se almacena en el archivo HelloWorld.scala , el usuario lo compila con el comando:
$ scalac HolaMundo.scala
y lo ejecuta con
$ Scala Hola Mundo
Esto es análogo al proceso para compilar y ejecutar código Java. De hecho, el modelo de compilación y ejecución de Scala es idéntico al de Java, lo que lo hace compatible con herramientas de compilación de Java como Apache Ant .
Una versión más corta del programa Scala "Hello World" es:
println ( "¡Hola mundo!" )
Scala incluye un shell interactivo y soporte para secuencias de comandos. [29] Guardado en un archivo llamado HelloWorld2.scala
, esto se puede ejecutar como un script usando el comando:
$scala HolaMundo2.scala
Los comandos también se pueden ingresar directamente en el intérprete de Scala, usando la opción -e :
$ scala -e 'println("¡Hola, mundo!")'
Las expresiones se pueden ingresar de forma interactiva en REPL :
$ scala Bienvenido a Scala 2.12.2 (VM de servidor Java HotSpot(TM) de 64 bits, Java 1.8.0_131). Escriba expresiones para evaluación. O prueba: ayuda.scala> Lista(1, 2, 3).map(x => x * x) res0: Lista[Int] = Lista(1, 4, 9)escala>
El siguiente ejemplo muestra las diferencias entre la sintaxis de Java y Scala. La función mathFunction toma un número entero, lo eleva al cuadrado y luego suma la raíz cúbica de ese número al logaritmo natural de ese número, devolviendo el resultado (es decir, ):
Algunas diferencias sintácticas en este código son:
Int, Double, Boolean
en lugar de int, double, boolean
.def
.val
(indica una variable inmutablevar
) o (indica una variable mutable ).return
operador es innecesario en una función (aunque está permitido); el valor de la última declaración o expresión ejecutada es normalmente el valor de la función.(Type) foo
, Scala utiliza foo.asInstanceOf[Type]
, o una función especializada como toDouble
o toInt
.import foo.*;
, Scala usa import foo._
.foo()
también se puede llamar simplemente foo
; El método thread.send(signo)
también se puede llamar simplemente thread send signo
; y el método foo.toString()
también se puede llamar simplemente foo toString
.Estas flexibilizaciones sintácticas están diseñadas para permitir la compatibilidad con lenguajes de dominios específicos .
Algunas otras diferencias sintácticas básicas:
array(i)
en lugar de array[i]
. (Internamente en Scala, el primero se expande a array.apply(i) que devuelve la referencia)List[String]
en lugar de Java List<String>
.void
, Scala tiene la clase singleton Unit
real (ver más abajo).El siguiente ejemplo contrasta la definición de clases en Java y Scala.
El código anterior muestra algunas de las diferencias conceptuales entre el manejo de clases de Java y Scala:
object
en lugar de class
. Es común colocar variables y métodos estáticos en un objeto singleton con el mismo nombre que el nombre de la clase, que luego se conoce como objeto complementario . [15] (La clase subyacente para el objeto singleton tiene un objeto $
adjunto. Por lo tanto, para class Foo
el objeto complementario object Foo
, debajo del capó hay una clase Foo$
que contiene el código del objeto complementario y se crea un objeto de esta clase, utilizando el patrón singleton ).val
o var
, los campos también se definen con el mismo nombre y se inicializan automáticamente a partir de los parámetros de clase. (En el fondo, el acceso externo a los campos públicos siempre pasa a través de los métodos de acceso (captador) y mutador (establecedor), que se crean automáticamente. La función de acceso tiene el mismo nombre que el campo, por lo que en el ejemplo anterior no es necesario declarar explícitamente métodos de acceso). Tenga en cuenta que también se pueden declarar constructores alternativos, como en Java. El código que iría al constructor predeterminado (aparte de inicializar las variables miembro) va directamente al nivel de clase.addPoint
, el ejemplo de Scala define +=
, que luego se invoca con notación infija como grid += this
.public
.Scala tiene el mismo modelo de compilación que Java y C# , es decir, compilación separada y carga dinámica de clases , de modo que el código Scala pueda llamar a las bibliotecas Java.
Las características operativas de Scala son las mismas que las de Java. El compilador de Scala genera código de bytes que es casi idéntico al generado por el compilador de Java. [15] De hecho, el código Scala se puede descompilar en código Java legible, con la excepción de ciertas operaciones del constructor. Para la máquina virtual Java (JVM), el código Scala y el código Java son indistinguibles. La única diferencia es una biblioteca de tiempo de ejecución adicional, scala-library.jar
. [30]
Scala agrega una gran cantidad de características en comparación con Java y tiene algunas diferencias fundamentales en su modelo subyacente de expresiones y tipos, lo que hace que el lenguaje sea teóricamente más limpio y elimina varios casos extremos en Java. Desde la perspectiva de Scala, esto es prácticamente importante porque varias características agregadas en Scala también están disponibles en C#.
Como se mencionó anteriormente, Scala tiene mucha flexibilidad sintáctica, en comparación con Java. Los siguientes son algunos ejemplos:
"%d apples".format(num)
y "%d apples" format num
son equivalentes. De hecho, a los operadores aritméticos les gusta +
y <<
se tratan como cualquier otro método, ya que se permite que los nombres de funciones consistan en secuencias de símbolos arbitrarios (con algunas excepciones para elementos como paréntesis, corchetes y llaves que deben manejarse de manera especial); El único tratamiento especial que reciben estos métodos con nombres de símbolos se refiere al manejo de la precedencia.apply
y update
tienen formas cortas sintácticas. foo()
—donde foo
hay un valor (objeto único o instancia de clase): es la abreviatura de foo.apply()
y foo() = 42
es la abreviatura de foo.update(42)
. De manera similar, foo(42)
es la abreviatura de foo.apply(42)
y foo(4) = 2
es la abreviatura de foo.update(4, 2)
. Esto se utiliza para clases de colección y se extiende a muchos otros casos, como las células STM .def foo = 42
) y con padres vacíos ( ). def foo() = 42
Cuando se llama a un método de pares vacíos, se pueden omitir los paréntesis, lo cual es útil cuando se llama a bibliotecas Java que no conocen esta distinción, por ejemplo, usando foo.toString
en lugar de foo.toString()
. Por convención, un método debe definirse con pares vacíos cuando realiza efectos secundarios .:
) esperan que el argumento esté en el lado izquierdo y el receptor en el lado derecho. Por ejemplo, 4 :: 2 :: Nil
es lo mismo que Nil.::(2).::(4)
, la primera forma corresponde visualmente al resultado (una lista con el primer elemento 4 y el segundo elemento 2).trait FooLike { var bar: Int }
, una implementación puede ser . El sitio de la llamada aún podrá utilizar un formato conciso .object Foo extends FooLike { private var x = 0; def bar = x; def bar_=(value: Int) { x = value }} } }
foo.bar = 42
breakable { ... if (...) break() ... }
parece como si breakable
fuera una palabra clave definida por el lenguaje, pero en realidad es solo un método que toma un argumento de procesador . Los métodos que toman procesadores o funciones a menudo los colocan en una segunda lista de parámetros, lo que permite mezclar la sintaxis de paréntesis y llaves: Vector.fill(4) { math.random }
es lo mismo que Vector.fill(4)(math.random)
. La variante de llaves permite que la expresión abarque varias líneas.map
, flatMap
y filter
.Por sí solas, estas pueden parecer opciones cuestionables, pero en conjunto sirven para permitir que lenguajes de dominio específicos se definan en Scala sin necesidad de ampliar el compilador. Por ejemplo, la sintaxis especial de Erlangactor ! message
para enviar un mensaje a un actor, es decir , puede implementarse (y se implementa) en una biblioteca Scala sin necesidad de extensiones de lenguaje.
Java hace una clara distinción entre tipos primitivos (por ejemplo, int
y boolean
) y tipos de referencia (cualquier clase ). Sólo los tipos de referencia forman parte del esquema de herencia, que se deriva de java.lang.Object
. En Scala, todos los tipos heredan de una clase de nivel superior Any
, cuyos hijos inmediatos son AnyVal
(tipos de valor, como Int
y Boolean
) y AnyRef
(tipos de referencia, como en Java). Esto significa que la distinción de Java entre tipos primitivos y tipos encajonados (por ejemplo, int
vs. Integer
) no está presente en Scala; boxing y unboxing es completamente transparente para el usuario. Scala 2.10 permite que el usuario defina nuevos tipos de valores.
En lugar de los bucles " foreach " de Java para recorrer un iterador, Scala tiene for
expresiones -, que son similares a las listas por comprensión en lenguajes como Haskell, o una combinación de listas por comprensión y expresiones generadoras en Python . Las expresiones For que utilizan la palabra clave permiten generar yield
una nueva colección iterando sobre una existente, devolviendo una nueva colección del mismo tipo. El compilador los traduce en una serie de llamadas y map
. Cuando no se utiliza, el código se aproxima a un bucle de estilo imperativo, traduciéndolo a .flatMap
filter
yield
foreach
Un ejemplo sencillo es:
val s = for ( x <- 1 a 25 si x * x > 50 ) produce 2 * x
El resultado de ejecutarlo es el siguiente vector:
Vector(16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50)
(Tenga en cuenta que la expresión 1 to 25
no tiene una sintaxis especial. El método to
más bien se define en la biblioteca estándar de Scala como un método de extensión en números enteros, utilizando una técnica conocida como conversiones implícitas [32] que permite agregar nuevos métodos a los tipos existentes).
Un ejemplo más complejo de iteración sobre un mapa es:
// Dado un mapa que especifica los usuarios de Twitter mencionados en un conjunto de tweets // y el número de veces que se mencionó a cada usuario, busque los usuarios // en un mapa de políticos conocidos y devuelva un nuevo mapa que proporcione solo // los demócratas políticos (como objetos, más que como hilos). val dem_mentions = for ( mención , veces ) <- cuenta de menciones <- cuentas . obtener ( mencionar ) si cuenta . partido == rendimiento "demócrata" ( cuenta , tiempos )
La expresión (mention, times) <- mentions
es un ejemplo de coincidencia de patrones (ver más abajo). La iteración sobre un mapa devuelve un conjunto de tuplas clave-valor , y la coincidencia de patrones permite desestructurar fácilmente las tuplas en variables separadas para la clave y el valor. De manera similar, el resultado de la comprensión también devuelve tuplas clave-valor, que se reconstruyen automáticamente en un mapa porque el objeto fuente (de la variable mentions
) es un mapa. Tenga en cuenta que si, mentions
en cambio, tuviera una lista, conjunto, matriz u otra colección de tuplas, exactamente el mismo código anterior produciría una nueva colección del mismo tipo.
Si bien admite todas las funciones orientadas a objetos disponibles en Java (y, de hecho, las aumenta de varias maneras), Scala también proporciona una gran cantidad de capacidades que normalmente se encuentran solo en lenguajes de programación funcionales . Juntas, estas características permiten que los programas Scala se escriban en un estilo casi completamente funcional y también permiten mezclar estilos funcionales y orientados a objetos.
Ejemplos son:
A diferencia de C o Java , pero similar a lenguajes como Lisp , Scala no hace distinción entre declaraciones y expresiones . Todas las declaraciones son, de hecho, expresiones que se evalúan con algún valor. En Scala se considera que las funciones que se declararían como retornantes void
en C o Java, y declaraciones como while
esa que lógicamente no devuelven un valor, devuelven el tipo Unit
, que es un tipo singleton , con un solo objeto de ese tipo. Las funciones y operadores que nunca regresan (por ejemplo, el throw
operador o una función que siempre sale de forma no local usando una excepción) lógicamente tienen un tipo de retorno Nothing
, un tipo especial que no contiene objetos; es decir, un tipo inferior , es decir, una subclase de todos los tipos posibles. (Esto a su vez hace que el tipo Nothing
sea compatible con todos los tipos, lo que permite que la inferencia de tipos funcione correctamente) .
De manera similar, una if-then-else
"declaración" es en realidad una expresión que produce un valor, es decir, el resultado de evaluar una de las dos ramas. Esto significa que dicho bloque de código se puede insertar dondequiera que se desee una expresión, obviando la necesidad de un operador ternario en Scala:
Por razones similares, return
las declaraciones son innecesarias en Scala y, de hecho, no se recomiendan. Como en Lisp , la última expresión en un bloque de código es el valor de ese bloque de código, y si el bloque de código es el cuerpo de una función, la función lo devolverá.
Para dejar claro que todas las funciones son expresiones, incluso los métodos que devuelven Unit
se escriben con un signo igual
def printValue ( x : String ): Unidad = println ( "Me comí un %s" . formato ( x ))
o de manera equivalente (con inferencia de tipos y omitiendo la nueva línea innecesaria):
def printValue ( x : String ) = println ( formato "Me comí un %s" x )
Debido a la inferencia de tipos , el tipo de variables, los valores de retorno de la función y muchas otras expresiones normalmente se pueden omitir, ya que el compilador puede deducirlo. Algunos ejemplos son val x = "foo"
(para una constante inmutable u objeto inmutable ) o var x = 1.5
(para una variable cuyo valor se puede cambiar más adelante). La inferencia de tipos en Scala es esencialmente local, en contraste con el algoritmo Hindley-Milner más global utilizado en Haskell , ML y otros lenguajes más puramente funcionales. Esto se hace para facilitar la programación orientada a objetos. El resultado es que aún es necesario declarar ciertos tipos (en particular, los parámetros de función y los tipos de retorno de funciones recursivas ), por ejemplo
def formatApples ( x : Int ) = "Comí %d manzanas" . formato ( x )
o (con un tipo de retorno declarado para una función recursiva)
def factorial ( x : Int ): Int = si x == 0 entonces 1 si no x * factorial ( x - 1 )
En Scala, las funciones son objetos y existe una sintaxis conveniente para especificar funciones anónimas . Un ejemplo es la expresión x => x < 2
, que especifica una función con un parámetro, que compara su argumento para ver si es menor que 2. Es equivalente a la forma Lisp (lambda (x) (< x 2))
. x
Tenga en cuenta que no es necesario especificar explícitamente ni el tipo ni el tipo de retorno y, por lo general, se pueden inferir mediante inferencia de tipos ; pero se pueden especificar explícitamente, por ejemplo, como (x: Int) => x < 2
o incluso (x: Int) => (x < 2): Boolean
.
Las funciones anónimas se comportan como cierres verdaderos en el sentido de que capturan automáticamente cualquier variable que esté léxicamente disponible en el entorno de la función adjunta. Esas variables estarán disponibles incluso después de que regrese la función adjunta y, a diferencia del caso de las clases internas anónimas de Java , no es necesario declararlas como finales. (Incluso es posible modificar dichas variables si son mutables, y el valor modificado estará disponible la próxima vez que se llame a la función anónima).
Una forma aún más corta de función anónima utiliza variables de marcador de posición : por ejemplo, la siguiente:
list map { x => sqrt(x) }
se puede escribir de manera más concisa como
list map { sqrt(_) }
o incluso
list map sqrt
Scala impone una distinción entre variables inmutables y mutables. Las variables mutables se declaran mediante la var
palabra clave y los valores inmutables se declaran mediante la val
palabra clave. Una variable declarada usando la val
palabra clave no se puede reasignar de la misma manera que una variable declarada usando la final
palabra clave no se puede reasignar en Java. val
s son sólo superficialmente inmutables, es decir, no se garantiza que un objeto al que hace referencia un val sea inmutable.
Sin embargo, la convención recomienda las clases inmutables y la biblioteca estándar de Scala proporciona un rico conjunto de clases de colección inmutables . Scala proporciona variantes mutables e inmutables de la mayoría de las clases de colección, y la versión inmutable siempre se usa a menos que la versión mutable se importe explícitamente. [34] Las variantes inmutables son estructuras de datos persistentes que siempre devuelven una copia actualizada de un objeto antiguo en lugar de actualizar el objeto antiguo de forma destructiva en su lugar. Un ejemplo de esto son las listas enlazadas inmutables donde anteponer un elemento a una lista se realiza devolviendo un nuevo nodo de lista que consta del elemento y una referencia al final de la lista. Solo se puede agregar un elemento a una lista anteponiendo todos los elementos de la lista anterior a una lista nueva con solo el elemento nuevo. De la misma manera, insertar un elemento en medio de una lista copiará la primera mitad de la lista, pero mantendrá una referencia a la segunda mitad de la lista. A esto se le llama compartir estructural. Esto permite una simultaneidad muy sencilla: no se necesitan bloqueos ya que nunca se modifican objetos compartidos. [35]
La evaluación es estricta ("ansiosa") por defecto. En otras palabras, Scala evalúa las expresiones tan pronto como están disponibles, en lugar de según sea necesario. Sin embargo, es posible declarar una variable no estricta ("lazy") con la lazy
palabra clave, lo que significa que el código para producir el valor de la variable no se evaluará hasta la primera vez que se haga referencia a la variable. También existen colecciones no estrictas de varios tipos (como el tipo Stream
, una lista enlazada no estricta), y cualquier colección se puede convertir en no estricta con el view
método. Las colecciones no estrictas proporcionan un buen ajuste semántico a cosas como datos producidos por el servidor, donde la evaluación del código para generar elementos posteriores de una lista (que a su vez desencadena una solicitud a un servidor, posiblemente ubicado en otro lugar de la web) solo sucede cuando los elementos son realmente necesarios.
Los lenguajes de programación funcional suelen proporcionar optimización de llamadas de cola para permitir un uso extensivo de la recursividad sin problemas de desbordamiento de pila . Las limitaciones en el código de bytes de Java complican la optimización de las llamadas finales en la JVM. En general, una función que se llama a sí misma con una llamada final se puede optimizar, pero las funciones mutuamente recursivas no. Se han sugerido trampolines como solución alternativa. [36] La biblioteca Scala proporciona soporte para trampolín con el objeto scala.util.control.TailCalls
desde Scala 2.8.0 (lanzado el 14 de julio de 2010). Opcionalmente, una función puede tener anotaciones con @tailrec
, en cuyo caso no se compilará a menos que sea recursiva al final. [37]
Un ejemplo de esta optimización podría implementarse utilizando la definición factorial . Por ejemplo, la versión recursiva del factorial:
def factorial ( n : Int ): Int = si n == 0 entonces 1 si no n * factorial ( n - 1 )
Podría optimizarse para la versión recursiva de cola de esta manera:
@tailrec def factorial ( n : Int , accum : Int ): Int = si n == 0 entonces accum else factorial ( n - 1 , n * accum )
Sin embargo, esto podría comprometer la componibilidad con otras funciones debido al nuevo argumento en su definición, por lo que es común usar cierres para preservar su firma original:
def factorial ( n : Int ): Int = @tailrec def bucle ( actual : Int , acumulado : Int ): Int = si n == 0 entonces acumular else bucle ( actual - 1 , n * accum ) loop ( n , 1 ) // Llamada al cierre usando el caso base factorial final
Esto garantiza la optimización de las llamadas finales y, por tanto, evita un error de desbordamiento de pila.
Scala tiene soporte incorporado para la coincidencia de patrones , que puede considerarse como una versión más sofisticada y extensible de una declaración de cambio , donde se pueden hacer coincidir tipos de datos arbitrarios (en lugar de solo tipos simples como enteros, booleanos y cadenas), incluidos los arbitrarios. anidación. Se proporciona un tipo especial de clase conocida como clase de caso , que incluye soporte automático para la coincidencia de patrones y se puede utilizar para modelar los tipos de datos algebraicos utilizados en muchos lenguajes de programación funcionales. (Desde la perspectiva de Scala, una clase de caso es simplemente una clase normal para la cual el compilador agrega automáticamente ciertos comportamientos que también podrían proporcionarse manualmente, por ejemplo, definiciones de métodos que proporcionan comparaciones profundas y hash, y desestructuran una clase de caso en su constructor. parámetros durante la coincidencia de patrones.)
Un ejemplo de una definición del algoritmo de clasificación rápida que utiliza la coincidencia de patrones es este:
def qsort ( lista : Lista [ Int ]): Lista [ Int ] = lista de coincidencias caso Nil => Nil caso pivote :: cola => val ( más pequeño , resto ) = cola . partición ( _ < pivote ) qsort ( más pequeño ) ::: pivote :: qsort ( resto )
La idea aquí es dividir una lista en elementos menores que un pivote y elementos no menores, ordenar recursivamente cada parte y pegar los resultados junto con el pivote intermedio. Esto utiliza la misma estrategia de divide y vencerás de mergesort y otros algoritmos de clasificación rápida.
El match
operador se utiliza para hacer coincidencias de patrones en el objeto almacenado en list
. Cada case
expresión se prueba por turno para ver si coincide, y la primera coincidencia determina el resultado. En este caso, Nil
solo coincide con el objeto literal Nil
, pero pivot :: tail
coincide con una lista que no está vacía y simultáneamente desestructura la lista de acuerdo con el patrón dado. En este caso, el código asociado tendrá acceso a una variable local denominada que pivot
ocupa el principio de la lista y a otra variable tail
que ocupa el final de la lista. Tenga en cuenta que estas variables son de solo lectura y son semánticamente muy similares a los enlaces de variables establecidos mediante el letoperador en Lisp y Scheme.
La coincidencia de patrones también ocurre en declaraciones de variables locales. En este caso, el valor de retorno de la llamada a tail.partition
es una tupla (en este caso, dos listas). (Las tuplas se diferencian de otros tipos de contenedores, por ejemplo las listas, en que siempre tienen un tamaño fijo y los elementos pueden ser de diferentes tipos, aunque aquí ambos son iguales.) La coincidencia de patrones es la forma más fácil de recuperar las dos partes de un contenedor. la tupla.
El formulario _ < pivot
es una declaración de una función anónima con una variable marcador de posición; consulte la sección anterior sobre funciones anónimas.
Aparecen los operadores de lista ::
(que agrega un elemento al comienzo de una lista, similar a cons
Lisp y Scheme) y :::
(que agrega dos listas juntas, similar a Lisp y Scheme). append
A pesar de las apariencias, ninguno de estos operadores tiene nada "integrado". Como se especificó anteriormente, cualquier cadena de símbolos puede servir como nombre de función, y un método aplicado a un objeto puede escribirse en estilo "infijo" sin punto ni paréntesis. La línea de arriba como está escrita:
qsort(smaller) ::: pivot :: qsort(rest)
también podría escribirse así:
qsort(rest).::(pivot).:::(qsort(smaller))
en notación de llamada de método más estándar. (Los métodos que terminan con dos puntos son asociativos por la derecha y se vinculan al objeto de la derecha).
En el ejemplo anterior de coincidencia de patrones, el cuerpo del match
operador es una función parcial , que consta de una serie de case
expresiones, prevaleciendo la primera expresión coincidente, similar al cuerpo de una declaración de cambio . Las funciones parciales también se utilizan en la parte de manejo de excepciones de una try
declaración:
intente ... detectar caso nfe : NumberFormatException => { println ( nfe ); Lista ( 0 ) } caso _ => Nulo
Finalmente, una función parcial se puede usar sola y el resultado de llamarla equivale a realizar una match
sobre ella. Por ejemplo, el código anterior para la clasificación rápida se puede escribir así:
val qsort : Lista [ Int ] => Lista [ Int ] = caso Nil => Nil caso pivote :: cola => val ( más pequeño , resto ) = cola . partición ( _ < pivote ) qsort ( más pequeño ) ::: pivote :: qsort ( resto )
Aquí se declara una variable de solo lectura cuyo tipo es una función de listas de números enteros a listas de números enteros, y la vincula a una función parcial. (Tenga en cuenta que el parámetro único de la función parcial nunca se declara ni se nombra explícitamente). Sin embargo, aún podemos llamar a esta variable exactamente como si fuera una función normal:
scala > qsort ( Lista ( 6 , 2 , 5 , 9 )) res32 : Lista [ Int ] = Lista ( 2 , 5 , 6 , 9 )
Scala es un lenguaje puro orientado a objetos en el sentido de que cada valor es un objeto . Los tipos de datos y comportamientos de los objetos se describen mediante clases y rasgos . Las abstracciones de clases se amplían mediante subclases y mediante un mecanismo de composición flexible basado en mixin para evitar los problemas de herencia múltiple .
Los rasgos son el reemplazo de Scala para las interfaces de Java . Las interfaces en las versiones de Java inferiores a 8 están muy restringidas y solo pueden contener declaraciones de funciones abstractas. Esto ha llevado a críticas de que proporcionar métodos convenientes en las interfaces es incómodo (los mismos métodos deben volver a implementarse en cada implementación) y que es imposible extender una interfaz publicada de manera compatible con versiones anteriores. Los rasgos son similares a las clases mixtas en el sentido de que tienen casi todo el poder de una clase abstracta regular, careciendo sólo de parámetros de clase (el equivalente de Scala a los parámetros del constructor de Java), ya que los rasgos siempre se mezclan con una clase. El super
operador se comporta especialmente en los rasgos, permitiendo que los rasgos se encadenen mediante composición además de herencia. El siguiente ejemplo es un sistema de ventanas simple:
Ventana de clase abstracta : // resumen def draw () clase SimpleWindow extiende la ventana : def draw () println ( "en SimpleWindow" ) // dibuja una ventana básica rasgo WindowDecoration extiende la ventana rasgo HorizontalScrollbarDecoration extiende WindowDecoration : // Aquí se necesita "anulación abstracta" para que "super()" funcione porque la // función principal es abstracta. Si fuera concreto, una "anulación" regular sería suficiente. anulación abstracta def draw () println ( "en HorizontalScrollbarDecoration" ) super . draw () // ahora dibuja una barra de desplazamiento horizontal rasgo VerticalScrollbarDecoration extiende WindowDecoration : anulación abstracta def draw () println ( "en VerticalScrollbarDecoration" ) super . draw () // ahora dibuja una barra de desplazamiento vertical rasgo TitleDecoration extiende WindowDecoration : anulación abstracta def draw () println ( "en TitleDecoration" ) super . draw () // ahora dibuja la barra de título
Una variable puede declararse así:
val mywin = nueva SimpleWindow con VerticalScrollbarDecoration con HorizontalScrollbarDecoration con TitleDecoration
El resultado de llamar mywin.draw()
es:
en TítuloDecoración en barra de desplazamiento horizontalDecoración en barra de desplazamiento verticalDecoración en ventana simple
En otras palabras, la llamada a draw
primero ejecutó el código en TitleDecoration
(el último rasgo mezclado), luego (a través de las super()
llamadas) volvió a pasar por los otros rasgos mezclados y finalmente al código en Window
, aunque ninguno de los rasgos heredados de unos a otros . Esto es similar al patrón decorador , pero es más conciso y menos propenso a errores, ya que no requiere encapsular explícitamente la ventana principal, reenviar explícitamente funciones cuya implementación no se modifica ni depender de la inicialización en tiempo de ejecución de las relaciones entre entidades. . En otros lenguajes, se podría lograr un efecto similar en tiempo de compilación con una larga cadena lineal de herencia de implementación , pero con la desventaja en comparación con Scala de que se tendría que declarar una cadena de herencia lineal para cada combinación posible de mezclas.
Scala está equipado con un expresivo sistema de tipo estático que principalmente impone el uso seguro y coherente de abstracciones. Sin embargo, el sistema de tipos no es sólido . [38] En particular, el sistema de tipos admite:
Scala es capaz de inferir tipos por uso. Esto hace que la mayoría de las declaraciones de tipos estáticos sean opcionales. No es necesario declarar explícitamente los tipos estáticos a menos que un error del compilador indique la necesidad. En la práctica, se incluyen algunas declaraciones de tipos estáticos para mayor claridad del código.
Una técnica común en Scala, conocida como "enriquecer mi biblioteca" [39] (originalmente denominada " proxeneta mi biblioteca " por Martin Odersky en 2006; [32] surgieron preocupaciones sobre esta frase debido a sus connotaciones negativas [40] e inmadurez [ 41] ), permite utilizar nuevos métodos como si se hubieran agregado a tipos existentes. Esto es similar al concepto de métodos de extensión de C# pero más poderoso, porque la técnica no se limita a agregar métodos y puede, por ejemplo, usarse para implementar nuevas interfaces. En Scala, esta técnica implica declarar una conversión implícita del tipo que "recibe" el método a un nuevo tipo (normalmente, una clase) que envuelve el tipo original y proporciona el método adicional. Si no se puede encontrar un método para un tipo determinado, el compilador busca automáticamente cualquier conversión implícita aplicable a tipos que proporcionen el método en cuestión.
Esta técnica permite agregar nuevos métodos a una clase existente utilizando una biblioteca complementaria de modo que solo el código que importa la biblioteca complementaria obtenga la nueva funcionalidad y el resto del código no se vea afectado.
El siguiente ejemplo muestra el enriquecimiento del tipo Int
con métodos isEven
y isOdd
:
objeto MisExtensiones : extensión ( i : Int ) def esPar = i % 2 == 0 def esImpar = ! incluso importar MisExtensiones . _ // llevar el enriquecimiento implícito al alcance 4 . es par // -> verdadero
La importación de los miembros de trae al alcance MyExtensions
la conversión implícita a la clase de extensión . [42]IntPredicates
La biblioteca estándar de Scala incluye soporte para futuros y promesas , además de las API de concurrencia estándar de Java. Originalmente, también incluía soporte para el modelo de actor , que ahora está disponible como una plataforma de fuente separada Akka [43] con licencia de Lightbend Inc. Los actores de Akka pueden distribuirse o combinarse con memoria transaccional de software ( transactors ). Las implementaciones alternativas de procesos secuenciales de comunicación (CSP) para el paso de mensajes basado en canales son la comunicación de objetos Scala [44] o simplemente a través de JCSP .
Un actor es como una instancia de hilo con un buzón. Puede crearse system.actorOf
anulando el receive
método para recibir mensajes y utilizando el !
método (signo de exclamación) para enviar un mensaje. [45]
El siguiente ejemplo muestra un EchoServer que puede recibir mensajes y luego imprimirlos.
val echoServer = actor ( nueva Ley : convertirse en : case msg => println ( "echo" + msg ) ) ecoServidor ! "Hola"
Scala también viene con soporte incorporado para programación de datos paralelos en forma de Colecciones Paralelas [46] integradas en su Biblioteca Estándar desde la versión 2.9.0.
El siguiente ejemplo muestra cómo utilizar Colecciones paralelas para mejorar el rendimiento. [47]
val URL = Lista ( "https://scala-lang.org" , "https://github.com/scala/scala" ) def de URL ( url : cadena ) = scala . yo . Fuente . desdeURL ( url ) . obtener líneas (). cadenamk ( "\n" ) val t = Sistema . URL de currentTimeMillis () . par . map ( fromURL ( _ )) // par devuelve la implementación paralela de una colección println ( "time: " + ( System . currentTimeMillis - t ) + "ms" )
Además de futuros y promesas, soporte de actores y paralelismo de datos , Scala también admite programación asincrónica con memoria transaccional de software y flujos de eventos. [48]
La solución de computación en clúster de código abierto más conocida escrita en Scala es Apache Spark . Además, Apache Kafka , la cola de mensajes de publicación y suscripción popular entre Spark y otras tecnologías de procesamiento de flujo, está escrita en Scala.
Hay varias formas de probar código en Scala. ScalaTest admite múltiples estilos de prueba y puede integrarse con marcos de prueba basados en Java. [49] ScalaCheck es una biblioteca similar a QuickCheck de Haskell . [50] specs2 es una biblioteca para escribir especificaciones de software ejecutables. [51] ScalaMock proporciona soporte para probar funciones de alto orden y curry. [52] JUnit y TestNG son marcos de prueba populares escritos en Java.
Scala se compara a menudo con Groovy y Clojure , otros dos lenguajes de programación que también utilizan JVM. Existen diferencias sustanciales entre estos lenguajes en el sistema de tipos, en la medida en que cada lenguaje admite programación funcional y orientada a objetos, y en la similitud de su sintaxis con la de Java.
Scala se escribe estáticamente , mientras que Groovy y Clojure se escriben dinámicamente . Esto hace que el sistema de tipos sea más complejo y difícil de entender, pero permite detectar casi todos los errores de tipo [38] en tiempo de compilación y puede dar como resultado una ejecución significativamente más rápida. Por el contrario, la escritura dinámica requiere más pruebas para garantizar la corrección del programa y, por lo tanto, es generalmente más lenta para permitir una mayor flexibilidad y simplicidad de programación. En cuanto a las diferencias de velocidad, las versiones actuales de Groovy y Clojure permiten anotaciones de tipo opcionales para ayudar a los programas a evitar la sobrecarga de la escritura dinámica en casos donde los tipos son prácticamente estáticos. Esta sobrecarga se reduce aún más cuando se utilizan versiones recientes de JVM, que se ha mejorado con una instrucción dinámica de invocación para métodos que se definen con argumentos escritos dinámicamente. Estos avances reducen la diferencia de velocidad entre la escritura estática y la dinámica, aunque un lenguaje de escritura estática, como Scala, sigue siendo la opción preferida cuando la eficiencia de la ejecución es muy importante.
En cuanto a los paradigmas de programación, Scala hereda el modelo orientado a objetos de Java y lo extiende de diversas formas. Groovy, aunque también está fuertemente orientado a objetos, está más centrado en reducir la verbosidad. En Clojure, se resta importancia a la programación orientada a objetos, siendo la programación funcional la principal fortaleza del lenguaje. Scala también tiene muchas facilidades de programación funcional, incluidas características que se encuentran en lenguajes funcionales avanzados como Haskell , y trata de ser agnóstico entre los dos paradigmas, permitiendo al desarrollador elegir entre los dos paradigmas o, más frecuentemente, alguna combinación de los mismos.
En cuanto a la similitud de sintaxis con Java, Scala hereda gran parte de la sintaxis de Java, como es el caso de Groovy. Clojure, por otro lado, sigue la sintaxis Lisp , que es diferente tanto en apariencia como en filosofía. [ cita necesaria ]
En 2013, cuando Scala estaba en la versión 2.10, ThoughtWorks Technology Radar, que es un informe bianual basado en la opinión de un grupo de tecnólogos senior, [122] recomendó la adopción de Scala en su categoría de lenguajes y marcos. [123]
En julio de 2014, esta evaluación se hizo más específica y ahora se refiere a “Scala, las partes buenas”, que se describe como “Para usar Scala con éxito, es necesario investigar el lenguaje y tener una opinión muy sólida sobre qué partes son correctas”. para usted, creando su propia definición de Scala, las partes buenas”. [124]
En la edición de 2018 de la encuesta State of Java , [125] que recopiló datos de 5160 desarrolladores sobre diversos temas relacionados con Java, Scala ocupa el tercer lugar en términos de uso de lenguajes alternativos en la JVM . En relación con la edición de la encuesta del año anterior, el uso de Scala entre los lenguajes JVM alternativos cayó del 28,4 % al 21,5 %, superado por Kotlin , que aumentó del 11,4 % en 2017 al 28,8 % en 2018. El índice de popularidad del lenguaje de programación, [126 ] que rastrea las búsquedas de tutoriales de idiomas, clasificó a Scala en el puesto 15 en abril de 2018 con una pequeña tendencia a la baja, y en el puesto 17 en enero de 2021. Esto convierte a Scala en el tercer lenguaje basado en JVM más popular después de Java y Kotlin , en el puesto 12.
Las clasificaciones de lenguajes de programación de RedMonk, que establecen clasificaciones basadas en la cantidad de proyectos de GitHub y preguntas formuladas en Stack Overflow , en enero de 2021 clasificaron a Scala en el puesto 14. [127] Aquí, Scala se colocó dentro de un grupo de lenguajes de segundo nivel, por delante de Go , PowerShell y Haskell , y detrás de Swift , Objective -C , Typescript y R.
El índice TIOBE [128] de popularidad de lenguajes de programación emplea clasificaciones en motores de búsqueda de Internet y recuentos de publicaciones similares para determinar la popularidad del lenguaje. En septiembre de 2021, Scala ocupaba el puesto 31. En este ranking, Scala estaba por delante de Haskell (38.º) y Erlang , pero por debajo de Go (14.º), Swift (15.º) y Perl (19.º).
A partir de 2022 [actualizar], los lenguajes basados en JVM como Clojure, Groovy y Scala están altamente clasificados, pero siguen siendo significativamente menos populares que el lenguaje Java original , que generalmente se ubica entre los tres primeros lugares. [127] [128]
En noviembre de 2011, Yammer se alejó de Scala por motivos que incluían la curva de aprendizaje para los nuevos miembros del equipo y la incompatibilidad de una versión del compilador de Scala a la siguiente. [158] En marzo de 2015, el ex vicepresidente del grupo de ingeniería de plataformas en Twitter , Raffi Krikorian , declaró que no habría elegido Scala en 2011 debido a su curva de aprendizaje . [159] El mismo mes, el vicepresidente senior de LinkedIn , Kevin Scott, declaró su decisión de "minimizar [su] dependencia de Scala". [160]
if
o, while
no se pueden volver a implementar. Existe un proyecto de investigación, Scala-Virtualized, que tenía como objetivo eliminar estas restricciones: Adriaan Moors, Tiark Rompf, Philipp Haller y Martin Odersky. Scala-virtualizado. Actas del taller ACM SIGPLAN 2012 sobre evaluación parcial y manipulación de programas , 117–120. Julio de 2012.También conocido como patrón proxeneta-mi-biblioteca
implicit def IntPredicate(i: Int) = new IntPredicate(i)
. La clase también se puede definir como implicit class IntPredicates(val i: Int) extends AnyVal { ... }
, lo que produce la llamada clase de valor , también introducida en Scala 2.10. Luego, el compilador eliminará las instancias reales y generará métodos estáticos en su lugar, lo que permitirá que los métodos de extensión prácticamente no tengan sobrecarga de rendimiento.Lo que habría hecho diferente hace cuatro años es usar Java y no Scala como parte de esta reescritura.
[...] un ingeniero tardaría dos meses antes de ser completamente productivo y escribir código Scala.[ enlace muerto permanente ]