Qual é a diferença formal em Scala entre chaves e parênteses, e quando devem ser usados?

329

Qual é a diferença formal entre passar argumentos para funções entre parênteses ()e chaves {}?

A sensação que tive do livro Programação em Scala é que o Scala é bastante flexível e devo usar o que mais gosto, mas acho que alguns casos são compilados, enquanto outros não.

Por exemplo (apenas como exemplo; eu apreciaria qualquer resposta que discuta o caso geral, não apenas este exemplo específico):

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

=> erro: início ilegal de expressão simples

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

=> bem.

Jean-Philippe Pellet
fonte

Respostas:

365

Tentei escrever uma vez sobre isso, mas desisti no final, pois as regras são um pouco difusas. Basicamente, você terá que pegar o jeito.

Talvez seja melhor se concentrar em onde chaves e parênteses podem ser usados ​​de forma intercambiável: ao passar parâmetros para chamadas de método. Você pode substituir parênteses por chaves se, e somente se, o método esperar um único parâmetro. Por exemplo:

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

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

No entanto, é preciso saber mais para entender melhor essas regras.

Maior verificação de compilação com parens

Os autores do Spray recomendam parênteses redondos porque fornecem maior verificação de compilação. Isso é especialmente importante para DSLs como o Spray. Ao usar parens, você está dizendo ao compilador que ele deve receber apenas uma única linha; portanto, se você acidentalmente der dois ou mais, ele reclamará. Agora, esse não é o caso das chaves - se, por exemplo, você esquecer um operador em algum lugar, seu código será compilado e você obterá resultados inesperados e, potencialmente, um bug muito difícil de encontrar. Abaixo é inventado (já que as expressões são puras e pelo menos darão um aviso), mas defende o argumento:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

A primeira compila, a segunda dá error: ')' expected but integer literal found. O autor quis escrever 1 + 2 + 3.

Pode-se argumentar que é semelhante para métodos multiparâmetros com argumentos padrão; é impossível esquecer acidentalmente uma vírgula para separar parâmetros ao usar parênteses.

Verbosidade

Uma observação importante, muitas vezes esquecida, sobre a verbosidade. O uso de chavetas inevitavelmente leva a códigos detalhados, pois o guia de estilo Scala afirma claramente que os chavetas de fechamento devem estar em sua própria linha:

… A chave de fechamento está em sua própria linha imediatamente após a última linha da função.

Muitos reformadores automáticos, como no IntelliJ, executam automaticamente essa reformatação para você. Portanto, tente continuar usando parênteses redondas quando puder.

Notação Infix

Ao usar a notação infix, List(1,2,3) indexOf (2)é possível omitir parênteses se houver apenas um parâmetro e escrevê-lo como List(1, 2, 3) indexOf 2. Este não é o caso da notação de ponto.

Observe também que, quando você tem um único parâmetro que é uma expressão com vários tokens, como x + 2ou a => a % 2 == 0, é necessário usar parênteses para indicar os limites da expressão.

Tuplas

Como você pode omitir parênteses algumas vezes, algumas vezes uma tupla precisa de parênteses extras, como em ((1, 2)), e algumas vezes o parêntese externo pode ser omitido, como em (1, 2). Isso pode causar confusão.

Literais de Função / Função Parcial com case

Scala tem uma sintaxe para literais de função e função parcial. Se parece com isso:

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

Os únicos outros lugares onde você pode usar caseinstruções são com as palavras-chave 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
}

Você não pode usar caseinstruções em nenhum outro contexto . Então, se você quiser usar case, precisará de chaves. Caso você esteja se perguntando o que torna literal a distinção entre uma função e uma função parcial, a resposta é: contexto. Se Scala espera uma função, você recebe uma função. Se ele espera uma função parcial, você obtém uma função parcial. Se ambos forem esperados, ocorrerá um erro sobre ambiguidade.

Expressões e blocos

Parênteses podem ser usados ​​para fazer subexpressões. Os chavetas podem ser usadas para criar blocos de código (essa não é uma função literal, portanto, tente usá-la como uma). Um bloco de código consiste em várias instruções, cada uma das quais pode ser uma declaração de importação, uma declaração ou uma expressão. É assim:

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

Portanto, se você precisar de declarações, várias instruções importou algo parecido, precisará de chaves. E como uma expressão é uma declaração, parênteses podem aparecer dentro de chaves. Mas o interessante é que os blocos de código também são expressões, para que você possa usá-los em qualquer lugar dentro de uma expressão:

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

Portanto, como expressões são instruções e blocos de códigos são expressões, tudo abaixo é 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...

Onde eles não são intercambiáveis

Basicamente, você não pode substituir {}com ()ou vice-versa em qualquer outro lugar. Por exemplo:

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

