¿Qué significan todos los operadores simbólicos de Scala?

La syntax de Scala tiene muchos símbolos. Dado que este tipo de nombres son difíciles de encontrar usando los motores de búsqueda, una lista completa de ellos sería útil.

¿Cuáles son todos los símbolos en Scala, y qué hace cada uno de ellos?

En particular, me gustaría saber acerca de -> , ||= , ++= , <= , _._ , :: y :+= .

Dividí los operadores, con el propósito de enseñar, en cuatro categorías :

  • Palabras clave / símbolos reservados
  • Métodos importados automáticamente
  • Métodos comunes
  • Azúcares sintácticos / composición

Es una suerte, entonces, que la mayoría de las categorías estén representadas en la pregunta:

 -> // Automatically imported method ||= // Syntactic sugar ++= // Syntactic sugar/composition or common method < = // Common method _._ // Typo, though it's probably based on Keyword/composition :: // Common method :+= // Common method 

El significado exacto de la mayoría de estos métodos depende de la clase que los define. Por ejemplo, < = en Int significa "menor que o igual a" . El primero, -> , voy a dar como ejemplo a continuación. :: es probablemente el método definido en List (aunque podría ser el objeto del mismo nombre), y :+= es probablemente el método definido en varias clases de Buffer .

Entonces, veámoslos.

Palabras clave / símbolos reservados

Hay algunos símbolos en Scala que son especiales. Dos de ellos se consideran palabras clave adecuadas, mientras que otros son simplemente "reservados". Son:

 // Keywords < - // Used on for-comprehensions, to separate pattern from generator => // Used for function types, function literals and import renaming // Reserved ( ) // Delimit expressions and parameters [ ] // Delimit type parameters { } // Delimit blocks . // Method call and path separator // /* */ // Comments # // Used in type notations : // Type ascription or context bounds <:>: < % // Upper, lower and view bounds  

Todos son parte del lenguaje y, como tales, se pueden encontrar en cualquier texto que describa correctamente el idioma, como la Especificación de Scala (PDF).

El último, el subrayado, merece una descripción especial, porque es muy utilizado y tiene muchos significados diferentes. Aquí hay una muestra:

 import scala._ // Wild card -- all of Scala is imported import scala.{ Predef => _, _ } // Exception, everything except Predef def f[M[_]] // Higher kinded type parameter def f(m: M[_]) // Existential type _ + _ // Anonymous function placeholder parameter m _ // Eta expansion of method into method value m(_) // Partial function application _ => 5 // Discarded parameter case _ => // Wild card pattern -- matches anything f(xs: _*) // Sequence xs is passed as multiple parameters to f(ys: T*) case Seq(xs @ _*) // Identifier xs is bound to the whole matched sequence 

Sin embargo, probablemente haya olvidado algún otro significado.

Métodos importados automáticamente

Entonces, si no encontró el símbolo que está buscando en la lista anterior, entonces debe ser un método o parte de uno. Pero, a menudo, verá algún símbolo y la documentación de la clase no tendrá ese método. Cuando esto sucede, o bien está buscando una composición de uno o más métodos con otra cosa, o el método ha sido importado en el scope, o está disponible a través de una conversión implícita importada.

Todavía se pueden encontrar en ScalaDoc : solo tienes que saber dónde buscarlos. O, en su defecto, mire el índice (actualmente roto en 2.9.1, pero disponible todas las noches).

Cada código de Scala tiene tres importaciones automáticas:

 // Not necessarily in this order import _root_.java.lang._ // _root_ denotes an absolute path import _root_.scala._ import _root_.scala.Predef._ 

Los primeros dos solo hacen que las clases y los objetos únicos estén disponibles. El tercero contiene todas las conversiones implícitas y los métodos importados, ya que Predef es un objeto en sí mismo.

Mirando dentro de Predef rápidamente muestra algunos símbolos:

 class <: < class =:= object <%< object =:= 

