¿Cuáles son las reglas precisas para cuando puede omitir paréntesis, puntos, llaves, = (funciones), etc.?

106

¿Cuáles son las reglas precisas para cuando puede omitir (omitir) paréntesis, puntos, llaves, = (funciones), etc.?

Por ejemplo,

(service.findAllPresentations.get.first.votes.size) must be equalTo(2).
  • service es mi objeto
  • def findAllPresentations: Option[List[Presentation]]
  • votes devoluciones List[Vote]
  • deben y ser son funciones de especificaciones

¿Por qué no puedo ir?

(service findAllPresentations get first votes size) must be equalTo(2)

?

El error del compilador es:

"RestServicesSpecTest.this.service.findAllPresentations de tipo Option [List [com.sharca.Presentation]] no toma parámetros"

¿Por qué cree que estoy intentando pasar un parámetro? ¿Por qué debo usar puntos para cada llamada a un método?

¿Por qué debe (service.findAllPresentations get first votes size)ser igual a (2) como resultado:

"no encontrado: valor primero"

Sin embargo, el "debe ser igual a 2" de (service.findAllPresentations.get.first.votes.size)debe ser igual a 2, es decir, ¿el encadenamiento de métodos funciona bien? - objeto cadena cadena cadena param.

He revisado el libro y el sitio web de Scala y no puedo encontrar una explicación completa.

¿Es de hecho, como explica Rob H en la pregunta de Stack Overflow, ¿Qué caracteres puedo omitir en Scala? , ese es el único caso de uso válido para omitir el '.' es para operaciones de estilo "operando operador operando", y no para el encadenamiento de métodos?

Antony Stubbs
fuente

Respuestas:

87

Parece que ha tropezado con la respuesta. De todos modos, intentaré dejarlo claro.

Puede omitir el punto cuando utilice las notaciones de prefijo, infijo y sufijo, la denominada notación de operador . Mientras usa la notación del operador, y solo entonces, puede omitir el paréntesis si hay menos de dos parámetros pasados ​​al método.

Ahora, la notación del operador es una notación para la llamada al método , lo que significa que no se puede usar en ausencia del objeto que se está llamando.

Detallaré brevemente las notaciones.

Prefijo:

Sólo ~, !, +y -puede ser utilizado en prefijo notación. Esta es la notación que está usando cuando escribe !flago val liability = -debt.

Infijo:

Esa es la notación donde aparece el método entre un objeto y sus parámetros. Todos los operadores aritméticos encajan aquí.

Postfix (también sufijo):

Esa notación se usa cuando el método sigue a un objeto y no recibe parámetros . Por ejemplo, puede escribir list tail, y eso es notación postfija.

Puede encadenar llamadas de notación infija sin problemas, siempre y cuando no se utilice ningún método. Por ejemplo, me gusta usar el siguiente estilo:

(list
 filter (...)
 map (...)
 mkString ", "
)

Eso es lo mismo que:

list filter (...) map (...) mkString ", "

Ahora, ¿por qué estoy usando paréntesis aquí, si el filtro y el mapa toman un solo parámetro? Es porque les estoy pasando funciones anónimas. No puedo mezclar definiciones de funciones anónimas con estilo infijo porque necesito un límite para el final de mi función anónima. Además, la definición de parámetro de la función anónima podría interpretarse como el último parámetro del método infijo.

Puede usar infijo con múltiples parámetros:

string substring (start, end) map (_ toInt) mkString ("<", ", ", ">")

Las funciones curry son difíciles de usar con notación infija. Las funciones de plegado son un claro ejemplo de ello:

(0 /: list) ((cnt, string) => cnt + string.size)
(list foldLeft 0) ((cnt, string) => cnt + string.size)

Necesita usar paréntesis fuera de la llamada infijo. No estoy seguro de las reglas exactas en juego aquí.

Ahora, hablemos de postfix. El sufijo puede ser difícil de usar, porque nunca se puede usar en ningún lugar excepto al final de una expresión . Por ejemplo, no puede hacer lo siguiente:

 list tail map (...)

Porque tail no aparece al final de la expresión. Tampoco puedes hacer esto:

 list tail length

Puede usar la notación infija usando paréntesis para marcar el final de las expresiones:

 (list tail) map (...)
 (list tail) length