Como não é uma chamada de método, não é possível escrevê-la de nenhuma outra maneira. Bem, você pode colocar chaves dentro dos parênteses para o condition, bem como usar parênteses dentro dos chaves para o bloco de código:

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

Então, espero que isso ajude.

Daniel C. Sobral
fonte
53
É por isso que as pessoas argumentam que Scala é complexo. E eu me consideraria um entusiasta do Scala.
precisa saber é o seguinte
Não ter que introduzir um escopo para todos os métodos que eu acho que torna o código Scala mais simples! Idealmente nenhum método deve usar {}- tudo deve ser uma única expressão pura
samthebest
1
@andyczerwonka Eu concordo totalmente, mas é o preço natural e inevitável (?) que você paga pela flexibilidade e pelo poder expressivo => O Scala não é muito caro. Se esta é a escolha certa para qualquer situação em particular, é obviamente outra questão.
Ashkan Kh. Nazary
Olá, quando você diz que List{1, 2, 3}.reduceLeft(_ + _)é inválido, você quer dizer que ele tem sintaxe errada? Mas acho que esse código pode compilar. Eu coloquei meu código aqui
Calvin
Você usou List(1, 2, 3)em todos os exemplos, em vez de List{1, 2, 3}. Infelizmente, na versão atual do Scala (2.13), isso falha com uma mensagem de erro diferente (vírgula inesperada). Você precisaria voltar para 2,7 ou 2,8 para obter o erro original, provavelmente.
Daniel C. Sobral
56

Existem algumas regras e inferências diferentes acontecendo aqui: em primeiro lugar, Scala infere as chaves quando um parâmetro é uma função, por exemplo, nas list.map(_ * 2)chaves são inferidas, é apenas uma forma mais curta de list.map({_ * 2}). Em segundo lugar, Scala permite que você pule os parênteses na última lista de parâmetros, se essa lista de parâmetros tiver um parâmetro e for uma função, então list.foldLeft(0)(_ + _)poderá ser escrita como list.foldLeft(0) { _ + _ }(ou list.foldLeft(0)({_ + _})se você quiser ser mais explícito).

No entanto, se você adicionar case, obtém, como outros já mencionaram, uma função parcial em vez de uma função, e Scala não deduzirá o suporte para funções parciais; portanto list.map(case x => x * 2), não funcionará, mas ambos list.map({case x => 2 * 2})e o list.map { case x => x * 2 }farão.

Theo
fonte
4
Não apenas da última lista de parâmetros. Por exemplo, list.foldLeft{0}{_+_}funciona.
Daniel C. Sobral
1
Ah, eu tinha certeza que tinha lido que era apenas a última lista de parâmetros, mas claramente estava errado! Bom saber.
Theo
23

Há um esforço da comunidade para padronizar o uso de chaves e parênteses, consulte o Scala Style Guide (página 21): http://www.codecommit.com/scala-style-guide.pdf

A sintaxe recomendada para chamadas de métodos de ordem superior é sempre usar chaves e pular o ponto:

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

Para chamadas metodológicas "normais", você deve usar o ponto e os parênteses.

val result = myInstance.foo(5, "Hello")
olle kullberg
fonte
18
Na verdade, a convenção é usar chaves redondas, esse link não é oficial. Isso ocorre porque na programação funcional todas as funções são apenas cidadãos de primeira ordem e, portanto, NÃO devem ser tratadas de maneira diferente. Em segundo lugar, Martin Odersky diz que você deve tentar usar apenas o infixo para métodos do tipo operador (por exemplo +, --), NÃO para métodos regulares takeWhile. O ponto inteiro da notação infix é permitir DSLs e operadores personalizados; portanto, deve-se usá-lo nesse contexto nem sempre.
samthebest
17

Eu não acho que exista algo particular ou complexo em aparelhos fixos em Scala. Para dominar o uso aparentemente complexo deles no Scala, lembre-se de algumas coisas simples:

  1. chaves entre dentes formam um bloco de código, que é avaliado até a última linha de código (quase todos os idiomas fazem isso)
  2. uma função, se desejar, pode ser gerada com o bloco de código (segue a regra 1)
  3. chaves entre chaves podem ser omitidas para o código de uma linha, exceto para uma cláusula case (opção Scala)
  4. parênteses podem ser omitidos na chamada de função com bloco de código como parâmetro (opção Scala)