Cualquier otro símbolo estará disponible a través de una conversión implícita . Basta con mirar los métodos etiquetados con implicit que reciben, como parámetro, un objeto de tipo que está recibiendo el método. Por ejemplo:

 "a" -> 1 // Look for an implicit from String, AnyRef, Any or type parameter 

En el caso anterior, -> se define en la clase ArrowAssoc través del método any2ArrowAssoc que toma un objeto de tipo A , donde A es un parámetro de tipo ilimitado para el mismo método.

Métodos comunes

Entonces, muchos símbolos son simplemente métodos en una clase. Por ejemplo, si lo haces

 List(1, 2) ++ List(3, 4) 

Encontrará el método ++ en el ScalaDoc for List . Sin embargo, hay una convención que debe conocer al buscar métodos. Los métodos que terminan en colon (:) se unen a la derecha en lugar de a la izquierda. En otras palabras, mientras que la llamada al método anterior es equivalente a:

 List(1, 2).++(List(3, 4)) 

Si tuviera, en cambio 1 :: List(2, 3) , eso sería equivalente a:

 List(2, 3).::(1) 

Por lo tanto, debe buscar el tipo que se encuentra a la derecha al buscar los métodos que terminan en dos puntos. Considera, por ejemplo:

 1 +: List(2, 3) :+ 4 

