Quelle est la différence formelle dans Scala entre les accolades et les parenthèses, et quand devraient-elles être utilisées?

329

Quelle est la différence formelle entre le passage d'arguments aux fonctions entre parenthèses ()et entre accolades {}?

Le sentiment que j'ai eu du livre Programmation en Scala est que Scala est assez flexible et que je devrais utiliser celui que je préfère, mais je trouve que certains cas se compilent tandis que d'autres ne le font pas.

Par exemple (juste à titre d'exemple; j'apprécierais toute réponse qui traite du cas général, pas seulement cet exemple particulier):

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

=> erreur: début illégal d'une expression simple

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

=> bien.

Jean-Philippe Pellet
la source

Réponses:

365

J'ai essayé une fois d'écrire à ce sujet, mais j'ai finalement abandonné, car les règles sont quelque peu diffuses. Fondamentalement, vous devrez vous y familiariser.

Il est peut-être préférable de se concentrer sur les endroits où les accolades et les parenthèses peuvent être utilisées de manière interchangeable: lors du passage des paramètres aux appels de méthode. Vous pouvez remplacer les parenthèses par des accolades si et seulement si la méthode attend un seul paramètre. Par exemple:

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

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

Cependant, vous devez en savoir plus pour mieux comprendre ces règles.

Augmentation de la vérification de la compilation avec des parens

Les auteurs de Spray recommandent des parens ronds car ils permettent une vérification accrue de la compilation. Ceci est particulièrement important pour les DSL comme Spray. En utilisant des parens, vous dites au compilateur qu'il ne doit lui être donné qu'une seule ligne; donc si vous lui en donnez accidentellement deux ou plus, il se plaindra. Maintenant, ce n'est pas le cas avec les accolades - si, par exemple, vous oubliez un opérateur quelque part, votre code se compilera et vous obtiendrez des résultats inattendus et potentiellement un bogue très difficile à trouver. Ci-dessous est artificiel (puisque les expressions sont pures et donneront au moins un avertissement), mais fait le point:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

Le premier compile, le second donne error: ')' expected but integer literal found. L'auteur a voulu écrire 1 + 2 + 3.

On pourrait dire que c'est similaire pour les méthodes multi-paramètres avec des arguments par défaut; il est impossible d'oublier accidentellement une virgule pour séparer les paramètres lors de l'utilisation de parens.

Verbosité

Une note importante souvent négligée sur la verbosité. L'utilisation d'accolades conduit inévitablement à un code détaillé car le guide de style Scala indique clairement que la fermeture des accolades doit être sur leur propre ligne:

… L'accolade fermante est sur sa propre ligne immédiatement après la dernière ligne de la fonction.

De nombreux reformatages automatiques, comme dans IntelliJ, effectueront automatiquement ce reformatage pour vous. Essayez donc de vous en tenir à des rondelles rondes lorsque vous le pouvez.

Notation d'infixe

Lorsque vous utilisez la notation infixe, comme List(1,2,3) indexOf (2)vous pouvez omettre les parenthèses s'il n'y a qu'un seul paramètre et l'écrire comme List(1, 2, 3) indexOf 2. Ce n'est pas le cas de la notation par points.

Notez également que lorsque vous avez un seul paramètre qui est une expression à plusieurs jetons, comme x + 2ou a => a % 2 == 0, vous devez utiliser des parenthèses pour indiquer les limites de l'expression.

Tuples

Parce que vous pouvez parfois omettre des parenthèses, parfois un tuple a besoin de parenthèses supplémentaires comme dans ((1, 2)), et parfois la parenthèse externe peut être omise, comme dans (1, 2). Cela peut être source de confusion.

Littéraux de fonction / fonction partielle avec case

Scala a une syntaxe pour les littéraux de fonction et de fonction partielle. Cela ressemble à ceci:

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

Les seuls autres endroits où vous pouvez utiliser des caseinstructions sont les mots clés matchet catch:

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

