¿Cuál es la diferencia formal en Scala entre llaves y paréntesis, y cuándo deben usarse?

329

¿Cuál es la diferencia formal entre pasar argumentos a funciones entre paréntesis ()y llaves {}?

La sensación que obtuve del libro Programación en Scala es que Scala es bastante flexible y debería usar el que más me gusta, pero encuentro que algunos casos se compilan mientras que otros no.

Por ejemplo (solo como un ejemplo; agradecería cualquier respuesta que discuta el caso general, no solo este ejemplo en particular):

val tupleList = List[(String, String)]()
val filtered = tupleList.takeWhile( case (s1, s2) => s1 == s2 )

=> error: inicio ilegal de expresión simple

val filtered = tupleList.takeWhile{ case (s1, s2) => s1 == s2 }

=> bien.

Jean-Philippe Pellet
fuente

Respuestas:

365

Una vez intenté escribir sobre esto, pero al final me di por vencido, ya que las reglas son algo difusas. Básicamente, tendrás que acostumbrarte.

Quizás sea mejor concentrarse en dónde las llaves y los paréntesis se pueden usar indistintamente: al pasar parámetros a llamadas a métodos. Usted puede sustituir el paréntesis con llaves si, y sólo si, el método espera un solo parámetro. Por ejemplo:

List(1, 2, 3).reduceLeft{_ + _} // valid, single Function2[Int,Int] parameter

List{1, 2, 3}.reduceLeft(_ + _) // invalid, A* vararg parameter

Sin embargo, hay más que necesita saber para comprender mejor estas reglas.

Mayor verificación de compilación con parens

Los autores de Spray recomiendan parens redondos porque brindan una mayor verificación de compilación. Esto es especialmente importante para DSL como Spray. Al usar parens le está diciendo al compilador que solo se le debe dar una sola línea; por lo tanto, si accidentalmente le da dos o más, se quejará. Ahora, este no es el caso con las llaves: si, por ejemplo, olvida a un operador en algún lugar, su código se compilará y obtendrá resultados inesperados y, potencialmente, un error muy difícil de encontrar. A continuación se inventa (ya que las expresiones son puras y al menos darán una advertencia), pero hace el punto:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

El primero compila, el segundo da error: ')' expected but integer literal found. El autor quería escribir 1 + 2 + 3.

Se podría argumentar que es similar para los métodos de parámetros múltiples con argumentos predeterminados; Es imposible olvidar accidentalmente una coma para separar los parámetros cuando se usan parens.

Verbosidad

Una nota importante a menudo pasada por alto sobre la verbosidad. El uso de llaves se lleva inevitablemente a un código detallado, ya que la guía de estilo Scala establece claramente que el cierre de llaves debe estar en su propia línea:

... la llave de cierre está en su propia línea inmediatamente después de la última línea de la función.

Muchos reformateadores automáticos, como en IntelliJ, realizarán automáticamente este reformateo por usted. Por lo tanto, trate de usar parens redondos cuando pueda.

Notación de infijo

Al usar la notación infija, como List(1,2,3) indexOf (2)puede omitir paréntesis si solo hay un parámetro y escribirlo como List(1, 2, 3) indexOf 2. Este no es el caso de la notación de puntos.

Tenga en cuenta también que cuando tiene un único parámetro que es una expresión de múltiples tokens, como x + 2o a => a % 2 == 0, debe usar paréntesis para indicar los límites de la expresión.

Tuplas

Debido a que a veces puede omitir paréntesis, a veces una tupla necesita paréntesis adicionales como en ((1, 2)), y a veces el paréntesis externo puede omitirse, como en (1, 2). Esto puede causar confusión.

Literales de función / función parcial con case

Scala tiene una sintaxis para funciones y literales de funciones parciales. Se parece a esto:

{
    case pattern if guard => statements
    case pattern => statements
}

Los únicos otros lugares donde puede usar casedeclaraciones son con las palabras clave matchy catch:

object match {
    case pattern if guard => statements
    case pattern => statements
}
try {
    block
} catch {
    case pattern if guard => statements
    case pattern => statements
} finally {
    block
}

No puede usar casedeclaraciones en ningún otro contexto . Entonces, si quieres usar case, necesitas llaves. En caso de que se pregunte qué hace que la distinción entre una función y una función parcial sea literal, la respuesta es: contexto. Si Scala espera una función, una función que obtienes. Si espera una función parcial, obtienes una función parcial. Si se esperan ambos, da un error sobre la ambigüedad.