Vamos explicar alguns exemplos de acordo com as três regras acima:

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
fonte
1. não é realmente verdade em todos os idiomas. 4. Na verdade, não é verdade em Scala. Por exemplo: def f (x: Int) = fx
seja,
@aij, obrigado pelo comentário. Para 1, eu estava sugerindo a familiaridade que Scala fornece para o {}comportamento. Atualizei o texto para obter precisão. E para 4, é um pouco complicado devido à interação entre ()e {}, como def f(x: Int): Int = f {x}funciona, e é por isso que eu tive o quinto. :)
lcn 24/09/15
1
Costumo pensar em () e {} como principalmente intercambiáveis ​​no Scala, exceto que ele analisa o conteúdo de maneira diferente. Normalmente eu não escrevo f ({x}), então f {x} não parece omitir parênteses, mas substituí-los por curlies. Outros idiomas permitem omitir parênteses, por exemplo, fun f(x) = f xé válido no SML.
aij
@aij, tratar f {x}o f({x})que parece ser uma explicação melhor para mim, pois pensar ()e {}intercambiar é menos intuitivo. A propósito, a f({x})interpretação é um pouco apoiada pelas especificações do Scala (seção 6.6):ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
lcn
13

Eu acho que vale a pena explicar o uso deles em chamadas de função e por que várias coisas acontecem. Como alguém já disse que chavetas definem um bloco de código, que também é uma expressão, pode ser colocado onde a expressão é esperada e será avaliada. Quando avaliadas, suas instruções são executadas e o valor da última instrução é o resultado de uma avaliação de bloco inteiro (um pouco como no Ruby).

Tendo isso, podemos fazer coisas como:

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

O último exemplo é apenas uma chamada de função com três parâmetros, dos quais cada um é avaliado primeiro.

Agora, para ver como funciona com chamadas de função, vamos definir funções simples que usam outra função como parâmetro.

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

Para chamá-lo, precisamos passar a função que usa um parâmetro do tipo Int, para que possamos usar a função literal e passá-la para foo:

foo( x => println(x) )

Agora, como dito antes, podemos usar o bloco de código no lugar de uma expressão, então vamos usá-lo

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

O que acontece aqui é que o código dentro de {} é avaliado e o valor da função é retornado como um valor da avaliação do bloco; esse valor é passado para foo. É semanticamente o mesmo da chamada anterior.

Mas podemos adicionar algo mais:

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

Agora, nosso bloco de código contém duas instruções e, como é avaliado antes da execução do foo, o que acontece é que primeiro "Hey" é impresso, nossa função é passada para foo, "Digitando foo" é impresso e, por fim, "4" é impresso. .

Parece um pouco feio e Scala nos permite pular os parênteses nesse caso, para que possamos escrever:

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

ou

foo { x => println(x) }

Isso parece muito melhor e é equivalente aos primeiros. Aqui ainda o bloco de código é avaliado primeiro e o resultado da avaliação (que é x => println (x)) é passado como argumento para foo.

Lukasz Korzybski
fonte
1
Sou só eu. mas eu realmente prefiro a natureza explícita de foo({ x => println(x) }). Talvez eu esteja muito preso em meus caminhos ...
dade 12/05
7

Como você está usando case, você está definindo uma função parcial e funções parciais requerem chaves.

fjdumont
fonte
1
Eu pedi uma resposta em geral, não apenas uma resposta para este exemplo.
Marc-François
5

Maior verificação de compilação com parens

Os autores do Spray recomendam que as parênteses arredondadas aumentem a verificação de compilação. Isso é especialmente importante para DSLs como o Spray. Ao usar parens, você está dizendo ao compilador que ele deve receber apenas uma única linha; portanto, se você acidentalmente fornecer duas ou mais, ele irá reclamar. Agora, esse não é o caso das chaves, se, por exemplo, você esquecer um operador em algum lugar que seu código será compilado, obter resultados inesperados e potencialmente um bug muito difícil de encontrar. Abaixo é inventado (já que as expressões são puras e, pelo menos, emitem um aviso), mas defende

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

A primeira compila, a segunda dá error: ')' expected but integer literal found.ao autor que queria escrever 1 + 2 + 3.

Pode-se argumentar que é semelhante para métodos multiparâmetros com argumentos padrão; é impossível esquecer acidentalmente uma vírgula para separar parâmetros ao usar parênteses.

Verbosidade

Uma observação importante, muitas vezes esquecida, sobre a verbosidade. O uso de chavetas inevitavelmente leva a códigos detalhados, pois o guia de estilo da scala indica claramente que as chaves de fechamento devem estar em sua própria linha: http://docs.scala-lang.org/style/declarations.html "... a chave de fechamento está em sua própria linha imediatamente após a última linha da função ". Muitos reformadores automáticos, como no Intellij, executam automaticamente essa reformatação para você. Portanto, tente continuar usando parênteses redondas quando puder. Por exemplo, List(1, 2, 3).reduceLeft{_ + _}torna-se:

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

Com chaves, você tem ponto e vírgula induzido para você e parênteses não. Considere a takeWhilefunção, uma vez que espera função parcial, somente {case xxx => ??? }é uma definição válida em vez de parênteses em torno da expressão do caso.

keitine
fonte