Tenga en cuenta que se desaconseja la notación postfija porque puede ser insegura .

Espero que esto haya aclarado todas las dudas. Si no es así, deja un comentario y veré qué puedo hacer para mejorarlo.

Daniel C. Sobral
fuente
ahh, entonces estás diciendo que en mi declaración: ((((realService findAllPresentations) get) first) votes) size) debe ser igual a 2 - get, first, votes y size son todos operadores postfix, porque no toman parámetros ? así que me pregunto qué debe ser, ser y ser igual a ...
Antony Stubbs
No digo nada por el estilo, aunque estoy bastante seguro de que ese debe ser el caso. :-) "be" es probablemente un objeto auxiliar, para hacer la sintaxis más bonita. O, más específicamente, para habilitar el uso de la notación infija con "must".
Daniel C. Sobral
Bueno, get es Option.get, First es list.first, los votos es una propiedad de la clase de caso y el tamaño es list.size. ¿Que piensas ahora?
Antony Stubbs
Ah, sí, todo esto se ve reforzado por el hecho de que "(realService findPresentation 1) .get.id debe ser igual a 1" funciona, ya que Service # findPresentations (id: Int) es un operador infijo al parecer. Genial, creo que lo entiendo ahora. :)
Antony Stubbs
42

Definiciones de clase:

valo varpuede omitirse de los parámetros de la clase, lo que hará que el parámetro sea privado.

Agregar var o val hará que sea público (es decir, se generan descriptores de acceso y mutadores de método).

{} se puede omitir si la clase no tiene cuerpo, es decir,

class EmptyClass

Creación de instancias de clase:

Los parámetros genéricos se pueden omitir si el compilador puede inferirlos. Sin embargo, tenga en cuenta que si sus tipos no coinciden, el parámetro de tipo siempre se infiere para que coincida. Entonces, sin especificar el tipo, es posible que no obtenga lo que espera, es decir, dado

class D[T](val x:T, val y:T);

Esto le dará un error de tipo (Int encontrado, Cadena esperada)

var zz = new D[String]("Hi1", 1) // type error

Considerando que esto funciona bien:

var z = new D("Hi1", 1)
== D{def x: Any; def y: Any}

Porque el parámetro de tipo, T, se infiere como el supertipo menos común de los dos: Cualquiera.


Definiciones de funciones:

= se puede descartar si la función devuelve Unit (nada).

{}para el cuerpo de la función se puede descartar si la función es una sola declaración, pero solo si la declaración devuelve un valor (necesita el =signo), es decir,

def returnAString = "Hi!"

pero esto no funciona:

def returnAString "Hi!" // Compile error - '=' expected but string literal found."

El tipo de retorno de la función se puede omitir si se puede inferir (un método recursivo debe tener especificado su tipo de retorno).

() se puede descartar si la función no acepta ningún argumento, es decir,

def endOfString {
  return "myDog".substring(2,1)
}

que por convención está reservado para métodos que no tienen efectos secundarios, más sobre eso más adelante.

()en realidad no se descarta per se al definir un parámetro de paso por nombre , pero en realidad es una notación bastante diferente semánticamente, es decir,

def myOp(passByNameString: => String)

Dice que myOp toma un parámetro de paso por nombre, lo que da como resultado una cadena (es decir, puede ser un bloque de código que devuelve una cadena) en lugar de los parámetros de función,

def myOp(functionParam: () => String)

que dice myOptoma una función que tiene cero parámetros y devuelve una cadena.

(Eso sí, los parámetros de paso por nombre se compilan en funciones; solo hace que la sintaxis sea más agradable).

() se puede eliminar en la definición del parámetro de función si la función solo toma un argumento, por ejemplo:

def myOp2(passByNameString:(Int) => String) { .. } // - You can drop the ()
def myOp2(passByNameString:Int => String) { .. }

Pero si toma más de un argumento, debe incluir el ():

def myOp2(passByNameString:(Int, String) => String) { .. }

Declaraciones:

.se puede eliminar para usar la notación de operador, que solo se puede usar para operadores infijos (operadores de métodos que toman argumentos). Vea la respuesta de Daniel para obtener más información.

  • . también se puede eliminar para funciones postfijas lista cola

  • () se puede eliminar para la lista de operadores de postfix.

  • () no se puede utilizar con métodos definidos como:

    def aMethod = "hi!" // Missing () on method definition
    aMethod // Works
    aMethod() // Compile error when calling method

