Scala ( / ˈ s k ɑː l ɑː / SKAH -lah ) [7] [8] es un lenguaje de programación de propósito general de alto nivel , tipado estáticamente y fuerte 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 . [6]
El código fuente de Scala se puede compilar en bytecode de Java y ejecutarse en una máquina virtual Java (JVM). Scala también se puede transpilar a JavaScript para ejecutarse en un navegador, o compilarse directamente en un ejecutable nativo. Cuando se ejecuta en la JVM, Scala proporciona interoperabilidad de lenguaje con Java para que las bibliotecas escritas en cualquiera de los lenguajes puedan referenciarse directamente 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 de 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 funcional (como Scheme , Standard ML y Haskell ), incluyendo currying , inmutabilidad , evaluación diferida y 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 nombrados , cadenas sin formato y una versión experimental de efectos algebraicos solo para excepciones que puede verse como una versión más poderosa de las excepciones comprobadas de Java . [12]
El nombre Scala es una combinación 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 comenzó en 2001 en la Escuela Politécnica Federal de Lausana (EPFL) (en Lausana , Suiza ) por Martin Odersky . Fue la 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 de 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] [6] [14] [16] Una segunda versión (v2.0) siguió en marzo de 2006. [6]
El 17 de enero de 2011, el equipo de Scala ganó una subvención de investigación de cinco años por 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. (posteriormente rebautizada como Lightbend Inc. ), una empresa para proporcionar soporte comercial, formació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 bytecode de Java al bytecode de Dalvik (que puede traducirse aún más a código de máquina nativo durante la instalación) cuando se empaquetan, la compatibilidad de Scala con Java lo hace adecuado para el desarrollo de Android, más aún cuando se prefiere un enfoque funcional. [22]
La distribución del software de referencia Scala, incluido 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 en Scala que se pueden ejecutar en navegadores web o Node.js. [24] El compilador, en desarrollo desde 2013, se anunció que ya no era 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 entorno de ejecución administrado liviano, que utiliza el recolector de basura Boehm . El proyecto está dirigido por Denys Shabalin y tuvo su primer lanzamiento, 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 la JVM al eliminar la compilación inicial del código en tiempo de ejecución y también brindar la capacidad de llamar a rutinas nativas directamente. [26] [27]
En junio de 2004 se lanzó un compilador de referencia de Scala dirigido a .NET Framework y su Common Language Runtime , [14] pero se abandonó oficialmente en 2012. [28]
El programa Hola Mundo 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 como 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 de compilación y ejecución de 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 "Hola Mundo" es:
println ( "¡Hola, mundo!" )
Scala incluye un shell interactivo y soporte para scripts. [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 el REPL :
$ scala Bienvenido a Scala 2.12.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_131). Escriba expresiones para su evaluación. O pruebe :help.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 inmutable ) o var
(indica una variable mutable ).return
operador no es necesario en una función (aunque está permitido); el valor de la última declaración o expresión ejecutada normalmente es el valor de la función.(Type) foo
, Scala utiliza foo.asInstanceOf[Type]
, o una función especializada como toDouble
o toInt
.foo()
también se pueden llamar como just foo
; un método thread.send(signo)
también se pueden llamar como just thread send signo
; y un método foo.toString()
también se pueden llamar como just foo toString
.Estas relajaciones sintácticas están diseñadas para permitir el soporte de lenguajes específicos del dominio .
Algunas otras diferencias sintácticas básicas:
array(i)
en lugar de array[i]
. (Internamente en Scala, la primera se expande en array.apply(i) que devuelve la referencia)List[String]
en lugar de como List<String>
.void
real (ver a continuación). Unit
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 $
adjunto. Por lo tanto, para class Foo
con objeto complementario object Foo
, bajo el capó hay una clase Foo$
que contiene el código del objeto complementario, y se crea un objeto de esta clase, usando 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 esencia, el acceso externo a los campos públicos siempre pasa por métodos de acceso (getter) y mutador (setter), 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 los 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 puede llamar a las bibliotecas Java.
Las características operativas de Scala son las mismas que las de Java. El compilador de Scala genera un código de bytes que es casi idéntico al generado por el compilador de Java. [15] De hecho, el código de Scala se puede descompilar en código Java legible, con la excepción de ciertas operaciones de construcción. Para la máquina virtual Java (JVM), el código de Scala y el código de Java son indistinguibles. La única diferencia es una biblioteca de tiempo de ejecución adicional, scala-library.jar
. [30]
Scala añade 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 claro y elimina varios casos especiales en Java. Desde la perspectiva de Scala, esto es importante en la práctica porque varias de las características añadidas en Scala también están disponibles en C#.
Como se mencionó anteriormente, Scala tiene una gran flexibilidad sintáctica en comparación con Java. A continuación se muestran algunos ejemplos:
"%d apples".format(num)
, y "%d apples" format num
son equivalentes. De hecho, los operadores aritméticos como +
y <<
se tratan como cualquier otro método, ya que se permite que los nombres de funciones consten de 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 experimentan estos métodos nombrados con símbolos se refiere al manejo de la precedencia.apply
y update
tienen formas sintácticas cortas. foo()
—donde foo
es un valor (un objeto singleton o una 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 celdas STM .def foo = 42
) y sin paréntesis ( ). Al llamar a un método con paréntesis vacíos, se pueden omitir los paréntesis, lo que resulta útil al llamar a bibliotecas Java que no conocen esta distinción, por ejemplo, utilizando en lugar de . Por convención, un método debe definirse con paréntesis vacíos cuando realiza efectos secundarios .def foo() = 42
foo.toString
foo.toString()
:
) esperan el argumento en el lado izquierdo y el receptor en el lado derecho. Por ejemplo, the 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 llamada aún podrá usar un .object Foo extends FooLike { private var x = 0; def bar = x; def bar_=(value: Int) { x = value }} } }
foo.bar = 42
breakable { ... if (...) break() ... }
parece breakable
una palabra clave definida por el lenguaje, pero en realidad es solo un método que toma un argumento thunk . Los métodos que toman thunks 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 opciones pueden parecer cuestionables, pero en conjunto sirven para permitir que se definan lenguajes específicos de dominio en Scala sin necesidad de extender el compilador. Por ejemplo, la sintaxis especial de Erlangactor ! message
para enviar un mensaje a un actor, ie , puede implementarse (y se implementa) en una biblioteca de Scala sin necesidad de extensiones de lenguaje.
Java hace una distinción clara entre tipos primitivos (por ejemplo, int
and boolean
) y tipos de referencia (cualquier clase ). Solo los tipos de referencia forman parte del esquema de herencia, que derivan 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
and 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; el encajonamiento y el desencajonamiento son completamente transparentes para el usuario. Scala 2.10 permite que el usuario defina nuevos tipos de valor.
En lugar de los bucles " foreach " de Java para recorrer un iterador, Scala tiene for
expresiones for, que son similares a las comprensiones de listas en lenguajes como Haskell , o una combinación de comprensiones de listas y expresiones generadoras en Python . Las expresiones for que usan la yield
palabra clave permiten generar una nueva colección iterando sobre una existente, y devuelven una nueva colección del mismo tipo. El compilador las traduce en una serie de llamadas map
, flatMap
y filter
. Donde yield
no se usa , el código se aproxima a un bucle de estilo imperativo, traduciendo a foreach
.
Un ejemplo sencillo es:
val s = para ( x <- 1 a 25 si x * x > 50 ) rendimiento 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 es una sintaxis especial. El método to
está definido en la biblioteca Scala estándar como un método de extensión de números enteros, utilizando una técnica conocida como conversiones implícitas [32] que permite agregar nuevos métodos a 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 la cantidad de veces que cada usuario fue mencionado, busque los usuarios // en un mapa de políticos conocidos y devuelva un nuevo mapa que proporcione solo los políticos demócratas (como objetos, en lugar de cadenas). val dem_mentions = for ( mention , times ) <- mentions account <- accounts . get ( mention ) if account . party == "Democratic" yield ( account , times )
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 de clave-valor , y la coincidencia de patrones permite que las tuplas se desestructuran fácilmente en variables separadas para la clave y el valor. De manera similar, el resultado de la comprensión también devuelve tuplas de clave-valor, que se vuelven a construir automáticamente en un mapa porque el objeto de origen (de la variable mentions
) es un mapa. Tenga en cuenta que si mentions
en cambio se incluyera una lista, un conjunto, una matriz u otra colección de tuplas, exactamente el mismo código anterior generaría una nueva colección del mismo tipo.
Si bien Scala admite todas las características orientadas a objetos disponibles en Java (y, de hecho, las amplía de diversas maneras), también proporciona una gran cantidad de capacidades que normalmente solo se encuentran en lenguajes de programación funcionales . En conjunto, estas características permiten que los programas de Scala se escriban en un estilo casi completamente funcional y también permiten mezclar estilos funcionales y orientados a objetos.
Algunos 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 evalúan a algún valor. Las funciones que se declararían como de retorno void
en C o Java, y las declaraciones como while
esas lógicamente no devuelven un valor, se consideran en Scala que devuelven el tipo Unit
, que es un tipo singleton , con solo un objeto de ese tipo. Las funciones y operadores que nunca retornan en absoluto (por ejemplo, el throw
operador o una función que siempre sale de forma no local utilizando una excepción) tienen lógicamente un tipo de retorno Nothing
, un tipo especial que no contiene objetos; es decir, un tipo inferior , es decir, una subclase de cada tipo posible. (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). [33]
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 donde se desee una expresión, lo que evita la necesidad de un operador ternario en Scala:
Por razones similares, return
las declaraciones son innecesarias en Scala y, de hecho, no se recomiendan. Al igual que en Lisp , la última expresión de 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 en claro que todas las funciones son expresiones, incluso los métodos que retornan Unit
se escriben con un signo igual.
def printValue ( x : String ): Unit = println ( "Me comí un %s" .format ( x ) )
o equivalentemente (con inferencia de tipo y omitiendo la nueva línea innecesaria):
def printValue ( x : String ) = println ( "Me comí un %s" formato x )
Debido a la inferencia de tipos , el tipo de variables, valores de retorno de funciones y muchas otras expresiones normalmente se pueden omitir, ya que el compilador puede deducirlo. Algunos ejemplos son val x = "foo"
(para una constante inmutable o un objeto inmutable ) o var x = 1.5
(para una variable cuyo valor se puede cambiar más tarde). 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 ciertos tipos aún necesitan ser declarados (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 " .format ( x )
o (con un tipo de retorno declarado para una función recursiva)
def factorial ( x : Int ): Int = si x == 0 entonces 1 de lo contrario 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))
. Tenga en cuenta que ni el tipo de x
ni el tipo de retorno necesitan especificarse explícitamente, y generalmente se pueden inferir por inferencia de tipo ; pero se pueden especificar explícitamente, por ejemplo, as (x: Int) => x < 2
o even (x: Int) => (x < 2): Boolean
.
Las funciones anónimas se comportan como verdaderos cierres , ya que capturan automáticamente cualquier variable que esté disponible léxicamente en el entorno de la función que las contiene. Esas variables estarán disponibles incluso después de que la función que las contiene regrese 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 forma más concisa como
list map { sqrt(_) }
o incluso
list map sqrt
Scala impone una distinción entre variables mutables e inmutables. Las variables mutables se declaran utilizando la var
palabra clave y los valores inmutables se declaran utilizando la val
palabra clave. Una variable declarada utilizando la val
palabra clave no se puede reasignar de la misma manera que una variable declarada utilizando la final
palabra clave no se puede reasignar en Java. val
Los s son solo superficialmente inmutables, es decir, no se garantiza que un objeto al que hace referencia un val sea inmutable en sí mismo.
Sin embargo, por convención se fomenta el uso de clases inmutables, y la biblioteca estándar de Scala proporciona un amplio 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 utiliza 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 , en las que anteponer un elemento a una lista se hace devolviendo un nuevo nodo de lista que consiste en el elemento y una referencia a la cola de la lista. Anexar un elemento a una lista solo se puede hacer anteponiendo todos los elementos de la lista antigua a una nueva lista con solo el nuevo elemento. De la misma manera, insertar un elemento en el medio de una lista copiará la primera mitad de la lista, pero mantendrá una referencia a la segunda mitad de la lista. Esto se llama compartición estructural. Esto permite una concurrencia muy fácil: no se necesitan bloqueos ya que nunca se modifican los objetos compartidos. [35]
La evaluación es estricta ("eager") por defecto. En otras palabras, Scala evalúa las expresiones tan pronto como están disponibles, en lugar de cuando se necesitan. 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 puede convertirse en no estricta con el view
método . Las colecciones no estrictas proporcionan un buen ajuste semántico a cosas como los 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 ocurre cuando los elementos son realmente necesarios.
Los lenguajes de programación funcional suelen ofrecer optimización de llamadas de cola para permitir un uso extensivo de la recursión sin problemas de desbordamiento de pila . Las limitaciones en el bytecode de Java complican la optimización de llamadas de cola en la JVM. En general, una función que se llama a sí misma con una llamada de cola se puede optimizar, pero las funciones recursivas entre sí no. Se han sugerido trampolines como una solución alternativa. [36] La biblioteca Scala ofrece compatibilidad con trampolines con el objeto scala.util.control.TailCalls
desde Scala 2.8.0 (publicada el 14 de julio de 2010). Una función puede anotarse opcionalmente con @tailrec
, en cuyo caso no se compilará a menos que sea recursiva de cola. [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 de lo contrario n * factorial ( n - 1 )
Se podría optimizar la versión recursiva de cola de la siguiente 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 acum else bucle ( actual - 1 , n * accum ) loop ( n , 1 ) // Llamada al cierre usando el caso base factorial final
Esto garantiza la optimización de llamadas finales y, por lo tanto, evita un error de desbordamiento de pila.
Scala tiene soporte integrado para la comparación de patrones , que puede considerarse como una versión más sofisticada y extensible de una declaración switch , donde se pueden comparar tipos de datos arbitrarios (en lugar de solo tipos simples como números enteros, booleanos y cadenas), incluido el anidamiento arbitrario. Se proporciona un tipo especial de clase conocida como clase case , que incluye soporte automático para la comparación de patrones y se puede usar para modelar los tipos de datos algebraicos utilizados en muchos lenguajes de programación funcional. (Desde la perspectiva de Scala, una clase case 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 desestructuración de una clase case en sus parámetros de constructor durante la comparación de patrones).
Un ejemplo de una definición del algoritmo de ordenación rápida que utiliza coincidencia de patrones es el siguiente:
def qsort ( lista : Lista [ Int ]): Lista [ Int ] = lista match caso Nil => caso Nil pivot :: tail => val ( más pequeño , resto ) = tail.partition ( _ < pivot ) qsort ( más pequeño ) ::: pivot :: qsort ( resto )
La idea aquí es dividir una lista en los elementos menores que un pivote y los elementos no menores, ordenar recursivamente cada parte y pegar los resultados junto con el pivote en el medio. Esto utiliza la misma estrategia de dividir y vencer de mergesort y otros algoritmos de ordenamiento rápido.
El match
operador se utiliza para hacer una comparación 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 no vacía y, simultáneamente, desestructura la lista según el patrón dado. En este caso, el código asociado tendrá acceso a una variable local llamada pivot
que contiene el encabezado de la lista y a otra variable tail
que contiene 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 utilizando el letoperador en Lisp y Scheme.
La comparación de patrones también se produce en las 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 en este caso ambos son iguales). La comparación de patrones es la forma más sencilla de obtener las dos partes de la tupla.
El formulario _ < pivot
es una declaración de una función anónima con una variable de marcador de posición; consulte la sección anterior sobre funciones anónimas.
Aparecen los operadores de lista ::
(que añaden un elemento al principio de una lista, de forma similar a lo que ocurre cons
en Lisp y Scheme) y :::
(que unen dos listas, de forma similar a lo que ocurre append
en Lisp y Scheme). A pesar de las apariencias, no hay nada "integrado" en ninguno de estos operadores. 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 el punto ni los paréntesis. La línea anterior está escrita así:
qsort(smaller) ::: pivot :: qsort(rest)
También podría escribirse así:
qsort(rest).::(pivot).:::(qsort(smaller))
en una 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 de coincidencia de patrones anterior, el cuerpo del match
operador es una función parcial , que consta de una serie de case
expresiones, en la que prevalece la primera expresión coincidente, de forma similar al cuerpo de una sentencia switch . Las funciones parciales también se utilizan en la parte de manejo de excepciones de una try
sentencia:
Intente ... capture el caso nfe : NumberFormatException => { println ( nfe ); List ( 0 ) } caso _ => Nil
Por último, una función parcial se puede utilizar sola y el resultado de llamarla es equivalente a realizar una operación match
sobre ella. Por ejemplo, el código anterior para quicksort se puede escribir así:
val qsort : Lista [ Int ] => Lista [ Int ] = caso Nil => caso Nil pivote :: tail => val ( menor , resto ) = tail . partición ( _ < pivote ) qsort ( menor ) ::: 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 se 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 orientado a objetos puro en el sentido de que cada valor es un objeto . Los tipos de datos y los comportamientos de los objetos se describen mediante clases y rasgos . Las abstracciones de clase se extienden mediante subclases y 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 anteriores a la 8 están altamente 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 complicado (los mismos métodos deben reimplementarse en cada implementación) y extender una interfaz publicada de una manera compatible con versiones anteriores es imposible. Los rasgos son similares a las clases mixin en que tienen casi todo el poder de una clase abstracta regular, careciendo solo 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 de manera especial en los rasgos, lo que permite encadenarlos utilizando composición además de herencia. El siguiente ejemplo es un sistema de ventanas simple:
clase abstracta Ventana : // def abstracta draw () clase SimpleWindow extiende Window : def draw () println ( "in SimpleWindow" ) // dibuja una ventana básica El rasgo WindowDecoration extiende Window rasgo HorizontalScrollbarDecoration extiende WindowDecoration : // "abstract override" es necesario aquí para que "super()" funcione porque la función padre es abstracta. Si fuera concreta, bastaría con "override" normal. abstract override def draw () println ( "in HorizontalScrollbarDecoration" ) super . draw () // ahora dibuja una barra de desplazamiento horizontal rasgo VerticalScrollbarDecoration extiende WindowDecoration : abstract override def draw () println ( "en VerticalScrollbarDecoration" ) super . draw () // ahora dibuja una barra de desplazamiento vertical rasgo TitleDecoration extiende WindowDecoration : abstract override def draw () println ( "in TitleDecoration" ) super . draw () // ahora dibuja la barra de título
Una variable puede declararse así:
val mywin = new SimpleWindow con VerticalScrollbarDecoration con HorizontalScrollbarDecoration con TitleDecoration
El resultado de la llamada 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) se enhebró de nuevo a través de los otros rasgos mezclados y finalmente al código en Window
, aunque ninguno de los rasgos heredó uno del otro . 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 cambia o confiar en la inicialización en tiempo de ejecución de las relaciones de entidad. 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 los mix-ins.
Scala está equipado con un sistema de tipos estáticos expresivo que, en su mayor parte, refuerza el uso seguro y coherente de las abstracciones. Sin embargo, el sistema de tipos no es sólido . [38] En particular, el sistema de tipos admite:
Scala puede 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 " enriquecer mi biblioteca " por Martin Odersky en 2006; [32] se plantearon inquietudes sobre esta frase debido a sus connotaciones negativas [40] e inmadurez [41] ), permite que se utilicen nuevos métodos como si se agregaran a tipos existentes. Esto es similar al concepto de C# de métodos de extensión 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 (típicamente, una clase) que envuelve el tipo original y proporciona el método adicional. Si no se puede encontrar un método para un tipo dado, 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 usando una biblioteca complementaria, de modo que solo el código que importa la biblioteca complementaria obtiene la nueva funcionalidad y el resto del código no se ve afectado.
El siguiente ejemplo muestra el enriquecimiento del tipo Int
con los métodos isEven
y isOdd
:
objeto MisExtensiones : extensión ( i : Int ) def esPar = i % 2 == 0 def esImpar = ! i . es par import MyExtensions . * // trae enriquecimiento implícito al alcance 4 . isEven // -> true
La importación de los miembros de MyExtensions
lleva la conversión implícita a la clase de extensión IntPredicates
dentro del alcance. [42]
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 código fuente independiente Akka [43] con licencia de Lightbend Inc. Los actores de Akka pueden distribuirse o combinarse con memoria transaccional de software ( transactores ). Las implementaciones alternativas de procesos secuenciales de comunicación (CSP) para el paso de mensajes basado en canales son Communicating Scala Objects, [44] o simplemente a través de JCSP .
Un actor es como una instancia de hilo con un buzón de correo. Se puede crear 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 ( new Act : become : case msg => println ( "echo " + msg ) ) echoServer ! "hola"
Scala también viene con soporte integrado para programación paralela de datos 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 urls = Lista ( "https://scala-lang.org" , "https://github.com/scala/scala" ) def fromURL ( url : String ) = scala.io.Source.fromURL ( url ) .getLines ( ) . mkString ( " \ n " ) val t = System . currentTimeMillis () urls . 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 con Spark y otras tecnologías de procesamiento de flujo, está escrita en Scala.
Existen 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 orden superior y currificadas. [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 la JVM. Existen diferencias sustanciales entre estos lenguajes en el sistema de tipos, en el grado en que cada lenguaje admite la programación funcional y orientada a objetos, y en la similitud de su sintaxis con la de Java.
Scala es de tipado estático , mientras que tanto Groovy como Clojure son de tipado dinámico . Esto hace que el sistema de tipos sea más complejo y difícil de entender, pero permite que casi todos los [38] errores de tipo se detecten en tiempo de compilación y puede dar como resultado una ejecución significativamente más rápida. Por el contrario, el tipado dinámico requiere más pruebas para garantizar la corrección del programa y, por lo tanto, generalmente es más lento, para permitir una mayor flexibilidad y simplicidad de programación. Con respecto 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 del tipado dinámico en casos en los que los tipos son prácticamente estáticos. Esta sobrecarga se reduce aún más cuando se utilizan versiones recientes de la JVM, que se ha mejorado con una instrucción de invocación dinámica para métodos que se definen con argumentos de tipado dinámico. Estos avances reducen la brecha de velocidad entre el tipado estático y dinámico, aunque un lenguaje de tipado estático, como Scala, sigue siendo la opción preferida cuando la eficiencia de 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 varias maneras. Groovy, aunque también está fuertemente orientado a objetos, se centra más en reducir la verbosidad. En Clojure, se le resta importancia a la programación orientada a objetos y la programación funcional es 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, e intenta ser agnóstico entre los dos paradigmas, lo que permite al desarrollador elegir entre los dos paradigmas o, más frecuentemente, alguna combinación de ellos.
En cuanto a la similitud sintáctica con Java, Scala hereda gran parte de la sintaxis de Java, al igual que Groovy. Clojure, por su parte, sigue la sintaxis de Lisp , que es diferente tanto en apariencia como en filosofía. [ cita requerida ]
En 2013, cuando Scala estaba en la versión 2.10, el ThoughtWorks Technology Radar, que es un informe bianual basado en opiniones de un grupo de tecnólogos de alto nivel, [139] recomendó la adopción de Scala en su categoría de lenguajes y marcos de trabajo. [140]
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, debe investigar el lenguaje y tener una opinión muy sólida sobre qué partes son adecuadas para usted, creando su propia definición de Scala, las partes buenas”. [141]
En la edición de 2018 de la encuesta State of Java , [142] que recopiló datos de 5160 desarrolladores sobre varios 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 del año anterior de la encuesta, el uso de Scala entre los lenguajes alternativos de JVM 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 de lenguajes de programación, [143] que rastrea las búsquedas de tutoriales de lenguajes, 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 , que ocupa el puesto 12.
En enero de 2021, el ranking de lenguajes de programación RedMonk, que establece clasificaciones basadas en la cantidad de proyectos de GitHub y preguntas realizadas en Stack Overflow , clasificó a Scala en el puesto 14. [144] 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 [145] de popularidad de lenguajes de programación emplea clasificaciones de motores de búsqueda de Internet y recuentos de publicaciones similares para determinar la popularidad de los lenguajes. En septiembre de 2021, mostró a Scala en el puesto 31. En esta clasificación, 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 tienen una clasificación alta, pero siguen siendo significativamente menos populares que el lenguaje Java original , que generalmente se ubica en los tres primeros lugares. [144] [145]
En noviembre de 2011, Yammer se alejó de Scala por razones que incluían la curva de aprendizaje para los nuevos miembros del equipo y la incompatibilidad de una versión del compilador de Scala con la siguiente. [175] 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 . [176] El mismo mes, el vicepresidente sénior de LinkedIn, Kevin Scott, declaró su decisión de "minimizar [su] dependencia de Scala". [177]
{{cite web}}
: Falta o está vacío |title=
( ayuda )Los creadores de Scala lo pronuncian scah-lah , como la palabra italiana para "escaleras". Las dos "a" se pronuncian igual.
if
o while
no se pueden volver a implementar. Existe un proyecto de investigación, Scala-Virtualized, cuyo objetivo era eliminar estas restricciones: Adriaan Moors, Tiark Rompf, Philipp Haller y Martin Odersky. Scala-Virtualized. 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 pimp-my-library
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 una denominada clase de valor , también introducida en Scala 2.10. El compilador eliminará entonces 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 hubiera hecho de manera diferente hace cuatro años es usar Java y no usar Scala como parte de esta reescritura. [...] un ingeniero necesitaría dos meses antes de ser completamente productivo y escribir código Scala.[ enlace muerto permanente ]