Qual è la differenza formale in Scala tra parentesi graffe e parentesi, e quando dovrebbero essere usati?


329

Qual è la differenza formale tra passare argomenti a funzioni tra parentesi ()e parentesi graffe {}?

La sensazione che ho avuto dal libro di Programming in Scala è che Scala è piuttosto flessibile e dovrei usare quello che mi piace di più, ma trovo che alcuni casi si compilino mentre altri no.

Ad esempio (inteso solo come esempio; apprezzerei qualsiasi risposta che discute il caso generale, non solo questo esempio particolare):

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

=> errore: inizio illegale dell'espressione semplice

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

=> bene.

Risposte:


365

Ho provato una volta a scrivere di questo, ma alla fine mi sono arreso, poiché le regole sono piuttosto diffuse. Fondamentalmente, dovrai prenderne il controllo.

Forse è meglio concentrarsi su dove parentesi graffe e parentesi possono essere usate in modo intercambiabile: quando si passano i parametri alle chiamate di metodo. È possibile sostituire la parentesi con parentesi graffe se, e solo se, il metodo prevede un singolo parametro. Per esempio:

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

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

Tuttavia, c'è altro che devi sapere per capire meglio queste regole.

Maggiore controllo della compilazione con parentesi

Gli autori di Spray raccomandano parentesi rotonde perché offrono un maggiore controllo di compilazione. Ciò è particolarmente importante per i DSL come Spray. Usando le parentesi stai dicendo al compilatore che dovrebbe essere data una sola riga; quindi se gli dai accidentalmente due o più, si lamenterà. Ora questo non è il caso delle parentesi graffe: se per esempio dimentichi un operatore da qualche parte, il tuo codice verrà compilato e otterrai risultati imprevisti e potenzialmente un bug molto difficile da trovare. Di seguito è inventato (poiché le espressioni sono pure e daranno almeno un avvertimento), ma sottolinea il punto:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

Il primo compila, il secondo dà error: ')' expected but integer literal found. L'autore voleva scrivere 1 + 2 + 3.

Si potrebbe sostenere che è simile per i metodi multiparametrici con argomenti predefiniti; è impossibile dimenticare accidentalmente una virgola per separare i parametri quando si usano le parentesi.

Verbosità

Una nota importante spesso trascurata sulla verbosità. L'uso delle parentesi graffe porta inevitabilmente a un codice dettagliato poiché la guida di stile Scala afferma chiaramente che la chiusura delle parentesi graffe deve essere sulla loro stessa linea:

... la parentesi graffa di chiusura si trova sulla propria riga immediatamente dopo l'ultima riga della funzione.

Molti auto-riformatori, come in IntelliJ, eseguiranno automaticamente questa riformattazione per te. Quindi cerca di continuare a usare le parentesi rotonde quando puoi.

Notazione Infix

Quando si utilizza la notazione infissa, come List(1,2,3) indexOf (2)è possibile omettere la parentesi se esiste un solo parametro e scriverlo come List(1, 2, 3) indexOf 2. Questo non è il caso della notazione a punti.

Si noti inoltre che quando si dispone di un singolo parametro che è un'espressione multi-token, come x + 2o a => a % 2 == 0, è necessario utilizzare la parentesi per indicare i limiti dell'espressione.

Le tuple

Poiché a volte puoi omettere la parentesi, a volte una tupla ha bisogno di parentesi extra come in ((1, 2)), e talvolta la parentesi esterna può essere omessa, come in (1, 2). Ciò può causare confusione.

Letterali funzione / funzione parziale con case

Scala ha una sintassi per letterali di funzione e funzione parziale. Sembra così:

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

Gli unici altri luoghi in cui è possibile utilizzare le caseistruzioni sono con le parole chiave matche catch:

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

Non è possibile utilizzare le caseistruzioni in nessun altro contesto . Quindi, se vuoi usare case, hai bisogno di parentesi graffe. Nel caso ti stia chiedendo cosa renda letterale la distinzione tra funzione e funzione parziale, la risposta è: contesto. Se Scala prevede una funzione, una funzione che ottieni. Se si aspetta una funzione parziale, si ottiene una funzione parziale. Se sono previsti entrambi, viene visualizzato un errore sull'ambiguità.

Espressioni e blocchi

Le parentesi possono essere utilizzate per creare sottoespressioni. Le parentesi graffe possono essere utilizzate per creare blocchi di codice (questa non è una funzione letterale, quindi fai attenzione a provare a usarla come una). Un blocco di codice è costituito da più istruzioni, ciascuna delle quali può essere un'istruzione di importazione, una dichiarazione o un'espressione. Va così:

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

Quindi, se hai bisogno di dichiarazioni, dichiarazioni multiple, importo qualcosa del genere, hai bisogno di parentesi graffe. E poiché un'espressione è un'affermazione, la parentesi può apparire tra parentesi graffe. Ma la cosa interessante è che i blocchi di codice sono anche espressioni, quindi puoi usarli ovunque all'interno di un'espressione:

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

Quindi, poiché le espressioni sono dichiarazioni e blocchi di codici sono espressioni, tutto ciò che segue è valido:

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