Expresiones y Bloques

El paréntesis se puede usar para hacer subexpresiones. Las llaves se pueden usar para hacer bloques de código (esta no es una función literal, así que tenga cuidado de intentar usarla como tal). Un bloque de código consta de varias declaraciones, cada una de las cuales puede ser una declaración de importación, una declaración o una expresión. Dice así:

{
    import stuff._
    statement ; // ; optional at the end of the line
    statement ; statement // not optional here
    var x = 0 // declaration
    while (x < 10) { x += 1 } // stuff
    (x % 5) + 1 // expression
}

( expression )

Entonces, si necesita declaraciones, declaraciones múltiples, importo algo así, necesita llaves. Y debido a que una expresión es una declaración, los paréntesis pueden aparecer dentro de llaves. Pero lo interesante es que los bloques de código también son expresiones, por lo que puede usarlos en cualquier lugar dentro de una expresión:

( { var x = 0; while (x < 10) { x += 1}; x } % 5) + 1

Entonces, dado que las expresiones son declaraciones y los bloques de códigos son expresiones, todo lo siguiente es válido:

1       // literal
(1)     // expression
{1}     // block of code
({1})   // expression with a block of code
{(1)}   // block of code with an expression
({(1)}) // you get the drift...

Donde no son intercambiables

Básicamente, no se puede reemplazar {}con ()o viceversa en cualquier otro lugar. Por ejemplo:

while (x < 10) { x += 1 }

Esta no es una llamada al método, por lo que no puede escribirla de ninguna otra manera. Bueno, puedes poner llaves dentro del paréntesis para el condition, así como usar paréntesis dentro de las llaves para el bloque de código:

while ({x < 10}) { (x += 1) }

Entonces, espero que esto ayude.

Daniel C. Sobral
fuente
53
Por eso la gente argumenta que Scala es complejo. Y me llamaría entusiasta de Scala.
andyczerwonka
¡No tener que introducir un alcance para cada método, creo que simplifica el código Scala! Idealmente, ningún método debería usar {}, todo debería ser una sola expresión pura
samthebest
1
@andyczerwonka Estoy totalmente de acuerdo, pero es el precio natural e inevitable (?) que paga por la flexibilidad y el poder expresivo => Scala no es demasiado caro. Si esta es la elección correcta para cualquier situación particular, por supuesto, es otra cuestión.
Ashkan Kh. Nazary
Hola, cuando dices List{1, 2, 3}.reduceLeft(_ + _)que no es válido, ¿quieres decir que tiene una sintaxis err? Pero encuentro que el código puede compilarse. Puse mi código aquí
calvin
Usaste List(1, 2, 3)en todos los ejemplos, en lugar de List{1, 2, 3}. Por desgracia, en la versión actual de Scala (2.13), esto falla con un mensaje de error diferente (coma inesperado). Probablemente tendría que volver a 2.7 o 2.8 para obtener el error original.
Daniel C. Sobral
56

Aquí hay un par de reglas e inferencias diferentes: en primer lugar, Scala infiere las llaves cuando un parámetro es una función, por ejemplo, en list.map(_ * 2)las llaves se infiere, es solo una forma más corta de list.map({_ * 2}). En segundo lugar, Scala le permite omitir los paréntesis en la última lista de parámetros, si esa lista de parámetros tiene un parámetro y es una función, list.foldLeft(0)(_ + _)puede escribirse como list.foldLeft(0) { _ + _ }(o list.foldLeft(0)({_ + _})si desea ser más explícito).

Sin embargo, si se agrega caseque se obtiene, como otros han mencionado, una función parcial en lugar de una función, y Scala no inferir los apoyos para las funciones parciales, por lo que list.map(case x => x * 2)no va a funcionar, pero a la vez list.map({case x => 2 * 2})y list.map { case x => x * 2 }lo hará.

Theo
fuente
44
No solo de la última lista de parámetros. Por ejemplo, list.foldLeft{0}{_+_}funciona.
Daniel C. Sobral
1
Ah, estaba seguro de haber leído que era solo la última lista de parámetros, ¡pero claramente estaba equivocado! Bueno saber.
Theo
23

Hay un esfuerzo de la comunidad para estandarizar el uso de llaves y paréntesis, consulte la Guía de estilo de Scala (página 21): http://www.codecommit.com/scala-style-guide.pdf

La sintaxis recomendada para las llamadas a métodos de orden superior es usar siempre llaves y omitir el punto:

