Design da sintaxe - Por que usar parênteses quando nenhum argumento é passado?

66

Em muitos idiomas, a sintaxe function_name(arg1, arg2, ...)é usada para chamar uma função. Quando queremos chamar a função sem argumentos, devemos fazê-lo function_name().

Acho estranho que um compilador ou intérprete de script exigiria ()detectá-lo com êxito como uma chamada de função. Se uma variável é conhecida como chamadora, por que não function_name;seria suficiente?

Por outro lado, em alguns idiomas, podemos fazer: function_name 'test';ou mesmo function_name 'first' 'second';chamar uma função ou comando.

Acho que os parênteses teriam sido melhores se fossem necessários apenas para declarar a ordem de prioridade e, em outros lugares, eram opcionais. Por exemplo, fazer if expression == true function_name;deve ser tão válido quanto if (expression == true) function_name();.

A coisa mais irritante na minha opinião é fazer 'SOME_STRING'.toLowerCase()quando claramente nenhum argumento é necessário para a função prototype. Por que os designers decidiram contra o mais simples 'SOME_STRING'.lower?

Isenção de responsabilidade: não me interpretem mal, eu amo a sintaxe do tipo C! ;) Só estou perguntando se poderia ser melhor. A exigência ()tem vantagens de desempenho ou facilita a compreensão do código? Estou realmente curioso para saber exatamente qual é o motivo.

David Refoua
fonte
103
O que você faria se quisesse passar uma função como argumento para outra função?
Vincent Savard
30
Enquanto o código for necessário para ser lido pelos seres humanos, a legibilidade será o rei.
Tulains Córdova 05/10
75
Essa é a questão da legibilidade, você pergunta (), mas o que se destaca em seu post é a if (expression == true)afirmação. Você se preocupa com supérfluos ()'s, no entanto, em seguida, usar um supérfluo == true:)
David Arno
15
O Visual Basic permitiu isso
edc65 5/16
14
Era obrigatório em Pascal, você usava parens apenas para funções e procedimentos que recebiam argumentos.
RemcoGerlich 5/10

Respostas:

251

Para idiomas que usam funções de primeira classe , é bastante comum que a sintaxe de referência a uma função seja:

a = object.functionName

enquanto o ato de chamar essa função é:

b = object.functionName()

ano exemplo acima, seria referência à função acima (e você poderia chamá-la fazendo isso a()), enquanto bconteria o valor de retorno da função.

Enquanto alguns idiomas podem fazer chamadas de função sem parênteses, pode ficar confuso se eles estão chamando a função ou simplesmente se referindo à função.

Nathan Merrill
fonte
9
Você pode querer mencionar que esta diferença só é interessante na presença de efeitos colaterais - para uma função pura não importa
Bergi
73
@ Bergi: É muito importante para funções puras! Se eu tiver uma função make_listque agrupe seus argumentos em uma lista, há uma grande diferença entre f(make_list)passar uma função para fou passar uma lista vazia.
user2357112
12
@ Bergi: Mesmo assim, eu ainda gostaria de poder passar SATInstance.solveou alguma outra função pura incrivelmente cara para outra função sem imediatamente tentar executá-la. Isso também torna as coisas realmente estranhas, se someFunctionalgo é diferente, dependendo se someFunctioné declarado puro. Por exemplo, se eu tiver (pseudocódigo) func f() {return g}, func g() {doSomethingImpure}e func apply(x) {x()}, em seguida, apply(f)chama f. Se eu declaro que isso fé puro, repentinamente apply(f)passa gpara apply, applychama ge faz algo impuro.
user2357112
6
@ user2357112 A estratégia de avaliação é independente da pureza. Usando a sintaxe das chamadas como uma anotação quando avaliar o que é possível (mas provavelmente estranho), no entanto, isso não altera o resultado ou a sua correção. No que diz respeito ao seu exemplo apply(f), se gé impuro (e applytambém?), Tudo é impuro e quebra, é claro.
Bergi 5/10
14
Exemplos disso são Python e ECMAScript, os quais realmente não têm um conceito central de "métodos"; em vez disso, você possui uma propriedade nomeada que retorna um objeto de função de primeira classe anônima e, em seguida, chama essa função de primeira classe anônima. Especificamente, no Python e no ECMAScript, dizer foo.bar()não é uma operação "chamar método barde objeto foo", mas duas operações "desreferenciar o campo bardo objeto foo e depois chamar o objeto retornado desse campo". Portanto, .é o operador seletor de campo e ()é o operador de chamada e eles são independentes.
Jörg W Mittag
63

