Quais são as regras precisas para quando você pode omitir parênteses, pontos, colchetes, = (funções), etc.?


106

Quais são as regras precisas para quando você pode omitir (omitir) parênteses, pontos, colchetes, = (funções), etc.?

Por exemplo,

(service.findAllPresentations.get.first.votes.size) must be equalTo(2).
  • service é meu objeto
  • def findAllPresentations: Option[List[Presentation]]
  • votes retorna List[Vote]
  • deve e ser são funções de especificações

Por que não posso ir:

(service findAllPresentations get first votes size) must be equalTo(2)

?

O erro do compilador é:

"RestServicesSpecTest.this.service.findAllPresentations do tipo Option [List [com.sharca.Presentation]] não leva parâmetros"

Por que ele acha que estou tentando passar um parâmetro? Por que devo usar pontos para cada chamada de método?

Por que deve (service.findAllPresentations get first votes size)ser igual a (2) resultar em:

"não encontrado: valor primeiro"

Ainda assim, "deve ser igual a 2" de (service.findAllPresentations.get.first.votes.size)deve ser igual a 2, ou seja, o encadeamento de métodos funciona bem? - parâmetro de cadeia de cadeia de objeto.

Eu olhei o livro e o site do Scala e realmente não consigo encontrar uma explicação abrangente.

É mesmo, como Rob H explica na questão Stack Overflow Quais personagens posso omitir em Scala? , que é o único caso de uso válido para omitir o '.' é para operações de estilo "operando operador operando" e não para encadeamento de método?

Respostas:


87

Você parece ter tropeçado na resposta. De qualquer forma, vou tentar deixar isso claro.

Você pode omitir o ponto ao usar as notações de prefixo, infixo e pós-fixo - a chamada notação de operador . Ao usar a notação de operador, e somente então, você pode omitir o parêntese se houver menos de dois parâmetros passados ​​para o método.

Agora, a notação de operador é uma notação para chamada de método , o que significa que não pode ser usada na ausência do objeto que está sendo chamado.

Vou detalhar brevemente as notações.

Prefixo:

Apenas ~, !, +e -pode ser usado em prefixo notação. Esta é a notação que você está usando quando escreve !flagou val liability = -debt.

Infixo:

Essa é a notação onde o método aparece entre um objeto e seus parâmetros. Todos os operadores aritméticos se encaixam aqui.

Postfix (também sufixo):

Essa notação é usada quando o método segue um objeto e não recebe parâmetros . Por exemplo, você pode escrever list tail, e isso é notação pós-fixada.

Você pode encadear chamadas de notação infixada sem problemas, desde que nenhum método seja ativado. Por exemplo, gosto de usar o seguinte estilo:

(list
 filter (...)
 map (...)
 mkString ", "
)

Isso é a mesma coisa que:

list filter (...) map (...) mkString ", "

Agora, por que estou usando parênteses aqui, se o filtro e o mapa têm um único parâmetro? É porque estou passando funções anônimas para eles. Não posso misturar definições de funções anônimas com estilo infixo porque preciso de um limite para o fim de minha função anônima. Além disso, a definição do parâmetro da função anônima pode ser interpretada como o último parâmetro do método infix.

Você pode usar infix com vários parâmetros:

string substring (start, end) map (_ toInt) mkString ("<", ", ", ">")

Funções curried são difíceis de usar com notação de infixo. As funções de dobramento são um exemplo claro disso:

(0 /: list) ((cnt, string) => cnt + string.size)
(list foldLeft 0) ((cnt, string) => cnt + string.size)

Você precisa usar parênteses fora da chamada do infixo. Não tenho certeza das regras exatas em jogo aqui.

Agora, vamos falar sobre o postfix. Postfix pode ser difícil de usar, porque nunca pode ser usado em qualquer lugar, exceto no final de uma expressão . Por exemplo, você não pode fazer o seguinte:

 list tail map (...)

Porque tail não aparece no final da expressão. Você também não pode fazer isso:

 list tail length

Você pode usar notação infixa usando parênteses para marcar o fim das expressões:

 (list tail) map (...)
 (list tail) length

Observe que a notação pós-fixada é desencorajada porque pode não ser segura .

