Was ist der formale Unterschied in Klammern zwischen Klammern und Klammern und wann sollten sie verwendet werden?


329

Was ist der formale Unterschied zwischen der Übergabe von Argumenten an Funktionen in Klammern ()und in geschweiften Klammern {}?

Das Gefühl, das ich durch das Programmieren in Scala- Buch bekommen habe, ist, dass Scala ziemlich flexibel ist und ich das verwenden sollte, das mir am besten gefällt, aber ich finde, dass einige Fälle kompiliert werden, während andere dies nicht tun.

Zum Beispiel (nur als Beispiel gedacht; ich würde mich über jede Antwort freuen, die den allgemeinen Fall behandelt, nicht nur dieses spezielle Beispiel):

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

=> Fehler: unzulässiger Start des einfachen Ausdrucks

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

=> gut.

Antworten:


365

Ich habe einmal versucht, darüber zu schreiben, aber ich habe am Ende aufgegeben, da die Regeln etwas diffus sind. Grundsätzlich müssen Sie den Dreh raus bekommen.

Vielleicht ist es am besten, sich darauf zu konzentrieren, wo geschweifte Klammern und Klammern austauschbar verwendet werden können: wenn Parameter an Methodenaufrufe übergeben werden. Sie können Klammern nur dann durch geschweifte Klammern ersetzen, wenn die Methode einen einzelnen Parameter erwartet. Zum Beispiel:

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

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

Sie müssen jedoch noch mehr wissen, um diese Regeln besser verstehen zu können.

Verbesserte Kompilierungsprüfung mit Parens

Die Autoren von Spray empfehlen runde Parens, da sie die Kompilierungsprüfung verbessern. Dies ist besonders wichtig für DSLs wie Spray. Durch die Verwendung von Parens teilen Sie dem Compiler mit, dass ihm nur eine einzige Zeile zugewiesen werden soll. Wenn Sie es versehentlich zwei oder mehr geben, wird es sich beschweren. Dies ist bei geschweiften Klammern nicht der Fall. Wenn Sie beispielsweise irgendwo einen Operator vergessen, wird Ihr Code kompiliert und Sie erhalten unerwartete Ergebnisse und möglicherweise einen sehr schwer zu findenden Fehler. Das Folgende ist erfunden (da die Ausdrücke rein sind und zumindest eine Warnung geben), macht aber den Punkt:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

Der erste kompiliert, der zweite gibt error: ')' expected but integer literal found. Der Autor wollte schreiben 1 + 2 + 3.

Man könnte argumentieren, dass es für Multi-Parameter-Methoden mit Standardargumenten ähnlich ist; Es ist unmöglich, versehentlich ein Komma zu vergessen, um Parameter zu trennen, wenn Parens verwendet werden.

Ausführlichkeit

Eine wichtige, oft übersehene Anmerkung zur Ausführlichkeit. Die Verwendung von geschweiften Klammern führt unweigerlich zu ausführlichem Code, da im Scala-Styleguide eindeutig angegeben ist, dass das Schließen von geschweiften Klammern in einer eigenen Zeile erfolgen muss:

… Die schließende Klammer befindet sich in einer eigenen Zeile unmittelbar nach der letzten Zeile der Funktion.

Viele automatische Neuformatierer, wie in IntelliJ, führen diese Neuformatierung automatisch für Sie durch. Versuchen Sie also, runde Parens zu verwenden, wenn Sie können.

Infix-Notation

Wenn Sie die Infix-Notation verwenden, List(1,2,3) indexOf (2)können Sie beispielsweise Klammern weglassen, wenn nur ein Parameter vorhanden ist, und ihn als schreiben List(1, 2, 3) indexOf 2. Dies ist bei der Punktnotation nicht der Fall.

Beachten Sie auch, dass Sie, wenn Sie einen einzelnen Parameter haben, der ein Ausdruck mit mehreren Token ist, wie x + 2oder a => a % 2 == 0, Klammern verwenden müssen, um die Grenzen des Ausdrucks anzugeben.

Tupel

Da Sie manchmal Klammern weglassen können, benötigt ein Tupel manchmal zusätzliche Klammern wie in ((1, 2)), und manchmal kann die äußere Klammer wie in weggelassen werden (1, 2). Dies kann zu Verwirrung führen.

Funktions- / Teilfunktionsliterale mit case

Scala hat eine Syntax für Funktions- und Teilfunktionsliterale. Es sieht aus wie das:

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