Porque esta notación está reservada por convención para métodos que no tienen efectos secundarios, como List # tail (es decir, la invocación de una función sin efectos secundarios significa que la función no tiene ningún efecto observable, excepto por su valor de retorno).

  • () se puede eliminar para la notación del operador al pasar un solo argumento

  • () puede ser necesario utilizar operadores de sufijo que no están al final de una declaración

  • () puede ser necesario para designar declaraciones anidadas, fines de funciones anónimas o para operadores que toman más de un parámetro

Al llamar a una función que toma una función, no puede omitir el () de la definición de función interna, por ejemplo:

def myOp3(paramFunc0:() => String) {
    println(paramFunc0)
}
myOp3(() => "myop3") // Works
myOp3(=> "myop3") // Doesn't work

Cuando llama a una función que toma un parámetro por nombre, no puede especificar el argumento como una función anónima sin parámetros. Por ejemplo, dado:

def myOp2(passByNameString:Int => String) {
  println(passByNameString)
}

Debes llamarlo como:

myOp("myop3")

o

myOp({
  val source = sourceProvider.source
  val p = myObject.findNameFromSource(source)
  p
})

pero no:

myOp(() => "myop3") // Doesn't work

En mi opinión, el uso excesivo de tipos de retorno descartables puede ser perjudicial para la reutilización del código. Solo mire las especificaciones para ver un buen ejemplo de legibilidad reducida debido a la falta de información explícita en el código. La cantidad de niveles de direccionamiento indirecto para determinar realmente cuál es el tipo de variable puede ser una locura. Con suerte, mejores herramientas pueden evitar este problema y mantener nuestro código conciso.

(De acuerdo, en la búsqueda para compilar una respuesta más completa y concisa (si me he perdido algo, o he recibido algo incorrecto / inexacto, por favor comente), agregué al principio de la respuesta. Tenga en cuenta que esto no es un idioma especificación, por lo que no estoy tratando de hacerlo exactamente académicamente correcto, sino más bien como una tarjeta de referencia).

Antony Stubbs
fuente
10
Estoy llorando. Que es esto.
Profpatsch
12

Una colección de citas que dan una idea de las diversas condiciones ...

Personalmente, pensé que habría más en la especificación. Estoy seguro de que debe haberlos, simplemente no estoy buscando las palabras adecuadas ...

Sin embargo, hay un par de fuentes, y las he recopilado juntas, pero nada realmente completo / comprensible / comprensible / que me explique los problemas anteriores ...:

"Si el cuerpo de un método tiene más de una expresión, debe rodearlo con llaves {…}. Puede omitir las llaves si el cuerpo del método tiene solo una expresión".

Del capítulo 2, "Escriba menos, haga más", de Programming Scala :

"El cuerpo del método superior viene después del signo igual '='. ¿Por qué un signo igual? ¿Por qué no solo llaves {…}, como en Java? Porque punto y coma, tipos de retorno de función, listas de argumentos de método e incluso llaves a veces se omiten, el uso de un signo igual evita varias ambigüedades posibles de análisis. El uso de un signo igual también nos recuerda que las funciones pares son valores en Scala, lo cual es consistente con el soporte de Scala para la programación funcional, que se describe con más detalle en el Capítulo 8, Programación funcional en Scala ".

Del capítulo 1, "Zero to Sixty: Introducing Scala", de Programming Scala :

"Una función sin parámetros se puede declarar sin paréntesis, en cuyo caso se debe llamar sin paréntesis. Esto proporciona soporte para el Principio de Acceso Uniforme, de modo que el llamador no sabe si el símbolo es una variable o una función sin parámetros.

El cuerpo de la función está precedido por "=" si devuelve un valor (es decir, el tipo de retorno es diferente a Unit), pero el tipo de retorno y el "=" se pueden omitir cuando el tipo es Unit (es decir, parece un procedimiento en lugar de una función).

No se requieren aparatos ortopédicos alrededor del cuerpo (si el cuerpo es una sola expresión); más precisamente, el cuerpo de una función es solo una expresión, y cualquier expresión con múltiples partes debe estar entre llaves (una expresión con una parte puede opcionalmente estar entre llaves) ".