Dove non sono intercambiabili

Fondamentalmente, non è possibile sostituire {}con ()o viceversa altrove. Per esempio:

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

Questa non è una chiamata di metodo, quindi non puoi scriverla in nessun altro modo. Bene, puoi inserire parentesi graffe all'interno della parentesi per il condition, così come usare parentesi all'interno delle parentesi graffe per il blocco di codice:

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

Quindi, spero che questo aiuti.


53
Ecco perché la gente sostiene che Scala è complessa. Mi definirei un appassionato di Scala.
andyczerwonka,

Non dover introdurre un ambito per ogni metodo che penso semplifichi il codice Scala! Idealmente nessun metodo dovrebbe usare {}- tutto dovrebbe essere una singola espressione pura
possibile il

1
@andyczerwonka Sono assolutamente d'accordo, ma è il prezzo naturale e inevitabile (?) che si paga per la flessibilità e il potere espressivo => Scala non è troppo caro. Se questa è la scelta giusta per ogni situazione particolare, ovviamente è un'altra questione.
Ashkan Kh. Nazary,

Ciao, quando dici che List{1, 2, 3}.reduceLeft(_ + _)non è valido, vuoi dire che ha la sintassi err? Ma scopro che quel codice può essere compilato. Ho inserito qui il
calvin,

Hai usato List(1, 2, 3)tutti gli esempi, invece di List{1, 2, 3}. Purtroppo, sulla versione attuale di Scala (2.13), ciò non riesce con un messaggio di errore diverso (virgola imprevista). Dovresti tornare a 2.7 o 2.8 per ottenere l'errore originale, probabilmente.
Daniel C. Sobral,

56

Ci sono un paio di regole e inferenze diverse in corso qui: prima di tutto, Scala infila le parentesi graffe quando un parametro è una funzione, ad esempio nelle list.map(_ * 2)parentesi graffe vengono dedotte, è solo una forma più breve di list.map({_ * 2}). In secondo luogo, Scala consente di saltare le parentesi sull'ultimo elenco di parametri, se tale elenco di parametri ha un parametro ed è una funzione, quindi list.foldLeft(0)(_ + _)può essere scritto come list.foldLeft(0) { _ + _ }(o list.foldLeft(0)({_ + _})se si desidera essere espliciti in più).

Tuttavia, se aggiungi case, ottieni, come altri hanno già detto, una funzione parziale anziché una funzione, e Scala non inferirà le parentesi graffe per le funzioni parziali, quindi list.map(case x => x * 2)non funzionerà, ma entrambi list.map({case x => 2 * 2})e lo list.map { case x => x * 2 }faranno.


4
Non solo dell'ultimo elenco di parametri. Ad esempio, list.foldLeft{0}{_+_}funziona.
Daniel C. Sobral,

1
Ah, ero sicuro di aver letto che era solo l'ultimo elenco di parametri, ma chiaramente mi sbagliavo! Buono a sapersi.
Theo

23

C'è uno sforzo da parte della comunità per standardizzare l'uso di parentesi graffe e parentesi, vedere la Guida allo stile di Scala (pagina 21): http://www.codecommit.com/scala-style-guide.pdf

La sintassi consigliata per le chiamate di metodi di ordine superiore è quella di utilizzare sempre le parentesi graffe e di saltare il punto:

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

Per chiamate al metodo "normali" è necessario utilizzare il punto e le parentesi.

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

18
In realtà la convenzione prevede l'uso di parentesi tonde, tale collegamento non è ufficiale. Questo perché nella programmazione funzionale tutte le funzioni SONO solo cittadini del primo ordine e quindi NON dovrebbero essere trattate diversamente. In secondo luogo, Martin Odersky dice che dovresti provare a usare solo infix per metodi simili agli operatori (ad es +. --), NON metodi regolari come takeWhile. L'intero punto della notazione infix è consentire DSL e operatori personalizzati, quindi si dovrebbe usarlo in questo contesto non sempre.
Samthebest il

17

Non credo che ci sia qualcosa di particolare o complesso nelle parentesi graffe in Scala. Per padroneggiare il loro uso apparentemente complesso in Scala, tieni a mente solo un paio di cose semplici:

  1. le parentesi graffe formano un blocco di codice, che valuta l'ultima riga di codice (quasi tutte le lingue lo fanno)
  2. una funzione se lo si desidera può essere generata con il blocco di codice (segue la regola 1)
  3. le parentesi graffe possono essere omesse per il codice di una riga tranne una clausola case (scelta Scala)
  4. le parentesi possono essere omesse nella chiamata di funzione con blocco di codice come parametro (scelta Scala)

Spieghiamo un paio di esempi per le tre regole precedenti:

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. non è effettivamente vero in tutte le lingue. 4. in realtà non è vero in Scala. Ad esempio: def f (x: Int) = fx
aij

@aij, grazie per il commento. Per 1, stavo suggerendo la familiarità che Scala fornisce per il {}comportamento. Ho aggiornato la formulazione per la precisione. E per 4, è un po 'complicato a causa dell'interazione tra ()e {}, come def f(x: Int): Int = f {x}funziona, ed è per questo che ho avuto il 5 °. :)
lcn,