Espero que isso tenha esclarecido todas as dúvidas. Se não, deixe um comentário e verei o que posso fazer para melhorar.


ahh, então você está dizendo que em minha declaração: (((((realService findAllPresentations) get) first) votes) size) must be equalTo 2 - get, first, votes e size são todos operadores pós-fixados, porque não aceitam parâmetros ? então eu me pergunto o que deve, ser e igual a são ...
Antony Stubbs

Não estou dizendo nada disso, embora tenha quase certeza de que deve ser o caso. :-) "be" é provavelmente um objeto auxiliar, para tornar a sintaxe mais bonita. Ou, mais especificamente, para permitir o uso da notação infixa com "deve".
Daniel C. Sobral

Bem, obter é Option.get, primeiro é list.first, votes é uma propriedade de classe de caso e size é list.size. O que você pensa agora?
Antony Stubbs

Ah sim - tudo isso é reforçado pelo fato de que "(realService findPresentation 1) .get.id deve ser igual a 1" funciona - já que Service # findPresentations (id: Int) é um operador infixo ao que parece. Legal - acho que já entendi. :)
Antony Stubbs

42

Definições de classe:

valou varpode ser omitido dos parâmetros de classe, o que tornará o parâmetro privado.

Adicionar var ou val fará com que seja público (isto é, acessadores e modificadores de método são gerados).

{} pode ser omitido se a classe não tiver corpo, ou seja,

class EmptyClass

Instanciação de classe:

Os parâmetros genéricos podem ser omitidos se puderem ser inferidos pelo compilador. No entanto, observe que se seus tipos não corresponderem, o parâmetro de tipo sempre será inferido para que corresponda. Portanto, sem especificar o tipo, você pode não obter o que espera - isto é, dado

class D[T](val x:T, val y:T);

Isso lhe dará um erro de tipo (Int encontrado, String esperada)

var zz = new D[String]("Hi1", 1) // type error

Considerando que isso funciona bem:

var z = new D("Hi1", 1)
== D{def x: Any; def y: Any}

Porque o parâmetro de tipo, T, é inferido como o supertipo menos comum dos dois - Qualquer.


Definições de função:

= pode ser descartado se a função retornar Unit (nada).

{}pois o corpo da função pode ser eliminado se a função for uma única instrução, mas apenas se a instrução retornar um valor (você precisa do =sinal), ou seja,

def returnAString = "Hi!"

mas isso não funciona:

def returnAString "Hi!" // Compile error - '=' expected but string literal found."

O tipo de retorno da função pode ser omitido se puder ser inferido (um método recursivo deve ter seu tipo de retorno especificado).

() pode ser descartado se a função não receber nenhum argumento, ou seja,

def endOfString {
  return "myDog".substring(2,1)
}

que por convenção é reservado para métodos que não têm efeitos colaterais - mais sobre isso mais tarde.

()não é descartado per se ao definir um parâmetro de passagem por nome , mas é na verdade uma notação semanticamente diferente, isto é,

def myOp(passByNameString: => String)

Diz que myOp usa um parâmetro de passagem por nome, que resulta em uma String (ou seja, pode ser um bloco de código que retorna uma string) em vez de parâmetros de função,

def myOp(functionParam: () => String)

que diz que myOppega uma função que tem zero parâmetros e retorna uma String.

(Lembre-se, os parâmetros passados ​​por nome são compilados em funções; isso apenas torna a sintaxe mais agradável.)

() pode ser descartado na definição do parâmetro da função se a função tiver apenas um argumento, por exemplo:

def myOp2(passByNameString:(Int) => String) { .. } // - You can drop the ()
def myOp2(passByNameString:Int => String) { .. }

Mas se for necessário mais de um argumento, você deve incluir o ():

def myOp2(passByNameString:(Int, String) => String) { .. }

Afirmações:

.pode ser descartado para usar a notação de operador, que pode ser usada para operadores infixos (operadores de métodos que recebem argumentos). Veja a resposta de Daniel para mais informações.

  • . também pode ser descartado para o final da lista de funções postfix

  • () pode ser descartado para operadores postfix list.tail

  • () não pode ser usado com métodos definidos como:

    def aMethod = "hi!" // Missing () on method definition
    aMethod // Works
    aMethod() // Compile error when calling method