Vous ne pouvez pas utiliser des caseinstructions dans un autre contexte . Donc, si vous voulez utiliser case, vous avez besoin d' appareils orthopédiques. Au cas où vous vous demandez ce qui fait la distinction entre une fonction et une fonction partielle littérale, la réponse est: le contexte. Si Scala attend une fonction, une fonction que vous obtenez. S'il attend une fonction partielle, vous obtenez une fonction partielle. Si les deux sont attendus, cela donne une erreur d'ambiguïté.

Expressions et blocs

Des parenthèses peuvent être utilisées pour créer des sous-expressions. Les accolades peuvent être utilisées pour créer des blocs de code (ce n'est pas une fonction littérale, alors méfiez-vous d'essayer de l'utiliser comme une seule). Un bloc de code se compose de plusieurs instructions, chacune pouvant être une instruction d'importation, une déclaration ou une expression. Ça va comme ça:

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

Donc, si vous avez besoin de déclarations, de plusieurs déclarations, importou quelque chose comme ça, vous avez besoin d'accolades. Et comme une expression est une déclaration, des parenthèses peuvent apparaître entre accolades. Mais la chose intéressante est que les blocs de code sont également des expressions, vous pouvez donc les utiliser n'importe où dans une expression:

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

Donc, comme les expressions sont des instructions et que les blocs de codes sont des expressions, tout ce qui suit est valide:

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

Où ils ne sont pas interchangeables

Fondamentalement, vous ne pouvez pas remplacer {}par ()ou vice versa ailleurs. Par exemple:

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

Ce n'est pas un appel de méthode, vous ne pouvez donc pas l'écrire autrement. Eh bien, vous pouvez mettre des accolades à l' intérieur des parenthèses pour le condition, ainsi que des parenthèses à l' intérieur des accolades pour le bloc de code:

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

J'espère donc que cela aide.

Daniel C. Sobral
la source
53
C'est pourquoi les gens soutiennent que Scala est complexe. Et je m'appellerais un passionné de Scala.
andyczerwonka
Ne pas avoir à introduire une portée pour chaque méthode, je pense, rend le code Scala plus simple! Idéalement, aucune méthode ne devrait utiliser {}- tout devrait être une seule expression pure
samthebest
1
@andyczerwonka Je suis totalement d'accord mais c'est le prix naturel et inévitable (?) que vous payez pour la flexibilité et le pouvoir expressif => Scala n'est pas trop cher. Que ce soit le bon choix pour une situation particulière est bien sûr une autre affaire.
Ashkan Kh. Nazary
Bonjour, lorsque vous dites List{1, 2, 3}.reduceLeft(_ + _)invalide, voulez-vous dire qu'il a une erreur de syntaxe? Mais je trouve que le code peut être compilé. Je mets mon code ici
calvin
Vous avez utilisé List(1, 2, 3)dans tous les exemples, au lieu de List{1, 2, 3}. Hélas, sur la version actuelle de Scala (2.13), cela échoue avec un message d'erreur différent (virgule inattendue). Vous devrez probablement revenir à 2.7 ou 2.8 pour obtenir l'erreur d'origine, probablement.
Daniel C.Sobral du
56

Il y a quelques règles et inférences différentes en cours ici: tout d'abord, Scala déduit les accolades lorsqu'un paramètre est une fonction, par exemple dans list.map(_ * 2)les accolades sont inférées, c'est juste une forme plus courte de list.map({_ * 2}). Deuxièmement, Scala vous permet d'ignorer les parenthèses sur la dernière liste de paramètres, si cette liste de paramètres a un paramètre et qu'il s'agit d'une fonction, elle list.foldLeft(0)(_ + _)peut donc être écrite comme list.foldLeft(0) { _ + _ }(ou list.foldLeft(0)({_ + _})si vous voulez être plus explicite).

Cependant, si vous ajoutez, casevous obtenez, comme d'autres l'ont mentionné, une fonction partielle au lieu d'une fonction, et Scala ne déduira pas les accolades pour les fonctions partielles, donc list.map(case x => x * 2)ne fonctionnera pas, mais les deux list.map({case x => 2 * 2})et le list.map { case x => x * 2 }fera.

Théo
la source
4
Pas seulement de la dernière liste de paramètres. Par exemple, ça list.foldLeft{0}{_+_}marche.
Daniel C.Sobral
1
Ah, j'étais sûr d'avoir lu que ce n'était que la dernière liste de paramètres, mais clairement j'avais tort! Bon à savoir.
Theo
23

La communauté s'efforce de normaliser l'utilisation des accolades et des parenthèses, voir Guide de style Scala (page 21): http://www.codecommit.com/scala-style-guide.pdf

La syntaxe recommandée pour les appels de méthodes d'ordre supérieur consiste à toujours utiliser des accolades et à ignorer le point:

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

Pour les appels de métodes "normaux", vous devez utiliser le point et les parenthèses.

val result = myInstance.foo(5, "Hello")
olle kullberg
la source
18
En fait, la convention consiste à utiliser des accolades rondes, ce lien n'est pas officiel. En effet, dans la programmation fonctionnelle, toutes les fonctions NE SONT QUE des citoyens de premier ordre et ne doivent donc PAS être traitées différemment. Deuxièmement, Martin Odersky dit que vous devriez essayer d'utiliser uniquement l'infixe pour les méthodes de type opérateur (par exemple +, --), PAS les méthodes régulières comme takeWhile. Le point entier de la notation infixe est d'autoriser les DSL et les opérateurs personnalisés, donc on ne devrait pas l'utiliser dans ce contexte tout le temps.
samthebest
17

Je ne pense pas qu'il y ait quelque chose de particulier ou de complexe dans les accolades frisées à Scala. Pour maîtriser leur utilisation apparemment complexe dans Scala, gardez simplement à l'esprit quelques points simples:

  1. les accolades forment un bloc de code, qui correspond à la dernière ligne de code (presque toutes les langues le font)
  2. une fonction si vous le souhaitez peut être générée avec le bloc de code (suit la règle 1)
  3. les accolades peuvent être omises pour le code à une ligne, sauf pour une clause case (choix Scala)
  4. les parenthèses peuvent être omises dans l'appel de fonction avec le bloc de code comme paramètre (choix Scala)

Expliquons quelques exemples selon les trois règles ci-dessus:

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
la source
1. n'est pas vrai dans toutes les langues. 4. n'est pas vrai en Scala. Par exemple: def f (x: Int) = fx
aij
@aij, merci pour le commentaire. Pour 1, je suggérais la familiarité que Scala fournit pour le {}comportement. J'ai mis à jour le libellé pour plus de précision. Et pour 4, c'est un peu délicat en raison de l'interaction entre ()et {}, en tant que def f(x: Int): Int = f {x}travaux, et c'est pourquoi j'ai eu le 5ème. :)
lcn
1
J'ai tendance à penser à () et {} comme étant principalement interchangeables dans Scala, sauf qu'il analyse le contenu différemment. Je n'écris pas normalement f ({x}) donc f {x} n'a pas envie d'omettre les parenthèses autant que de les remplacer par des boucles. D'autres langues vous permettent en fait d'omettre les paréthèses, par exemple, fun f(x) = f xest valide en SML.
aij
@aij, traiter f {x}comme f({x})cela semble être une meilleure explication pour moi, car penser ()et {}interchangeable est moins intuitif. À propos, l' f({x})interprétation est quelque peu soutenue par la spécification Scala (section 6.6):ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
lcn
13

Je pense qu'il vaut la peine d'expliquer leur utilisation dans les appels de fonction et pourquoi diverses choses se produisent. Comme quelqu'un l'a déjà dit, les accolades définissent un bloc de code, qui est également une expression, donc peut être placé là où l'expression est attendue et elle sera évaluée. Lorsqu'elles sont évaluées, ses instructions sont exécutées et la valeur de la dernière instruction est le résultat de l'évaluation de bloc entier (un peu comme dans Ruby).

Ayant cela, nous pouvons faire des choses comme:

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

Le dernier exemple est juste un appel de fonction avec trois paramètres, dont chacun est évalué en premier.

Maintenant, pour voir comment cela fonctionne avec les appels de fonction, définissons une fonction simple qui prend une autre fonction comme paramètre.

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

Pour l'appeler, nous devons passer une fonction qui prend un paramètre de type Int, afin que nous puissions utiliser la fonction littérale et la passer à foo:

foo( x => println(x) )

Maintenant, comme dit précédemment, nous pouvons utiliser un bloc de code à la place d'une expression, alors utilisons-le

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

Ce qui se passe ici, c'est que le code à l'intérieur de {} est évalué et que la valeur de la fonction est renvoyée comme valeur de l'évaluation du bloc, cette valeur est ensuite passée à foo. C'est sémantiquement le même que l'appel précédent.

Mais nous pouvons ajouter quelque chose de plus:

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

Maintenant, notre bloc de code contient deux instructions, et parce qu'il est évalué avant l'exécution de foo, ce qui se passe est que le premier "Hey" est imprimé, puis notre fonction est passée à foo, "Entering foo" est imprimé et enfin "4" est imprimé .

Cela semble un peu moche cependant et Scala nous permet de sauter la parenthèse dans ce cas, afin que nous puissions écrire:

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

ou

foo { x => println(x) }

Cela semble beaucoup plus agréable et équivaut aux précédents. Ici encore, le bloc de code est évalué en premier et le résultat de l'évaluation (qui est x => println (x)) est passé en argument à foo.

Lukasz Korzybski
la source
1
Est-ce seulement moi. mais je préfère en fait la nature explicite de foo({ x => println(x) }). Peut-être que je suis trop coincé dans mes voies ...
dade
7

Parce que vous utilisez case, vous définissez une fonction partielle et les fonctions partielles nécessitent des accolades.

fjdumont
la source
1
J'ai demandé une réponse en général, pas seulement une réponse pour cet exemple.
Marc-François
5

Augmentation de la vérification de la compilation avec des parens

Les auteurs de Spray recommandent que les parens ronds donnent une vérification accrue de la compilation. Ceci est particulièrement important pour les DSL comme Spray. En utilisant des parens, vous dites au compilateur qu'il ne doit lui être donné qu'une seule ligne, donc si vous en avez accidentellement donné deux ou plus, il se plaindra. Maintenant, ce n'est pas le cas avec les accolades, si par exemple, vous oubliez un opérateur quelque part que votre code compilera, vous obtiendrez des résultats inattendus et potentiellement un bogue très difficile à trouver. Ci-dessous est artificiel (puisque les expressions sont pures et donneront au moins un avertissement), mais fait le point

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

Le premier compile, le second donne à error: ')' expected but integer literal found.l'auteur voulu écrire 1 + 2 + 3.

On pourrait dire que c'est similaire pour les méthodes multi-paramètres avec des arguments par défaut; il est impossible d'oublier accidentellement une virgule pour séparer les paramètres lors de l'utilisation de parens.

Verbosité

Une note importante souvent négligée sur la verbosité. L'utilisation d'accolades conduit inévitablement à un code détaillé car le guide de style scala indique clairement que les accolades fermantes doivent être sur leur propre ligne: http://docs.scala-lang.org/style/declarations.html "... l'accolade fermante est sur sa propre ligne immédiatement après la dernière ligne de la fonction. " De nombreux reformatages automatiques, comme dans Intellij, effectueront automatiquement ce reformatage pour vous. Essayez donc de vous en tenir à des rondelles rondes lorsque vous le pouvez. Par exemple List(1, 2, 3).reduceLeft{_ + _}devient:

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

Avec des accolades, vous avez un point-virgule induit pour vous et pas des parenthèses. Considérez la takeWhilefonction, car elle attend une fonction partielle, seule la {case xxx => ??? }définition est valide au lieu des parenthèses autour de l'expression de la casse.

keitine
la source