El primer método ( +: 🙂 se une a la derecha y se encuentra en la List . El segundo método ( :+ ) es solo un método normal, y se une a la izquierda - nuevamente, en la List .

Azúcares sintácticos / composición

Entonces, aquí hay algunos azúcares sintácticos que pueden esconder un método:

 class Example(arr: Array[Int] = Array.fill(5)(0)) { def apply(n: Int) = arr(n) def update(n: Int, v: Int) = arr(n) = v def a = arr(0); def a_=(v: Int) = arr(0) = v def b = arr(1); def b_=(v: Int) = arr(1) = v def c = arr(2); def c_=(v: Int) = arr(2) = v def d = arr(3); def d_=(v: Int) = arr(3) = v def e = arr(4); def e_=(v: Int) = arr(4) = v def +(v: Int) = new Example(arr map (_ + v)) def unapply(n: Int) = if (arr.indices contains n) Some(arr(n)) else None } val Ex = new Example // or var for the last example println(Ex(0)) // calls apply(0) Ex(0) = 2 // calls update(0, 2) Ex.b = 3 // calls b_=(3) // This requires Ex to be a "val" val Ex(c) = 2 // calls unapply(2) and assigns result to c // This requires Ex to be a "var" Ex += 1 // substituted for Ex = Ex + 1 

El último es interesante, porque cualquier método simbólico puede combinarse para formar un método de asignación de esa manera.

Y, por supuesto, hay varias combinaciones que pueden aparecer en el código:

 (_+_) // An expression, or parameter, that is an anonymous function with // two parameters, used exactly where the underscores appear, and // which calls the "+" method on the first parameter passing the // second parameter as argument. 

Una (buena, IMO) diferencia entre Scala y otros idiomas es que le permite nombrar sus métodos con casi cualquier carácter.

Lo que usted enumera no es “puntuación” sino métodos simples y simples, y como tal su comportamiento varía de un objeto a otro (aunque hay algunas convenciones).

Por ejemplo, consulte la documentación de Scaladoc para List , y verá algunos de los métodos que mencionó aquí.

Algunas cosas para tener en mente:

  • La mayoría de las veces, la combinación A operator+equal B traduce a A = A operator B , como en los ejemplos ||= o ++= .

  • Los métodos que terminan en : son correctos asociativos, esto significa que A :: B es en realidad B.::(A) .

Encontrará la mayoría de las respuestas en la documentación de Scala. Mantener una referencia aquí duplicaría los esfuerzos y se retrasaría rápidamente 🙂

Puede agrupar a los primeros según algunos criterios. En este post, solo explicaré el carácter de guión bajo y la flecha derecha.

_._ contiene un punto. Un período en Scala siempre indica una llamada a un método . Así que, a la izquierda del período que tiene el receptor, y a la derecha el mensaje (nombre del método). Ahora _ es un símbolo especial en Scala. Hay varias publicaciones al respecto, por ejemplo, esta entrada de blog, todos los casos de uso. Aquí se trata de un atajo de función anónima , es decir, un atajo para una función que toma un argumento e invoca el método _ en él. Ahora _ no es un método válido, por lo que seguramente estaba viendo _._1 o algo similar, es decir, invocando el método _._1 en el argumento de la función. _1 a _22 son los métodos de tuplas que extraen un elemento particular de una tupla. Ejemplo:

 val tup = ("Hallo", 33) tup._1 // extracts "Hallo" tup._2 // extracts 33 

Ahora supongamos un caso de uso para el acceso directo de la aplicación de función. Dado un mapa que mapea enteros a cadenas:

 val coll = Map(1 -> "Eins", 2 -> "Zwei", 3 -> "Drei") 

Wooop, ya hay otra ocurrencia de una puntuación extraña. Los caracteres de guión y mayor que, que se asemejan a una flecha de la derecha , es un operador que produce un Tuple2 . Entonces, no hay diferencia en el resultado de escribir (1, "Eins") o 1 -> "Eins" , solo que este último es más fácil de leer, especialmente en una lista de tuplas como el ejemplo del mapa. El -> no es mágico, es, como algunos otros operadores, disponible porque tienes todas las conversiones implícitas en el objeto scala.Predef en el scope. La conversión que tiene lugar aquí es

 implicit def any2ArrowAssoc [A] (x: A): ArrowAssoc[A] 

Donde ArrowAssoc tiene el método -> que crea el Tuple2 . Por lo tanto 1 -> "Eins" es real la llamada Predef.any2ArrowAssoc(1).->("Eins") . De acuerdo. Ahora volvamos a la pregunta original con el carácter de guión bajo:

 // lets create a sequence from the map by returning the // values in reverse. coll.map(_._2.reverse) // yields List(sniE, iewZ, ierD) 

El guión bajo aquí acorta el siguiente código equivalente:

 coll.map(tup => tup._2.reverse) 

Tenga en cuenta que el método de map de un Mapa pasa en la tupla de la clave y el valor al argumento de la función. Como solo estamos interesados ​​en los valores (las cadenas), los extraemos con el método _2 en la tupla.

Como una adición a las respuestas shinys de Daniel y 0__, tengo que decir que Scala entiende los análogos de Unicode para algunos de los símbolos, así que en vez de

 for (n < - 1 to 10) n % 2 match { case 0 => println("even") case 1 => println("odd") } 

uno puede escribir

 for (n ← 1 to 10) n % 2 match { case 0 ⇒ println("even") case 1 ⇒ println("odd") } 

< = es como si lo "leyeras": "menos que o igual". Entonces, ¿es un operador matemático, en la lista de < (es menor que?), > (Es mayor que?), == (¿es igual?) != (¿No es igual?), < = (Es menor que o igual ?), y >= (¿es mayor que o igual?).

Esto no debe confundirse con => que es una especie de doble flecha hacia la derecha , utilizada para separar la lista de argumentos del cuerpo de una función y para separar la condición de prueba en la coincidencia de patrones (un bloque de case ) del cuerpo ejecutado cuando una coincidencia ocurre. Puedes ver un ejemplo de esto en mis dos respuestas anteriores. Primero, el uso de la función:

 coll.map(tup => tup._2.reverse) 

que ya se abrevia cuando se omiten los tipos. La siguiente función sería

 // function arguments function body (tup: Tuple2[Int, String]) => tup._2.reverse 

y el uso de coincidencia de patrones:

 def extract2(l: List[Int]) = l match { // if l matches Nil return "empty" case Nil => "empty" // etc. case ::(head, Nil) => "exactly one element (" + head + ")" // etc. case ::(head, tail) => "more than one element" } 

En cuanto a :: hay otra entrada de Stackoverflow que cubre el :: caso. En resumen, se usa para construir Lists al’considerar ‘un elemento principal y una lista final . Es una clase que representa una lista detallada y que puede usarse como un extractor, pero lo más común es que sea un método en una lista. Como señala Pablo Fernández, dado que termina en dos puntos, es correcto asociativo , lo que significa que el receptor de la llamada al método está a la derecha, y el argumento a la izquierda del operador. De esta forma, puede express elegantemente el consumo como anteponiendo un nuevo elemento principal a una lista existente:

 val x = 2 :: 3 :: Nil // same result as List(2, 3) val y = 1 :: x // yields List(1, 2, 3) 

Esto es equivalente a

 val x = Nil.::(3).::(2) // successively prepend 3 and 2 to an empty list val y = x.::(1) // then prepend 1 

El uso como objeto extractor es el siguiente:

 def extract(l: List[Int]) = l match { case Nil => "empty" case head :: Nil => "exactly one element (" + head + ")" case head :: tail => "more than one element" } extract(Nil) // yields "empty" extract(List(1)) // yields "exactly one element (33)" extract(List(2, 3)) // yields "more than one element" 

Esto parece un operador aquí, pero en realidad es solo otra forma (más legible) de escribir

 def extract2(l: List[Int]) = l match { case Nil => "empty" case ::(head, Nil) => "exactly one element (" + head + ")" case ::(head, tail) => "more than one element" } 

Puede leer más sobre los extractores en esta publicación .

Considero que un IDE moderno es fundamental para comprender proyectos scala grandes. Dado que estos operadores también son métodos, en la idea inteligente solo control-clic o control-b en las definiciones.

Puede hacer clic con el botón derecho en un operador contra (:) y terminar en el scala javadoc diciendo “Agrega un elemento al principio de esta lista”. En los operadores definidos por el usuario, esto se vuelve aún más crítico, ya que podrían definirse en implícitas difíciles de encontrar … su IDE sabe dónde se definió lo implícito.

Scala hereda la mayoría de los operadores aritméticos de Java . Esto incluye bitwise o | (carácter de tubería única), bit a bit y & , bit a bit exclusivo ^ o ^ , así como lógico (booleano) o || (dos caracteres de tubería) y lógico-y && . Curiosamente, puede usar los operadores de un solo carácter en boolean , por lo que los operadores lógicos de java son totalmente redundantes:

 true && true // valid true & true // valid as well 3 & 4 // bitwise-and (011 & 100 yields 000) 3 && 4 // not valid 

Como se señaló en otra publicación, las llamadas que terminan en un signo igual = , se resuelven (si un método con ese nombre no existe) mediante una reasignación:

 var x = 3 x += 1 // `+=` is not a method in `int`, Scala makes it `x = x + 1` 

Esta ‘doble verificación’ hace posible intercambiar fácilmente un mutable por una colección inmutable:

 val m = collection.mutable.Set("Hallo") // `m` a val, but holds mutable coll var i = collection.immutable.Set("Hallo") // `i` is a var, but holds immutable coll m += "Welt" // destructive call m.+=("Welt") i += "Welt" // re-assignment i = i + "Welt" (creates a new immutable Set) 

Solo agregando a las otras excelentes respuestas. Scala ofrece dos operadores simbólicos a menudo criticados, /: ( foldLeft ) y :\ ( foldRight ) operadores, el primero es asociativo de la derecha. Entonces las siguientes tres declaraciones son el equivalente:

 ( 1 to 100 ).foldLeft( 0, _+_ ) ( 1 to 100 )./:( 0 )( _+_ ) ( 0 /: ( 1 to 100 ) )( _+_ ) 

Como son estos tres:

 ( 1 to 100 ).foldRight( 0, _+_ ) ( 1 to 100 ).:\( 0 )( _+_ ) ( ( 1 to 100 ) :\ 0 )( _+_ )