"Las funciones con cero o un argumento se pueden llamar sin el punto y los paréntesis. Pero cualquier expresión puede tener paréntesis alrededor, por lo que puede omitir el punto y seguir usando paréntesis.

Y dado que puede usar llaves en cualquier lugar donde pueda usar paréntesis, puede omitir el punto y poner llaves, que pueden contener múltiples declaraciones.

Las funciones sin argumentos se pueden llamar sin los paréntesis. Por ejemplo, la función length () en String se puede invocar como "abc" .length en lugar de "abc" .length (). Si la función es una función de Scala definida sin paréntesis, entonces la función debe llamarse sin paréntesis.

Por convención, las funciones sin argumentos que tengan efectos secundarios, como println, se llaman entre paréntesis; los que no tienen efectos secundarios se llaman sin paréntesis ".

De la publicación del blog Scala Syntax Primer :

"Una definición de procedimiento es una definición de función en la que se omiten el tipo de resultado y el signo igual; su expresión definitoria debe ser un bloque. Por ejemplo, def f (ps) {stats} es equivalente a def f (ps): Unit = {stats }.

Ejemplo 4.6.3 Aquí hay una declaración y una definición de un procedimiento llamado escritura:

trait Writer {
    def write(str: String)
}
object Terminal extends Writer {
    def write(str: String) { System.out.println(str) }
}

El código anterior se completa implícitamente con el siguiente código:

trait Writer {
    def write(str: String): Unit
}
object Terminal extends Writer {
    def write(str: String): Unit = { System.out.println(str) }
}"

De la especificación del idioma:

"Con métodos que solo toman un único parámetro, Scala permite al desarrollador reemplazar. Con un espacio y omitir los paréntesis, habilitando la sintaxis del operador que se muestra en nuestro ejemplo de operador de inserción. Esta sintaxis se usa en otros lugares de la API de Scala, como como construir instancias de Range:

val firstTen:Range = 0 to 9

Aquí de nuevo, to (Int) es un método vanilla declarado dentro de una clase (en realidad, hay algunas conversiones de tipo más implícitas aquí, pero entiendes la deriva) ".

De Scala for Java Refugees Parte 6: Superar Java :

"Ahora, cuando prueba" m 0 ", Scala lo descarta como un operador unario, con el argumento de que no es válido (~,!, - y +). Encuentra que" m "es un objeto válido - es una función, no un método, y todas las funciones son objetos.

Como "0" no es un identificador Scala válido, no puede ser un operador infijo ni postfijo. Por tanto, Scala se queja de que esperaba ";" - lo que separaría dos (casi) expresiones válidas: "m" y "0". Si lo inserta, se quejará de que m requiere un argumento o, en su defecto, un "_" para convertirlo en una función aplicada parcialmente ".

"Creo que el estilo de sintaxis del operador sólo funciona cuando tienes un objeto explícito en el lado izquierdo. La sintaxis está pensada para permitirte expresar operaciones de estilo" operando operador operando "de forma natural".

¿Qué caracteres puedo omitir en Scala?

Pero lo que también me confunde es esta cita:

"Es necesario que haya un objeto para recibir una llamada a un método. Por ejemplo, no puede hacer" println "¡Hola mundo!" "Ya que println necesita un destinatario del objeto. Puede hacer "Console println" Hello World! "" Que satisface la necesidad ".

Porque por lo que puedo ver, no es un objeto para recibir la llamada ...

Antony Stubbs
fuente
1
Bien, intente leer la fuente de especificaciones para obtener algunas pistas y woah. Ese es un gran ejemplo de los problemas con el código mágico: demasiados mixins, inferencia de tipos y conversiones implícitas y parámetros implícitos. ¡Es tan difícil de entender desde afuera hacia adentro! Para bibliotecas grandes como esa, mejores herramientas podrían hacer algunas maravillas ... algún día ...
Antony Stubbs
3

Me resulta más fácil seguir esta regla general: en las expresiones, los espacios alternan entre métodos y parámetros. En su ejemplo, (service.findAllPresentations.get.first.votes.size) must be equalTo(2)analiza como (service.findAllPresentations.get.first.votes.size).must(be)(equalTo(2)). Tenga en cuenta que los paréntesis alrededor del 2 tienen una mayor asociatividad que los espacios. Los puntos también tienen una mayor asociatividad, por (service.findAllPresentations.get.first.votes.size) must be.equalTo(2)lo que se analizarían como (service.findAllPresentations.get.first.votes.size).must(be.equalTo(2)).