Die einzigen anderen Stellen, an denen Sie caseAnweisungen verwenden können, sind die Schlüsselwörter matchund catch:

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

Sie können keine caseAnweisungen in einem anderen Kontext verwenden . Wenn Sie also verwenden möchten case, benötigen Sie geschweifte Klammern. Wenn Sie sich fragen, was die Unterscheidung zwischen einer Funktion und einer Teilfunktion wörtlich macht, lautet die Antwort: Kontext. Wenn Scala eine Funktion erwartet, erhalten Sie eine Funktion. Wenn eine Teilfunktion erwartet wird, erhalten Sie eine Teilfunktion. Wenn beide erwartet werden, gibt es einen Fehler bezüglich der Mehrdeutigkeit.

Ausdrücke und Blöcke

Klammern können verwendet werden, um Unterausdrücke zu machen. Geschweifte Klammern können verwendet werden, um Codeblöcke zu erstellen (dies ist kein Funktionsliteral, also hüten Sie sich davor, es wie eines zu verwenden). Ein Codeblock besteht aus mehreren Anweisungen, von denen jede eine Importanweisung, eine Deklaration oder ein Ausdruck sein kann. Es geht so:

{
    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 )

Wenn Sie also Deklarationen, mehrere Anweisungen importoder ähnliches benötigen, benötigen Sie geschweifte Klammern. Und weil ein Ausdruck eine Aussage ist, können Klammern in geschweiften Klammern stehen. Das Interessante ist jedoch, dass Codeblöcke auch Ausdrücke sind, sodass Sie sie überall in einem Ausdruck verwenden können:

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

Da Ausdrücke Anweisungen und Codeblöcke Ausdrücke sind, gilt Folgendes:

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...

Wo sie nicht austauschbar sind

Grundsätzlich kann man nicht ersetzen {}mit ()oder umgekehrt anderswo. Zum Beispiel:

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

Dies ist kein Methodenaufruf, daher können Sie ihn nicht auf andere Weise schreiben. Nun, Sie können geschweifte Klammern in die Klammern für das setzen conditionsowie Klammern in die geschweiften Klammern für den Codeblock verwenden:

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

Ich hoffe, das hilft.


53
Deshalb argumentieren die Leute, dass Scala komplex ist. Und ich würde mich Scala-Enthusiast nennen.
andyczerwonka

Wenn ich nicht für jede Methode einen Bereich einführen muss, wird der Scala-Code meiner Meinung nach einfacher! Im Idealfall sollte keine Methode verwenden {}- alles ein einziger reiner Ausdruck sein sollte
samthebest

1
@andyczerwonka Ich stimme vollkommen zu, aber es ist der natürliche und unvermeidliche Preis (?), den Sie für die Flexibilität und die Ausdruckskraft zahlen => Scala ist nicht überteuert. Ob dies die richtige Wahl für eine bestimmte Situation ist, ist natürlich eine andere Sache.
Ashkan Kh. Nazary

Hallo, wenn Sie sagen, dass List{1, 2, 3}.reduceLeft(_ + _)es ungültig ist, meinen Sie damit, dass die Syntax fehlerhaft ist? Aber ich finde, dass Code kompiliert werden kann. Ich habe meinen Code hier
eingegeben

Sie haben List(1, 2, 3)in allen Beispielen anstelle von verwendet List{1, 2, 3}. Leider schlägt dies in Scalas aktueller Version (2.13) mit einer anderen Fehlermeldung (unerwartetes Komma) fehl. Sie müssten wahrscheinlich zu 2.7 oder 2.8 zurückkehren, um den ursprünglichen Fehler zu erhalten.
Daniel C. Sobral

56

Hier gibt es ein paar verschiedene Regeln und Schlussfolgerungen: Zunächst leitet Scala die Klammern ab, wenn ein Parameter eine Funktion ist, z. B. wenn list.map(_ * 2)die Klammern abgeleitet werden, ist es nur eine kürzere Form von list.map({_ * 2}). Zweitens können Sie mit Scala die Klammern in der letzten Parameterliste überspringen, wenn diese Parameterliste einen Parameter enthält und eine Funktion ist, list.foldLeft(0)(_ + _)die als geschrieben werden kann list.foldLeft(0) { _ + _ }(oder list.foldLeft(0)({_ + _})wenn Sie besonders explizit sein möchten).