Porque essa notação é reservada por convenção para métodos que não têm efeitos colaterais, como List # tail (ou seja, a invocação de uma função sem efeitos colaterais significa que a função não tem efeito observável, exceto por seu valor de retorno).

  • () pode ser descartado para notação de operador ao passar um único argumento

  • () pode ser necessário usar operadores postfix que não estão no final de uma instrução

  • () pode ser necessário para designar instruções aninhadas, fins de funções anônimas ou para operadores que usam mais de um parâmetro

Ao chamar uma função que assume uma função, você não pode omitir o () da definição de função interna, por exemplo:

def myOp3(paramFunc0:() => String) {
    println(paramFunc0)
}
myOp3(() => "myop3") // Works
myOp3(=> "myop3") // Doesn't work

Ao chamar uma função que usa um parâmetro por nome, você não pode especificar o argumento como uma função anônima sem parâmetro. Por exemplo, dado:

def myOp2(passByNameString:Int => String) {
  println(passByNameString)
}

Você deve chamá-lo como:

myOp("myop3")

ou

myOp({
  val source = sourceProvider.source
  val p = myObject.findNameFromSource(source)
  p
})

mas não:

myOp(() => "myop3") // Doesn't work

IMO, o uso excessivo de tipos de retorno de descarte pode ser prejudicial para o código a ser reutilizado. Basta olhar a especificação para um bom exemplo de legibilidade reduzida devido à falta de informações explícitas no código. O número de níveis de indireção para realmente descobrir qual é o tipo de uma variável pode ser louco. Esperançosamente, ferramentas melhores podem evitar esse problema e manter nosso código conciso.

(OK, na busca para compilar uma resposta mais completa e concisa (se eu perdi alguma coisa ou entendi algo errado / impreciso, por favor, comente), adicionei no início da resposta. Observe que esta não é uma linguagem especificação, portanto, não estou tentando torná-la exatamente academicamente correta - apenas mais como um cartão de referência.)


10
Estou chorando. O que é isso.
Profpatsch

12

Uma coleção de citações que fornecem informações sobre as várias condições ...

Pessoalmente, pensei que haveria mais na especificação. Tenho certeza que deve haver, só não estou procurando as palavras certas ...

Existem algumas fontes, entretanto, e eu as reuni, mas nada realmente completo / abrangente / compreensível / que explique os problemas acima para mim ...:

"Se o corpo de um método tiver mais de uma expressão, você deve colocá-lo entre colchetes {…}. Você pode omitir as chaves se o corpo do método tiver apenas uma expressão."

Do capítulo 2, "Digite menos, faça mais", do Scala de programação :

"O corpo do método superior vem depois do sinal de igual '='. Por que um sinal de igual? Por que não apenas chaves {...}, como em Java? Porque ponto-e-vírgulas, tipos de retorno de função, listas de argumentos de método e até mesmo as chaves às vezes são omitidos, o uso de um sinal de igual evita várias ambigüidades de análise possíveis. O uso de um sinal de igual também nos lembra que mesmo funções são valores em Scala, o que é consistente com o suporte de programação funcional do Scala, descrito com mais detalhes no Capítulo 8, Programação Funcional em Scala. "

Do capítulo 1, "Zero a sessenta: Apresentando Scala", de Programação Scala :

"Uma função sem parâmetros pode ser declarada sem parênteses, caso em que deve ser chamada sem parênteses. Isso fornece suporte para o Princípio de Acesso Uniforme, de modo que o chamador não sabe se o símbolo é uma variável ou uma função sem parâmetros.

O corpo da função é precedido por "=" se retornar um valor (ou seja, o tipo de retorno é algo diferente de Unidade), mas o tipo de retorno e o "=" podem ser omitidos quando o tipo é Unidade (ou seja, parece um procedimento em oposição a uma função).

Não são necessários colchetes ao redor do corpo (se o corpo for uma única expressão); mais precisamente, o corpo de uma função é apenas uma expressão e qualquer expressão com várias partes deve ser colocada entre colchetes (uma expressão com uma parte pode ser opcionalmente incluída entre colchetes). "