De fato, Scala permite isso, embora exista uma convenção a seguir: se o método tiver efeitos colaterais, parênteses devem ser usados ​​de qualquer maneira.

Como escritor de compilador, consideraria conveniente a presença garantida de parênteses; Eu sempre saberia que é uma chamada de método e não precisaria criar uma bifurcação para o caso estranho.

Como programador e leitor de código, a presença de parênteses não deixa dúvidas de que é uma chamada de método, mesmo que nenhum parâmetro seja passado.

A passagem de parâmetros não é a única característica definidora de uma chamada de método. Por que eu trataria um método sem parâmetro diferente de um método que possui parâmetros?

Robert Harvey
fonte
Obrigado, e você deu razões válidas para isso ser importante. Você também pode dar alguns exemplos de por que os designers de linguagem devem usar funções em protótipos, por favor?
precisa saber é o seguinte
a presença de parênteses não deixa dúvidas de que é uma chamada de método que eu concordo completamente. Prefiro minhas linguagens de programação a mais explícitas do que implícitas.
Corey Ogburn
20
O Scala é realmente um pouco mais sutil que isso: enquanto outros idiomas permitem apenas uma lista de parâmetros com zero ou mais parâmetros, o Scala permite zero ou mais listas de parâmetros com zero ou mais parâmetros. Então, def foo()é um método com uma lista de parâmetros vazia e def fooé um método sem lista de parâmetros e os dois são coisas diferentes ! Em geral, um método sem lista de parâmetros precisa ser chamado sem lista de argumentos e um método com uma lista de parâmetros vazia precisa ser chamado com uma lista de argumentos vazia. Chamar um método sem uma lista de parâmetros com um argumento vazio é realmente…
Jörg W Mittag 5/16
19
... interpretado como chamando o applymétodo do objeto retornado pela chamada de método com uma lista de argumentos vazia, enquanto chamar um método com uma lista de parâmetros vazia sem uma lista de argumentos pode, dependendo do contexto, ser interpretado de várias formas como chamar o método com uma lista de argumentos vazia, expansão de η para um método parcialmente aplicado (ou seja, "referência de método") ou talvez não seja compilado. Além disso, Scala segue o Princípio de acesso uniforme, não distinguindo (sintaticamente) entre chamar um método sem uma lista de argumentos e referenciar um campo.
Jörg W Mittag 5/10
3
Mesmo quando eu aprecio Ruby pela ausência de "cerimônia" necessária - parens, chaves, tipo explícito - posso dizer que parênteses de método lê mais rapidamente ; mas uma vez familiar, o Ruby idiomático é bastante legível e definitivamente mais rápido de escrever.
Radarbob # 6/16
19

Este é realmente um acaso bastante sutil de escolhas de sintaxe. Vou falar com linguagens funcionais, que são baseadas no cálculo lambda digitado.

Nas referidas linguagens, toda função tem exatamente um argumento . O que geralmente consideramos "múltiplos argumentos" é na verdade um único parâmetro do tipo de produto. Por exemplo, a função que compara dois números inteiros:

leq : int * int -> bool
leq (a, b) = a <= b

leva um único par de números inteiros. Os parênteses não indicam os parâmetros da função; eles são usados ​​para combinar o padrão com o argumento. Para convencê-lo de que esse é realmente um argumento, podemos usar as funções projetivas em vez da correspondência de padrões para desconstruir o par:

leq some_pair = some_pair.1 <= some_pair.2

Assim, os parênteses são realmente uma conveniência que permite padronizar a correspondência e salvar algumas digitações. Eles não são necessários.

E uma função que ostensivamente não tem argumentos? Essa função realmente tem domínio Unit. O único membro de Unitgeralmente é escrito como (), é por isso que os parênteses aparecem.