Wenn Sie jedoch hinzufügen, erhalten caseSie, wie andere bereits erwähnt haben, eine Teilfunktion anstelle einer Funktion, und Scala leitet die Klammern für Teilfunktionen nicht ab, funktioniert also list.map(case x => x * 2)nicht, sondern beides list.map({case x => 2 * 2})und list.map { case x => x * 2 }wird.


4
Nicht nur von der letzten Parameterliste. Zum Beispiel list.foldLeft{0}{_+_}funktioniert.
Daniel C. Sobral

1
Ah, ich war mir sicher, dass ich gelesen hatte, dass es nur die letzte Parameterliste war, aber ich habe mich eindeutig geirrt! Gut zu wissen.
Theo

23

Die Community bemüht sich, die Verwendung von Klammern und Klammern zu standardisieren, siehe Scala Style Guide (Seite 21): http://www.codecommit.com/scala-style-guide.pdf

Die empfohlene Syntax für Methodenaufrufe höherer Ordnung besteht darin, immer geschweifte Klammern zu verwenden und den Punkt zu überspringen:

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

Für "normale" Metod-Aufrufe sollten Sie den Punkt und die Klammern verwenden.

val result = myInstance.foo(5, "Hello")

18
Eigentlich ist die Konvention, runde Klammern zu verwenden, dieser Link ist nicht offiziell. Dies liegt daran, dass in der funktionalen Programmierung alle Funktionen nur Bürger erster Ordnung sind und daher NICHT anders behandelt werden sollten. Zweitens Martin Odersky sagt , Sie sollten nur versuchen Infix wie Methoden für Operator zu verwenden (zB +, --), NICHT regelmäßige Methoden wie takeWhile. Der gesamte Punkt der Infix-Notation besteht darin, DSLs und benutzerdefinierte Operatoren zuzulassen. Daher sollte man sie in diesem Zusammenhang nicht immer verwenden.
Samthebest

17

Ich glaube nicht, dass geschweifte Zahnspangen in Scala etwas Besonderes oder Komplexes haben. Um die scheinbar komplexe Verwendung in Scala zu beherrschen, sollten Sie nur ein paar einfache Dinge beachten:

  1. geschweifte Klammern bilden einen Codeblock, der bis zur letzten Codezeile ausgewertet wird (fast alle Sprachen tun dies)
  2. Auf Wunsch kann mit dem Codeblock eine Funktion generiert werden (folgt Regel 1).
  3. geschweifte Klammern können für einzeiligen Code mit Ausnahme einer case-Klausel weggelassen werden (Scala-Auswahl)
  4. Klammern können im Funktionsaufruf mit Codeblock als Parameter weggelassen werden (Scala-Auswahl)

Lassen Sie uns ein paar Beispiele anhand der oben genannten drei Regeln erklären:

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

1. ist nicht in allen Sprachen wahr. 4. ist in Scala eigentlich nicht wahr. ZB: def f (x: Int) = fx
aij

@aij, danke für den Kommentar. Zum einen schlug ich die Vertrautheit vor, die Scala für das {}Verhalten bereitstellt . Ich habe den Wortlaut aus Gründen der Genauigkeit aktualisiert. Und für 4 ist es ein bisschen schwierig aufgrund der Interaktion zwischen ()und {}, wie es def f(x: Int): Int = f {x}funktioniert, und deshalb hatte ich die 5 .. :)
lcn

1
Ich neige dazu, () und {} in Scala als meist austauschbar zu betrachten, außer dass es den Inhalt anders analysiert. Normalerweise schreibe ich nicht f ({x}), daher möchte f {x} keine Klammern weglassen, sondern sie durch Locken ersetzen. In anderen Sprachen können Sie tatsächlich Parethesen weglassen. Dies ist beispielsweise fun f(x) = f xin SML gültig.
aij

@aij, das Behandeln f {x}als f({x})scheint eine bessere Erklärung für mich zu sein, da das Denken ()und {}Austauschen weniger intuitiv ist. Übrigens wird die f({x})Interpretation etwas durch Scala spec (Abschnitt 6.6) gestützt:ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
lcn

13

Ich denke, es lohnt sich, ihre Verwendung in Funktionsaufrufen zu erklären und zu erklären, warum verschiedene Dinge passieren. Wie bereits erwähnt, definieren geschweifte Klammern einen Codeblock, der auch ein Ausdruck ist. Er kann also dort platziert werden, wo ein Ausdruck erwartet wird, und er wird ausgewertet. Bei der Auswertung werden die Anweisungen ausgeführt, und der Anweisungswert von last ist das Ergebnis der gesamten Blockbewertung (ähnlich wie in Ruby).