val filtered = tupleList takeWhile { case (s1, s2) => s1 == s2 }

Para las llamadas metodológicas "normales" debe usar el punto y los paréntesis.

val result = myInstance.foo(5, "Hello")
Olle Kullberg
fuente
18
En realidad, la convención es usar llaves redondas, ese enlace no es oficial. Esto se debe a que en la programación funcional todas las funciones SON solo ciudadanos de primer orden y, por lo tanto, NO deben ser tratados de manera diferente. En segundo lugar, Martin Odersky dice que debe intentar usar solo infijo para métodos similares al operador (por ejemplo +, --), NO métodos regulares como takeWhile. El punto completo de la notación infija es permitir DSL y operadores personalizados, por lo tanto, uno debe usarlo en este contexto no todo el tiempo.
samthebest
17

No creo que haya nada particular o complejo sobre las llaves en Scala. Para dominar el uso aparentemente complejo de ellos en Scala, solo tenga en cuenta un par de cosas simples:

  1. las llaves forman un bloque de código, que se evalúa como la última línea de código (casi todos los idiomas hacen esto)
  2. Se puede generar una función si se desea con el bloque de código (sigue la regla 1)
  3. Las llaves se pueden omitir para el código de una línea, excepto para una cláusula mayúscula (opción Scala)
  4. los paréntesis se pueden omitir en la llamada de función con el bloque de código como parámetro (opción Scala)

Expliquemos un par de ejemplos según las tres reglas anteriores:

val tupleList = List[(String, String)]()
// doesn't compile, violates case clause requirement
val filtered = tupleList.takeWhile( case (s1, s2) => s1 == s2 ) 
// block of code as a partial function and parentheses omission,
// i.e. tupleList.takeWhile({ case (s1, s2) => s1 == s2 })
val filtered = tupleList.takeWhile{ case (s1, s2) => s1 == s2 }

// curly braces omission, i.e. List(1, 2, 3).reduceLeft({_+_})
List(1, 2, 3).reduceLeft(_+_)
// parentheses omission, i.e. List(1, 2, 3).reduceLeft({_+_})
List(1, 2, 3).reduceLeft{_+_}
// not both though it compiles, because meaning totally changes due to precedence
List(1, 2, 3).reduceLeft _+_ // res1: String => String = <function1>

// curly braces omission, i.e. List(1, 2, 3).foldLeft(0)({_ + _})
List(1, 2, 3).foldLeft(0)(_ + _)
// parentheses omission, i.e. List(1, 2, 3).foldLeft(0)({_ + _})
List(1, 2, 3).foldLeft(0){_ + _}
// block of code and parentheses omission
List(1, 2, 3).foldLeft {0} {_ + _}
// not both though it compiles, because meaning totally changes due to precedence
List(1, 2, 3).foldLeft(0) _ + _
// error: ';' expected but integer literal found.
List(1, 2, 3).foldLeft 0 (_ + _)

def foo(f: Int => Unit) = { println("Entering foo"); f(4) }
// block of code that just evaluates to a value of a function, and parentheses omission
// i.e. foo({ println("Hey"); x => println(x) })
foo { println("Hey"); x => println(x) }

// parentheses omission, i.e. f({x})
def f(x: Int): Int = f {x}
// error: missing arguments for method f
def f(x: Int): Int = f x
lcn
fuente
1. no es realmente cierto en todos los idiomas. 4. no es realmente cierto en Scala. Por ejemplo: def f (x: Int) = fx
aij
@aij, gracias por el comentario. Para 1, estaba sugiriendo la familiaridad que Scala proporciona para el {}comportamiento. He actualizado la redacción para mayor precisión. Y para 4, es un poco complicado debido a la interacción entre ()y {}, como def f(x: Int): Int = f {x}funciona, y es por eso que tuve el 5to. :)
lcn
1
Tiendo a pensar en () y {} como mayormente intercambiables en Scala, excepto que analiza los contenidos de manera diferente. Normalmente no escribo f ({x}), por lo que f {x} no tiene ganas de omitir paréntesis sino de reemplazarlos con curvas. Otros idiomas realmente le permiten omitir paréntesis, por ejemplo, fun f(x) = f xes válido en SML.
aij
@aij, tratar f {x}como f({x})parece ser una mejor explicación para mí, ya que pensar ()e {}intercambiar es menos intuitivo. Por cierto, la f({x})interpretación está algo respaldada por la especificación de Scala (sección 6.6):ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
lcn
13