service findAllPresentations get first votes size must be equalTo 2analiza como service.findAllPresentations(get).first(votes).size(must).be(equalTo).2.

Mario Camou
fuente
2

En realidad, en una segunda lectura, tal vez esta sea la clave:

Con métodos que solo toman un único parámetro, Scala permite al desarrollador reemplazar el. con un espacio y omitir el paréntesis

Como se menciona en la publicación del blog: http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-6 .

Entonces, tal vez esto sea en realidad un "azúcar sintáctico" muy estricto que solo funciona cuando se llama efectivamente a un método, en un objeto, que toma un parámetro . p.ej

1 + 2
1.+(2)

Y nada más.

Esto explicaría mis ejemplos en la pregunta.

Pero como dije, si alguien pudiera señalar dónde se encuentra exactamente en la especificación del idioma, lo agradecería mucho.

Ok, un buen tipo (paulp_ de #scala) ha señalado en qué parte de la especificación de idioma se encuentra esta información:

6.12.3: La precedencia y la asociatividad de los operadores determinan la agrupación de partes de una expresión de la siguiente manera.

  • Si hay varias operaciones infijas en una expresión, los operadores con mayor precedencia se vinculan más estrechamente que los operadores con menor precedencia.
  • Si hay operaciones infijas consecutivas e0 op1 e1 op2. . .opn en con operadores op1,. . . , opn de la misma precedencia, entonces todos estos operadores deben tener la misma asociatividad. Si todos los operadores son asociativos por la izquierda, la secuencia se interpreta como (... (E0 op1 e1) op2...) Opn en. De lo contrario, si todos los operadores son correctamente asociativos, la secuencia se interpreta como e0 op1 (e1 op2 (.. .Opn en)...).
  • Los operadores de sufijo siempre tienen menor precedencia que los de infijo. Por ejemplo, e1 op1 e2 op2 es siempre equivalente a (e1 op1 e2) op2.

El operando de la derecha de un operador asociativo a la izquierda puede constar de varios argumentos entre paréntesis, por ejemplo, e op (e1,..., En). Esta expresión se interpreta entonces como e.op (e1,..., En).

Una operación binaria asociativa por la izquierda e1 op e2 se interpreta como e1.op (e2). Si op es asociativo correcto, la misma operación se interpreta como {val x = e1; e2.op (x)}, donde x es un nombre nuevo.

Hmm, para mí no encaja con lo que estoy viendo o simplemente no lo entiendo;)

Antony Stubbs
fuente
hmm, para aumentar aún más la confusión, esto también es válido: ((((realService findAllPresentations) get) first) votes) size) debe ser igual a 2, pero no si elimino cualquiera de esos pares de paréntesis ...
Antony Stubbs
2

No hay ninguno. Es probable que reciba consejos sobre si la función tiene o no efectos secundarios. Esto es falso. La corrección consiste en no utilizar efectos secundarios en la medida razonable permitida por Scala. En la medida en que no pueda, entonces todas las apuestas están canceladas. Todas las apuestas. El uso de paréntesis es un elemento del conjunto "todos" y es superfluo. No proporciona ningún valor una vez que se cancelan todas las apuestas.

Este consejo es esencialmente un intento de un sistema de efectos que falla (no debe confundirse con: es menos útil que otros sistemas de efectos).

Trate de no tener efectos secundarios. Después de eso, acepta que todas las apuestas están canceladas. Esconderse detrás de una notación sintáctica de facto para un sistema de efectos solo puede causar daño.

user92983
fuente
Bueno, pero ese es el problema cuando se trabaja con un lenguaje híbrido OO / funcional, ¿verdad? En cualquier ejemplo práctico, querrá tener funciones de efectos secundarios ... ¿Puede indicarnos alguna información sobre "sistemas de efectos"? Creo que la cita más precisa es la "Una función sin parámetros se puede declarar sin paréntesis, en cuyo caso se debe llamar sin paréntesis. Esto proporciona soporte para el Principio de acceso uniforme, de modo que la persona que llama no sabe si el símbolo es una variable o una función sin parámetros. ".
Antony Stubbs