"Funções com zero ou um argumento podem ser chamadas sem o ponto e parênteses. Mas qualquer expressão pode ter parênteses ao redor dela, então você pode omitir o ponto e ainda usar parênteses.

E como você pode usar colchetes em qualquer lugar, pode usar parênteses, pode omitir o ponto e colocá-los entre colchetes, que podem conter várias instruções.

Funções sem argumentos podem ser chamadas sem os parênteses. Por exemplo, a função length () em String pode ser chamada como "abc" .length em vez de "abc" .length (). Se a função for uma função Scala definida sem parênteses, a função deve ser chamada sem parênteses.

Por convenção, funções sem argumentos que tenham efeitos colaterais, como println, são chamadas entre parênteses; aqueles sem efeitos colaterais são chamados sem parênteses. "

Da postagem do blog Scala Syntax Primer :

"Uma definição de procedimento é uma definição de função em que o tipo de resultado e o sinal de igual são omitidos; sua expressão de definição deve ser um bloco. Por exemplo, def f (ps) {estatísticas} é equivalente a def f (ps): Unidade = {estatísticas }

Exemplo 4.6.3 Aqui está uma declaração e uma definição de um procedimento denominado write:

trait Writer {
    def write(str: String)
}
object Terminal extends Writer {
    def write(str: String) { System.out.println(str) }
}

O código acima é implicitamente completado com o seguinte código:

trait Writer {
    def write(str: String): Unit
}
object Terminal extends Writer {
    def write(str: String): Unit = { System.out.println(str) }
}"

Da especificação do idioma:

"Com métodos que usam apenas um único parâmetro, Scala permite que o desenvolvedor substitua o. Por um espaço e omita os parênteses, permitindo a sintaxe do operador mostrada em nosso exemplo de operador de inserção. Essa sintaxe é usada em outros lugares na API Scala, como como construir instâncias Range:

val firstTen:Range = 0 to 9

Aqui, novamente, to (Int) é um método simples declarado dentro de uma classe (na verdade, há mais algumas conversões de tipo implícitas aqui, mas você entendeu). "

Do Scala for Java Refugees Part 6: Getting Over Java :

"Agora, quando você tenta" m 0 ", Scala descarta-o como um operador unário, com base em não ser válido (~,!, - e +). Ele descobre que" m "é um objeto válido - é uma função, não um método, e todas as funções são objetos.

Como "0" não é um identificador Scala válido, ele não pode ser um operador infixo ou postfix. Portanto, Scala reclama que esperava ";" - que separaria duas expressões (quase) válidas: "m" e "0". Se você o inserisse, reclamaria que m requer um argumento ou, na sua falta, um "_" para transformá-lo em uma função parcialmente aplicada. "

"Eu acredito que o estilo da sintaxe do operador funciona apenas quando você tem um objeto explícito no lado esquerdo. A sintaxe se destina a permitir que você expresse operações de estilo de" operando operando operando "de uma maneira natural."

Quais personagens posso omitir em Scala?

Mas o que também me confunde é esta citação:

"É necessário que haja um objeto para receber uma chamada de método. Por exemplo, você não pode fazer“ println “Hello World!” "Porque o println precisa de um destinatário de objeto. Você pode fazer “Console println“ Hello World! ”" Que satisfaz a necessidade. "

Porque tanto quanto eu posso ver, não é um objeto para receber a chamada ...


1
Ok, então tentei ler a fonte de especificações para obter algumas pistas e uau. Esse é um ótimo exemplo dos problemas com código mágico - muitos mixins, inferência de tipo e conversões implícitas e parâmetros implícitos. É tão difícil entender de fora para dentro! Para grandes bibliotecas como essa, ferramentas melhores podem fazer maravilhas ... um dia ...
Antony Stubbs

3

Acho mais fácil seguir esta regra: nas expressões, os espaços se alternam entre métodos e parâmetros. Em seu exemplo, (service.findAllPresentations.get.first.votes.size) must be equalTo(2)analisa como (service.findAllPresentations.get.first.votes.size).must(be)(equalTo(2)). Observe que os parênteses em torno do 2 têm uma associatividade maior do que os espaços. Os pontos também têm maior associatividade, portanto (service.findAllPresentations.get.first.votes.size) must be.equalTo(2), analisaria como (service.findAllPresentations.get.first.votes.size).must(be.equalTo(2)).