Creo que vale la pena explicar su uso en las llamadas a funciones y por qué ocurren varias cosas. Como alguien ya dijo, las llaves definen un bloque de código, que también es una expresión, por lo que se puede colocar donde se espera la expresión y se evaluará. Cuando se evalúa, sus declaraciones se ejecutan y el valor de la última declaración es el resultado de la evaluación del bloque completo (algo así como en Ruby).

Teniendo eso, podemos hacer cosas como:

2 + { 3 }             // res: Int = 5
val x = { 4 }         // res: x: Int = 4
List({1},{2},{3})     // res: List[Int] = List(1,2,3)

El último ejemplo es solo una llamada de función con tres parámetros, de los cuales cada uno se evalúa primero.

Ahora, para ver cómo funciona con las llamadas a funciones, definamos una función simple que tome otra función como parámetro.

def foo(f: Int => Unit) = { println("Entering foo"); f(4) }

Para llamarlo, necesitamos pasar la función que toma un parámetro de tipo Int, para que podamos usar la función literal y pasarla a foo:

foo( x => println(x) )

Ahora, como se dijo antes, podemos usar un bloque de código en lugar de una expresión, así que usémoslo

foo({ x => println(x) })

Lo que sucede aquí es que se evalúa el código dentro de {}, y el valor de la función se devuelve como un valor de la evaluación del bloque, este valor luego se pasa a foo. Esto es semánticamente igual que la llamada anterior.

Pero podemos agregar algo más:

foo({ println("Hey"); x => println(x) })

Ahora nuestro bloque de código contiene dos declaraciones, y debido a que se evalúa antes de que se ejecute foo, lo que sucede es que primero se imprime "Hey", luego se pasa nuestra función a foo, se imprime "Enter foo" y finalmente se imprime "4" .

Sin embargo, esto parece un poco feo y Scala nos permite omitir el paréntesis en este caso, para que podamos escribir:

foo { println("Hey"); x => println(x) }

o

foo { x => println(x) }

Eso se ve mucho mejor y es equivalente a los anteriores. Aquí todavía se evalúa primero el bloque de código y el resultado de la evaluación (que es x => println (x)) se pasa como argumento a foo.

Lukasz Korzybski
fuente
1
¿Soy solo yo? pero en realidad prefiero la naturaleza explícita de foo({ x => println(x) }). Tal vez estoy demasiado atrapado en mis caminos ...
dade
7

Debido a que está utilizando case, está definiendo una función parcial y las funciones parciales requieren llaves.

fjdumont
fuente
1
Pedí una respuesta en general, no solo una respuesta para este ejemplo.
Marc-François
5

Mayor verificación de compilación con parens

Los autores de Spray recomiendan que los parens redondos proporcionen una mayor verificación de compilación. Esto es especialmente importante para DSL como Spray. Al usar parens, le está diciendo al compilador que solo se le debe dar una sola línea, por lo tanto, si accidentalmente le dio dos o más, se quejará. Ahora, este no es el caso con las llaves, si, por ejemplo, olvida a un operador en algún lugar donde su código se compilará, obtendrá resultados inesperados y potencialmente un error muy difícil de encontrar. A continuación se inventa (ya que las expresiones son puras y al menos darán una advertencia), pero hace el punto

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

El primero compila, el segundo da error: ')' expected but integer literal found.al autor que quería escribir 1 + 2 + 3.

Se podría argumentar que es similar para los métodos de parámetros múltiples con argumentos predeterminados; Es imposible olvidar accidentalmente una coma para separar los parámetros cuando se usan parens.

Verbosidad

Una nota importante a menudo pasada por alto sobre la verbosidad. El uso de llaves se convierte inevitablemente en un código detallado ya que la guía de estilo scala establece claramente que las llaves cerradas deben estar en su propia línea: http://docs.scala-lang.org/style/declarations.html "... la llave de cierre está en su propia línea inmediatamente después de la última línea de la función ". Muchos reformateadores automáticos, como en Intellij, realizarán automáticamente este reformateo por usted. Por lo tanto, trate de usar parens redondos cuando pueda. Por ejemplo, se List(1, 2, 3).reduceLeft{_ + _}convierte en:

List(1, 2, 3).reduceLeft {
  _ + _
}
samthebest
fuente
-2

Con llaves, tienes punto y coma inducido para ti y paréntesis no. Considere la takeWhilefunción, ya que espera una función parcial, solo {case xxx => ??? }es una definición válida en lugar de paréntesis alrededor de la expresión de mayúsculas y minúsculas.

keitina
fuente