1
Tendo a considerare () e {} come per lo più intercambiabili in Scala, tranne per il fatto che analizza i contenuti in modo diverso. Normalmente non scrivo f ({x}), quindi f {x} non ha voglia di omettere le parentesi quanto di sostituirle con i ricci. Altre lingue in realtà ti consentono di omettere le paretesi, ad esempio, fun f(x) = f xè valido in SML.
aij,

@aij, trattando f {x}come f({x})sembra essere una migliore spiegazione per me, come il pensiero di ()e {}intercambiabili è meno intuitiva. A proposito, l' f({x})interpretazione è in qualche modo supportata dalle specifiche di Scala (sezione 6.6):ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
lcn

13

Penso che valga la pena spiegare il loro utilizzo nelle chiamate di funzione e perché accadono varie cose. Come qualcuno ha già detto, le parentesi graffe definiscono un blocco di codice, che è anche un'espressione, quindi può essere messo dove l'espressione è prevista e verrà valutata. Quando valutato, le sue istruzioni vengono eseguite e il valore dell'istruzione last è il risultato della valutazione dell'intero blocco (un po 'come in Ruby).

Visto che possiamo fare cose come:

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

L'ultimo esempio è solo una chiamata di funzione con tre parametri, di cui ciascuno viene valutato per primo.

Ora per vedere come funziona con le chiamate di funzione, definiamo una funzione semplice che accetta un'altra funzione come parametro.

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

Per chiamarlo, dobbiamo passare la funzione che accetta un parametro di tipo Int, quindi possiamo usare la funzione letterale e passarla a pippo:

foo( x => println(x) )

Ora, come detto prima, possiamo usare il blocco di codice al posto di un'espressione, quindi usiamolo

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

Quello che succede qui è che il codice all'interno di {} viene valutato e il valore della funzione viene restituito come valore della valutazione del blocco, questo valore viene quindi passato a pippo. Questo è semanticamente lo stesso della chiamata precedente.

Ma possiamo aggiungere qualcosa di più:

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

Ora il nostro blocco di codice contiene due istruzioni e, poiché viene valutato prima dell'esecuzione di foo, ciò che accade è che prima viene stampato "Hey", quindi la nostra funzione viene passata a foo, viene stampato "Entering foo" e infine viene stampato "4" .

Sembra un po 'brutto e Scala ci consente di saltare la parentesi in questo caso, quindi possiamo scrivere:

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

o

foo { x => println(x) }

Sembra molto più bello ed equivale a quelli precedenti. Qui viene ancora valutato il blocco di codice e il risultato della valutazione (che è x => println (x)) viene passato come argomento a pippo.


1
Sono solo io. ma in realtà preferisco la natura esplicita di foo({ x => println(x) }). Forse sono troppo bloccato nei miei modi ...
Dade,

7

Poiché si sta utilizzando case, si sta definendo una funzione parziale e le funzioni parziali richiedono parentesi graffe.


1
Ho chiesto una risposta in generale, non solo una risposta per questo esempio.
Marc-François,

5

Maggiore controllo della compilazione con parentesi

Gli autori di Spray, raccomandano che le parentesi tonde forniscano un maggiore controllo di compilazione. Ciò è particolarmente importante per i DSL come Spray. Usando le parentesi stai dicendo al compilatore che dovrebbe essere data una sola riga, quindi se gli hai dato accidentalmente due o più, si lamenterà. Ora questo non è il caso delle parentesi graffe, se, ad esempio, dimentichi un operatore in un punto in cui il codice verrà compilato, otterrai risultati imprevisti e potenzialmente un bug molto difficile da trovare. Di seguito è inventato (poiché le espressioni sono pure e daranno almeno un avvertimento), ma sottolinea il punto

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

Il primo compila, il secondo dà error: ')' expected but integer literal found.all'autore voluto scrivere 1 + 2 + 3.

Si potrebbe sostenere che è simile per i metodi multiparametrici con argomenti predefiniti; è impossibile dimenticare accidentalmente una virgola per separare i parametri quando si usano le parentesi.

Verbosità

Una nota importante spesso trascurata sulla verbosità. L'uso delle parentesi graffe porta inevitabilmente a un codice dettagliato poiché la guida dello stile scala afferma chiaramente che le parentesi graffe di chiusura devono essere sulla loro stessa linea: http://docs.scala-lang.org/style/declarations.html "... la parentesi graffa di chiusura è sulla propria riga immediatamente dopo l'ultima riga della funzione. " Molti auto-riformatori, come in Intellij, eseguiranno automaticamente questa riformattazione per te. Quindi cerca di continuare a usare le parentesi rotonde quando puoi. Ad esempio List(1, 2, 3).reduceLeft{_ + _}diventa:

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

-2

Con le parentesi graffe, hai il punto e virgola indotto per te e non le parentesi. Considerare la takeWhilefunzione, poiché prevede una funzione parziale, {case xxx => ??? }è solo una definizione valida anziché parentesi attorno all'espressione maiuscola / minuscola.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.