service findAllPresentations get first votes size must be equalTo 2analisa como service.findAllPresentations(get).first(votes).size(must).be(equalTo).2.


2

Na verdade, em uma segunda leitura, talvez esta seja a chave:

Com métodos que usam apenas um único parâmetro, Scala permite que o desenvolvedor substitua o. com um espaço e omitir os parênteses

Conforme mencionado na postagem do blog: http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-6 .

Então, talvez este seja realmente um "açúcar de sintaxe" muito estrito que funciona quando você está efetivamente chamando um método, em um objeto, que leva um parâmetro . por exemplo

1 + 2
1.+(2)

E nada mais.

Isso explicaria meus exemplos na pergunta.

Mas, como eu disse, se alguém pudesse apontar exatamente onde na especificação do idioma isso está especificado, ficaria muito grato.

Ok, um bom sujeito (paulp_ de #scala) apontou onde na especificação do idioma esta informação é:

6.12.3: A precedência e a associatividade dos operadores determinam o agrupamento das partes de uma expressão como segue.

  • Se houver várias operações infixas em uma expressão, os operadores com precedência mais alta se ligam mais intimamente do que os operadores com precedência mais baixa.
  • Se houver operações infixas consecutivas e0 op1 e1 op2. . .opn pt com operadores op1,. . . , opn com a mesma precedência, todos esses operadores devem ter a mesma associatividade. Se todos os operadores forem associativos à esquerda, a sequência é interpretada como (... (E0 op1 e1) op2...) Opn en. Caso contrário, se todos os operadores forem associativos à direita, a sequência será interpretada como e0 op1 (e1 op2 (.. .Opn en)...).
  • Os operadores Postfix sempre têm precedência inferior aos operadores infixados. Por exemplo, e1 op1 e2 op2 é sempre equivalente a (e1 op1 e2) op2.

O operando à direita de um operador associativo à esquerda pode consistir em vários argumentos entre parênteses, por exemplo, e op (e1,..., En). Essa expressão é então interpretada como e.op (e1,..., En).

Uma operação binária associativa à esquerda e1 op e2 é interpretada como e1.op (e2). Se op é associativo à direita, a mesma operação é interpretada como {val x = e1; e2.op (x)}, onde x é um nome novo.

Hmm - para mim não combina com o que estou vendo ou simplesmente não entendo;)


hmm, para aumentar ainda mais a confusão, isso também é válido: (((((realService findAllPresentations) get) first) votes) size) must be equalTo 2, mas não se eu remover algum desses pares de parênteses ...
Antony Stubbs

2

Não existe nenhum. Você provavelmente receberá conselhos sobre se a função tem ou não efeitos colaterais. Isso é falso. A correção é não usar efeitos colaterais na extensão razoável permitida pelo Scala. Na medida em que não pode, todas as apostas estão canceladas. Todas as apostas. O uso de parênteses é um elemento do conjunto "tudo" e é supérfluo. Não fornece nenhum valor uma vez que todas as apostas estejam encerradas.

Este conselho é essencialmente uma tentativa de um sistema de efeitos que falha (não deve ser confundido com: é menos útil do que outros sistemas de efeitos).

Tente não ter efeito colateral. Depois disso, aceite que todas as apostas estão canceladas. Esconder-se atrás de uma notação sintática de fato para um sistema de efeitos só pode causar danos.


Bem, mas esse é o problema quando você está trabalhando com uma linguagem híbrida OO / Funcional, certo? Em qualquer exemplo prático, você desejará funções de efeito colateral ... Você pode nos indicar algumas informações sobre "sistemas de efeitos"? Acho que a citação mais precisa é "Uma função sem parâmetros pode ser declarada sem parênteses, caso em que deve ser chamada sem parênteses. Isso fornece suporte para o Princípio de Acesso Uniforme, de modo que o chamador não sabe se o símbolo é uma variável ou função sem parâmetros. ".
Antony Stubbs
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.