Damit können wir Dinge tun wie:

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

Das letzte Beispiel ist nur ein Funktionsaufruf mit drei Parametern, von denen jeder zuerst ausgewertet wird.

Um zu sehen, wie es mit Funktionsaufrufen funktioniert, definieren wir eine einfache Funktion, die eine andere Funktion als Parameter verwendet.

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

Um es aufzurufen, müssen wir eine Funktion übergeben, die einen Parameter vom Typ Int annimmt, damit wir das Funktionsliteral verwenden und an foo übergeben können:

foo( x => println(x) )

Wie bereits erwähnt, können wir anstelle eines Ausdrucks einen Codeblock verwenden. Verwenden wir ihn also

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

Was hier passiert, ist, dass Code in {} ausgewertet wird und der Funktionswert als Wert der Blockauswertung zurückgegeben wird. Dieser Wert wird dann an foo übergeben. Dies ist semantisch dasselbe wie beim vorherigen Aufruf.

Aber wir können noch etwas hinzufügen:

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

Jetzt enthält unser Codeblock zwei Anweisungen, und da er ausgewertet wird, bevor foo ausgeführt wird, wird zuerst "Hey" gedruckt, dann wird unsere Funktion an foo übergeben, "foo eingeben" wird gedruckt und zuletzt "4" wird gedruckt .

Das sieht allerdings etwas hässlich aus und Scala lässt uns in diesem Fall die Klammern überspringen, damit wir schreiben können:

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

oder

foo { x => println(x) }

Das sieht viel schöner aus und entspricht den ersteren. Hier wird noch ein Codeblock zuerst ausgewertet und das Ergebnis der Auswertung (x => println (x)) als Argument an foo übergeben.


1
Ist es nur ich. aber ich bevorzuge eigentlich die explizite Natur von foo({ x => println(x) }). Vielleicht bin ich zu fest in meinen Wegen ...
dade

7

Da Sie verwenden case, definieren Sie eine Teilfunktion und Teilfunktionen erfordern geschweifte Klammern.


1
Ich habe allgemein um eine Antwort gebeten, nicht nur um eine Antwort für dieses Beispiel.
Marc-François

5

Verbesserte Kompilierungsprüfung mit Parens

Die Autoren von Spray empfehlen, dass runde Parens eine verstärkte Kompilierungsprüfung durchführen. Dies ist besonders wichtig für DSLs wie Spray. Durch die Verwendung von Parens teilen Sie dem Compiler mit, dass ihm nur eine einzige Zeile zugewiesen werden soll. Wenn Sie ihm also versehentlich zwei oder mehr Zeilen gegeben haben, wird er sich beschweren. Dies ist bei geschweiften Klammern nicht der Fall. Wenn Sie beispielsweise einen Operator vergessen, an dem Ihr Code kompiliert wird, erhalten Sie unerwartete Ergebnisse und möglicherweise einen sehr schwer zu findenden Fehler. Unten ist erfunden (da die Ausdrücke rein sind und zumindest eine Warnung geben), macht aber den Punkt

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

Das erste kompiliert, das zweite gibt error: ')' expected but integer literal found.der Autor schreiben wollte 1 + 2 + 3.

Man könnte argumentieren, dass es für Multi-Parameter-Methoden mit Standardargumenten ähnlich ist; Es ist unmöglich, versehentlich ein Komma zu vergessen, um Parameter zu trennen, wenn Parens verwendet werden.

Ausführlichkeit

Eine wichtige, oft übersehene Anmerkung zur Ausführlichkeit. Die Verwendung von geschweiften Klammern führt unweigerlich zu ausführlichem Code, da im Scala-Styleguide eindeutig angegeben ist, dass das Schließen von geschweiften Klammern in einer eigenen Zeile erfolgen muss: http://docs.scala-lang.org/style/declarations.html "... die schließende Klammer steht in einer eigenen Zeile unmittelbar nach der letzten Zeile der Funktion. " Viele automatische Neuformatierer, wie in Intellij, führen diese Neuformatierung automatisch für Sie durch. Versuchen Sie also, runde Parens zu verwenden, wenn Sie können. ZB List(1, 2, 3).reduceLeft{_ + _}wird:

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

-2

Bei geschweiften Klammern wurde ein Semikolon für Sie induziert und bei Klammern nicht. Betrachten Sie die takeWhileFunktion, da sie eine Teilfunktion erwartet, nur {case xxx => ??? }eine gültige Definition anstelle von Klammern um den Fallausdruck.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.