say_hi : Unit -> string
say_hi a = "Hi buddy!"

Para chamar essa função, teríamos que aplicá-la a um valor do tipo Unit, que deve ser (), para que acabemos escrevendo say_hi ().

Portanto, não existe realmente uma lista de argumentos!

Gardenhead
fonte
3
E é por isso que parênteses vazios se ()assemelham a colheres, menos o cabo.
Mindwin
3
Definir uma leqfunção que usa <e não <=é bastante confusa!
KRyan #
5
Essa resposta pode ser mais fácil de entender se ela usasse a palavra "tupla", além de frases como "tipo de produto".
Kevin
A maioria dos formalismos tratará int f (int x, int y) como uma função de I ^ 2 a I. O cálculo lambda é diferente; ele não usa esse método para combinar o que em outros formalismos é alta aridade. Em vez disso, o cálculo lambda usa curry. A comparação seria x -> (y -> (x <= y)). (x -> (y -> (x <= y)) 2 avaliaria em (y-> 2 <= y) e (x -> (y -> (x <= y)) 2 3 avaliava em ( y-> 2 <= y) 3, que por sua vez é avaliado como 2 <= 3.
Taemyr
11
@PeriataBreatta Não estou familiarizado com o histórico de uso ()para representar a unidade. Eu não ficaria surpreso se pelo menos parte do motivo fosse parecido com uma "função sem parâmetros". No entanto, há outra explicação possível para a sintaxe. Na álgebra de tipos, Unité de fato a identidade dos tipos de produtos. Um produto binário é escrito como (a,b), poderíamos escrever um produto unário como (a), portanto, uma extensão lógica seria escrever o produto nulo da maneira mais simples ().
precisa saber é o seguinte
14

Em Javascript, por exemplo, usar um nome de método sem () retorna a própria função sem executá-la. Dessa forma, você pode, por exemplo, passar a função como argumento para outro método.

Em Java, optou-se por um identificador seguido por () ou (...) significa uma chamada de método, enquanto um identificador sem () se refere a uma variável de membro. Isso pode melhorar a legibilidade, pois você não tem dúvidas se está lidando com um método ou uma variável de membro. De fato, o mesmo nome pode ser usado para um método e uma variável membro, ambos estarão acessíveis com sua respectiva sintaxe.

Florian F
fonte
4
+1 Enquanto o código for necessário para ser lido pelos seres humanos, a legibilidade será o rei.
Tulains Córdova 05/10
11
@ TulainsCórdova Não tenho certeza se perl concorda com você.
Racheet 5/10
@Racheet: Perl não pode, mas Perl Best Practicesfaz
slebetman
11
O @Racheet Perl é muito legível se for escrito para ser legível. É o mesmo para praticamente todas as línguas não esotéricas. Um Perl curto e conciso é muito legível para programadores de Perl, assim como o código Scala ou Haskell é legível para pessoas com experiência nesses idiomas. Também posso falar alemão de uma maneira muito complicada, gramaticalmente completamente correta, mas ainda assim você não entenderá uma palavra, mesmo que seja proficiente em alemão. Isso também não é culpa do alemão. ;)
simbabque
em java, não "identifica" um método. Isso chama. Object::toStringidentifica um método.
Njzk2 6/10
10

A sintaxe segue a semântica, então vamos começar pela semântica:

Quais são as maneiras de usar uma função ou método?

Na verdade, existem várias maneiras:

  • uma função pode ser chamada, com ou sem argumentos
  • uma função pode ser tratada como um valor
  • uma função pode ser parcialmente aplicada (criando um fechamento usando pelo menos um argumento a menos e fechando sobre os argumentos passados)

Ter uma sintaxe semelhante para diferentes usos é a melhor maneira de criar uma linguagem ambígua ou, pelo menos, confusa (e temos o suficiente).

Nos idiomas C e C:

  • chamar uma função é feito entre parênteses para incluir os argumentos (pode não haver nenhum) como em func()
  • tratar uma função como um valor pode ser feito simplesmente usando seu nome como em &func(C criando um ponteiro de função)
  • em alguns idiomas, você possui sintaxe abreviada para aplicar parcialmente; Java permite, someVariable::someMethodpor exemplo (limitado ao receptor do método, mas ainda útil)

Observe como cada uso apresenta uma sintaxe diferente, permitindo diferenciá-los facilmente.

Matthieu M.
fonte
2
Bem, "facilmente" é um daqueles "fica claro apenas se já estiver entendido" situações. Um iniciante seria inteiramente justificado ao observar que não é de forma alguma óbvio sem explicação por que alguma pontuação tem o significado que tem.
Eric Lippert
2
@ EricLippert: De fato, facilmente não significa necessariamente intuitivo. Embora, neste caso, eu apontei que parênteses também são usados ​​para a função "invocação" em matemática. Dito isto, eu encontrei iniciantes em muitas línguas lutando mais com conceitos / semânticas do que com sintaxe em geral.
Matthieu M.
Esta resposta confunde a distinção entre "currying" e "aplicação parcial". O ::operador em Java é um operador de aplicativo parcial, não um operador de currying. Aplicação parcial é uma operação que pega uma função e um ou mais valores e retorna uma função que aceita menos valores. O curry opera apenas em funções, não em valores.
Periata Breatta
@PeriataBreatta: Bom ponto, fixo.
Matthieu M.
8

Em uma linguagem com efeitos colaterais, é muito útil para a OMI diferenciar entre (em teoria, sem efeito colateral) a leitura de uma variável

variable

e chamar uma função, que pode resultar em efeitos colaterais

launch_nukes()

OTOH, se não houver efeitos colaterais (além dos codificados no sistema de tipos), não há sentido em diferenciar entre ler uma variável e chamar uma função sem argumentos.

Daniel Jour
fonte
11
Observe que o OP apresentou o caso em que um objeto é o único argumento e é apresentado antes do nome da função (que também fornece um escopo para o nome da função). noun.verbA sintaxe pode ter um significado tão claro quanto noun.verb()e um pouco menos confuso. Da mesma forma, uma função member_retornar uma referência esquerda pode oferecer uma sintaxe amigável do que uma função setter típico: obj.member_ = new_valuevs. obj.set_member(new_value)(o _postfix é uma dica dizendo "exposto" uma reminiscência de _ prefixos para as funções "ocultas").
Paul A. Clayton
11
@ PaulA.Clayton Não vejo como isso não é coberto pela minha resposta: se noun.verbsignifica invocação de função, como você a diferencia do mero acesso de membro?
Daniel Jour
11
Em relação à leitura de variáveis ​​teoricamente livres de efeitos colaterais, só quero dizer o seguinte: sempre tenha cuidado com os amigos falsos .
Justin Time
8

Nenhuma das outras respostas tentou abordar a questão: quanta redundância deve haver no design de um idioma? Porque, mesmo que você possa projetar uma linguagem para x = sqrt ydefinir x como raiz quadrada de y, isso não significa que você deva necessariamente.

Em um idioma sem redundância, cada sequência de caracteres significa algo, o que significa que, se você cometer um único erro, não receberá uma mensagem de erro, seu programa fará a coisa errada, o que pode ser algo muito diferente do que você pretendido e muito difícil de depurar. (Como qualquer pessoa que tenha trabalhado com expressões regulares saberá.) Um pouco de redundância é uma coisa boa, pois permite que muitos de seus erros sejam detectados, e quanto mais redundância houver, maior a probabilidade de o diagnóstico ser preciso. Agora, é claro, você pode levar isso muito longe (nenhum de nós quer escrever no COBOL hoje em dia), mas existe um equilíbrio certo.

A redundância também ajuda na legibilidade, porque há mais pistas. O inglês sem redundância seria muito difícil de ler, e o mesmo se aplica às linguagens de programação.

Michael Kay
fonte
Em certo sentido, eu concordo: as linguagens de programação devem ter um mecanismo de "rede de segurança". Mas eu discordo que "um pouco de redundância" na sintaxe é uma boa maneira de fazer isso - isso é um clichê , e o que ela faz principalmente é introduzir ruído que dificulta a identificação dos erros e abre possibilidades para erros de sintaxe que desviam a atenção da linguagem. erros reais. O tipo de verbosidade que ajuda na legibilidade tem pouco a ver com sintaxe, mais com convenções de nomenclatura . E uma rede de segurança é de longe implementada com mais eficiência como um sistema de tipo forte , no qual a maioria das mudanças aleatórias tornará o programa incorreto.
usar o seguinte comando
@leftaroundabout: Eu gosto de pensar nas decisões de design de linguagem em termos de distância de Hamming. Se duas construções têm significados diferentes, geralmente é útil tê-las diferir de pelo menos duas maneiras e ter uma forma que difere de apenas uma maneira de gerar um grito do compilador. O designer de idiomas geralmente não pode ser responsável por garantir que os identificadores sejam facilmente distinguíveis, mas se, por exemplo, a atribuição exigir :=e comparação ==, sendo =apenas utilizável para inicialização em tempo de compilação, a probabilidade de erros de digitação transformando comparações em atribuições seria muito reduzida.
supercat
@leftaroundabout: Da mesma forma, se eu estivesse projetando uma linguagem, provavelmente exigiria ()a invocação de uma função de argumento zero, mas também exigiria um token para "pegar o endereço de" uma função. A ação anterior é muito mais comum, portanto, salvar um token no caso menos comum não é tão útil.
supercat
@ supercat: bem, novamente eu acho que isso está resolvendo o problema no nível errado. Designação e comparação são operações conceitualmente diferentes, assim como avaliação de função e aceitação de endereço de função, respectivamente. Portanto, eles devem ter tipos claramente distintos, então não importa mais se os operadores têm uma distância de Hamming baixa, porque qualquer erro de digitação será um erro de tipo em tempo de compilação.
leftaroundabout
@leftaroundabout: se uma atribuição estiver sendo feita para uma variável do tipo booleano ou se o tipo de retorno de uma função for um ponteiro para uma função (ou - em alguns idiomas - uma função real), substituindo uma comparação por uma atribuição, ou uma chamada de função com uma referência de função, pode gerar um programa que seja sintaticamente válido, mas que tenha um significado totalmente diferente da versão original. A verificação de tipo encontrará alguns desses erros em geral, mas tornar a sintaxe distinta parece uma abordagem melhor.
Supercat 10/10
5

Na maioria dos casos, essas são escolhas sintáticas da gramática do idioma. É útil para a gramática que as várias construções individuais sejam (relativamente) inequívocas quando tomadas em conjunto. (Se houver ambiguidades, como em algumas declarações de C ++, deve haver regras específicas para a resolução.) O compilador não tem latitude para adivinhar; é necessário seguir a especificação do idioma.


O Visual Basic, de várias formas, diferencia entre procedimentos que não retornam um valor e funções.

Os procedimentos devem ser chamados como uma declaração e não exigem parênteses, apenas argumentos separados por vírgula, se houver. As funções devem ser chamadas como parte de uma expressão e requerem os parênteses.

É uma distinção relativamente desnecessária que torna a refatoração manual entre as duas formas mais dolorosa do que precisa ser.

(Por outro lado Visual Basic usa os mesmos parênteses ()para referências matriz como para chamadas de função, de modo que as referências de matriz olhar como chamadas de função. E isso facilita refatoração manual de um array em uma chamada de função! Então, poderíamos ponderar outras línguas usam []'s para referências de matriz, mas discordo ...)


Nas linguagens C e C ++, o conteúdo das variáveis ​​é acessado automaticamente usando seu nome e, se você quiser se referir à própria variável em vez de seu conteúdo, aplica o &operador unário .

Esse tipo de mecanismo também pode ser aplicado aos nomes das funções. O nome da função bruta pode implicar uma chamada de função, enquanto um &operador unário seria usado para se referir à função (ela mesma) como dados. Pessoalmente, gosto da ideia de acessar funções sem argumento sem efeitos colaterais com a mesma sintaxe que as variáveis.

Isso é perfeitamente plausível (assim como outras opções sintáticas para isso).

Erik Eidt
fonte
2
Obviamente, enquanto a falta de colchetes do Visual Basic nas chamadas de procedimento é uma distinção desnecessária em comparação com as chamadas de função, adicionar colchetes necessários a eles seria uma distinção desnecessária entre instruções , muitas das quais em uma linguagem derivada do BASIC têm efeitos muito remanescente dos procedimentos. Também já faz um tempo desde que eu trabalhei em uma versão do MS BASIC, mas isso ainda não permite a sintaxe call <procedure name> (<arguments>)? Certeza de que era uma forma válida de utilização de procedimentos de volta quando eu fiz programação QuickBASIC em outra vida ...
Periata Breatta
11
@PeriataBreatta: O VB ainda permite a sintaxe "chamada" e, às vezes, exige isso nos casos em que a coisa a ser chamada é uma expressão [por exemplo, pode-se dizer "Chamada If (algumaCondição, Delegado1, Delegado2) (Argumentos)", mas direta invocação do resultado de um operador "If" não seria válido como uma declaração autônoma].
Supercat
@PeriataBreatta Gostei das chamadas de procedimento sem suporte do VB6 porque ele fez o código DSL parecer bom.
Mark Hurd
@supercat No LinqPad, é incrível a frequência com que preciso usar Callpara um método em que quase nunca preciso no código de produção "normal".
Mark Hurd
5

Consistência e legibilidade.

Se eu descobrir que você chamar a função Xcomo esta: X(arg1, arg2, ...), então eu espero que ele funcione da mesma forma para sem argumentos: X().

Agora, ao mesmo tempo, aprendi que você pode definir a variável como algum símbolo e usá-la assim:

a = 5
b = a
...

Agora, o que eu pensaria quando encontrar isso?

c = X

Seu palpite é tão bom quanto o meu. Em circunstâncias normais, Xé uma variável. Mas se seguíssemos o seu caminho, também poderia ser uma função! Devo lembrar se qual símbolo é mapeado para qual grupo (variáveis ​​/ funções)?

Poderíamos impor restrições artificiais, por exemplo, "Funções começam com letra maiúscula. Variáveis ​​começam com letra minúscula", mas isso não é necessário e torna as coisas mais complicadas, embora às vezes possa ajudar alguns dos objetivos do projeto.

Nota lateral 1: Outras respostas ignoram completamente mais uma coisa: a linguagem pode permitir que você use o mesmo nome para a função e a variável e faça a distinção por contexto. Veja Common Lispcomo um exemplo. Função xe variável xcoexistem perfeitamente.

Side-Nota 2: A resposta aceita nos mostra a sintaxe: object.functionname. Em primeiro lugar, não é universal para idiomas com funções de primeira classe. Em segundo lugar, como programador, eu trataria isso como uma informação adicional: functionnamepertence a um object. Seja objectum objeto, uma classe ou um espaço para nome, não importa muito, mas ele me diz que pertence a algum lugar . Isso significa que você deve adicionar sintaxe artificial object.para cada função global ou criar algumas objectpara armazenar todas as funções globais.

De qualquer forma, você perde a capacidade de ter espaços de nomes separados para funções e variáveis.

MatthewRock
fonte
Sim. A consistência é uma razão suficientemente boa, ponto final (não que seus outros pontos não sejam válidos - eles são).
Matthew Leia
4

Para adicionar às outras respostas, pegue este exemplo C:

void *func_factory(void)
{
        return 0;
}

void *(*ff)(void);

void example()
{
        ff = func_factory;
        ff = func_factory();
}

Se o operador de chamada fosse opcional, não haveria maneira de distinguir entre a atribuição de função e a chamada de função.

Isso é ainda mais problemático em idiomas que não possuem um sistema de tipos, por exemplo, JavaScript, onde a inferência de tipos não pode ser usada para descobrir o que é e o que não é uma função.

Mark K Cowan
fonte
3
JavaScript não possui um sistema de tipos. Falta declarações de tipo . Mas está totalmente ciente da diferença entre diferentes tipos de valor.
Periata Breatta
11
Periata Breatta: o ponto é que as variáveis ​​e propriedades Java podem conter tipos de qualquer valor, portanto, você nem sempre sabe se é uma função no momento da leitura do código.
reinierpost
Por exemplo, o que essa função faz? function cross(a, b) { return a + b; }
Mark K Cowan
@MarkKCowan Segue-se o seguinte: ecma-international.org/ecma-262/6.0/…
curiousdannii