Por que a maioria das linguagens de programação tem uma palavra-chave ou sintaxe especial para declarar funções? [fechadas]

39

A maioria das linguagens de programação (tanto dinamicamente quanto estaticamente) tem uma palavra-chave especial e / ou sintaxe que parece muito diferente de declarar variáveis para declarar funções. Eu vejo funções como declarar outra entidade nomeada:

Por exemplo, em Python:

x = 2
y = addOne(x)
def addOne(number): 
  return number + 1

Por que não:

x = 2
y = addOne(x)
addOne = (number) => 
  return number + 1

Da mesma forma, em uma linguagem como o Java:

int x = 2;
int y = addOne(x);

int addOne(int x) {
  return x + 1;
}

Por que não:

int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
  return x + 1;
}

Essa sintaxe parece ser uma forma mais natural de declarar algo (seja uma função ou uma variável) e uma palavra-chave a menos como def ou function em alguns idiomas. E, IMO, é mais consistente (eu olho no mesmo lugar para entender o tipo de uma variável ou função) e provavelmente torna o parser / gramática um pouco mais simples de escrever.

Eu sei que poucas linguagens usam essa idéia (CoffeeScript, Haskell), mas as linguagens mais comuns têm sintaxe especial para funções (Java, C ++, Python, JavaScript, C #, PHP, Ruby).

Mesmo no Scala, que suporta os dois modos (e tem inferência de tipos), é mais comum escrever:

def addOne(x: Int) = x + 1

Em vez de:

val addOne = (x: Int) => x + 1

IMO, pelo menos em Scala, esta é provavelmente a versão mais facilmente compreensível, mas essa expressão raramente é seguida:

val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1

Estou trabalhando na minha própria linguagem de brinquedo e estou me perguntando se há alguma armadilha se eu projetar minha linguagem de tal maneira e se houver alguma razão histórica ou técnica que esse padrão não seja amplamente seguido?

    
por pathikrit 26.09.2014 / 20:50
fonte

12 respostas

48

Eu acho que a razão é que as linguagens mais populares vêm ou foram influenciadas pela família de linguagens C em oposição às linguagens funcionais e sua raiz, o cálculo lambda.

E nessas linguagens, as funções não são apenas mais um valor:

  • Em C ++, C # e Java, você pode sobrecarregar funções: você pode ter duas funções com o mesmo nome, mas assinatura diferente.
  • Em C, C ++, C # e Java, você pode ter valores que representam funções, mas os ponteiros de função, os functores, os representantes e as interfaces funcionais são todos distintos das próprias funções. Parte da razão é que a maioria delas não são apenas funções, elas são uma função junto com algum estado (mutável).
  • As variáveis são mutáveis por padrão (você precisa usar const , readonly ou final para proibir a mutação), mas as funções não podem ser reatribuídas.
  • De uma perspectiva mais técnica, o código (que é composto de funções) e os dados são separados. Eles normalmente ocupam diferentes partes da memória e são acessados de forma diferente: o código é carregado uma vez e só executado (mas não lido ou escrito), enquanto os dados são constantemente alocados e desalocados e estão sendo gravados e lidos, mas nunca executados. / p>

    E como C deveria ser "próximo ao metal", faz sentido espelhar essa distinção na sintaxe da linguagem também.

  • A abordagem "função é apenas um valor" que forma a base da programação funcional ganhou força nas linguagens comuns há relativamente pouco tempo, como evidenciado pela introdução tardia de lambdas em C ++, C # e Java (2011, 2007, 2014).

por 26.09.2014 / 22:48
fonte
59

É importante que os seres humanos reconheçam que as funções não são apenas "outras entidades nomeadas". Às vezes, faz sentido manipulá-los como tal, mas eles ainda podem ser reconhecidos de relance.

Realmente não importa o que o computador pensa sobre a sintaxe, pois uma bolha de caracteres incompreensível é boa para uma máquina interpretar, mas isso seria quase impossível para os humanos entenderem e manterem.

É realmente a mesma razão pela qual temos loops while e for, switch e if else, etc, apesar de todos eles se resumirem a uma instrução de comparação e salto. A razão é porque está lá para o benefício de os humanos manterem e entenderem o código.

Tendo suas funções como "outra entidade nomeada", a maneira como você está propondo tornará seu código mais difícil de ver e, portanto, mais difícil de entender.

    
por 26.09.2014 / 21:04
fonte
10

Você pode estar interessado em saber que, em tempos pré-históricos, uma linguagem chamada ALGOL 68 usava uma sintaxe próxima ao que você propõe. Reconhecendo que os identificadores de função estão ligados a valores, assim como outros identificadores, você poderia nesse idioma declarar uma função (constante) usando a sintaxe

function-type name = (parameter-list) result-type : body ;

Concretamente, seu exemplo seria lido

PROC (INT)INT add one = (INT n) INT: n+1;

Reconhecendo a redundância em que o tipo inicial pode ser lido a partir do RHS da declaração, e sendo um tipo de função sempre começa com PROC , isso poderia (e normalmente seria) ser contratado para

PROC add one = (INT n) INT: n+1;

mas observe que o = ainda vem antes da lista de parâmetros. Observe também que se você quisesse uma função variável (à qual outro valor do mesmo tipo de função poderia ser atribuído posteriormente), o = deveria ser substituído por := , dando um dos

PROC (INT)INT func var := (INT n) INT: n+1;
PROC func var := (INT n) INT: n+1;

No entanto, neste caso, ambas as formas são, na verdade, abreviaturas; como o identificador func var designa uma referência a uma função gerada localmente, a forma totalmente expandida seria

REF PROC (INT)INT func var = LOC PROC (INT)INT := (INT n) INT: n+1;

Esta forma sintática em particular é fácil de se acostumar, mas claramente não tem um grande número de seguidores em outras linguagens de programação. Até mesmo linguagens de programação funcionais como Haskell preferem o estilo f n = n+1 com = seguindo a lista de parâmetros. Eu acho que a razão é principalmente psicológica; Afinal de contas, nem mesmo os matemáticos preferem, como eu, f = n n + 1 em f ( n ) = n + 1.

A propósito, a discussão acima destaca uma diferença importante entre variáveis e funções: definições de função geralmente ligam um nome a um valor de função específico, que não pode ser alterado posteriormente, enquanto definições de variável geralmente introduzem um identificador com um valor inicial , mas um que pode mudar mais tarde. (Não é uma regra absoluta; variáveis de função e constantes não-funcionais ocorrem na maioria das linguagens.) Além disso, em linguagens compiladas o valor vinculado em uma definição de função é geralmente uma constante de tempo de compilação, para que as chamadas para a função possam ser compilado usando um endereço fixo no código. Em C / C ++, isso é mesmo um requisito; o equivalente do ALGOL 68

PROC (REAL) REAL f = IF mood=sunny THEN sin ELSE cos FI;

não pode ser escrito em C ++ sem introduzir um ponteiro de função. Esse tipo de limitação específica justifica o uso de uma sintaxe diferente para definições de função. Mas eles dependem da semântica da linguagem e a justificativa não se aplica a todas as linguagens.

    
por 27.09.2014 / 12:20
fonte
8

Você mencionou Java e Scala como exemplos. No entanto, você ignorou um fato importante: essas não são funções, são métodos. Métodos e funções são fundamentalmente diferentes. Funções são objetos, métodos pertencem a objetos.

No Scala, que tem funções e métodos, existem as seguintes diferenças entre métodos e funções:

  • métodos podem ser genéricos, funções não podem
  • métodos podem ter nenhuma, uma ou muitas listas de parâmetros, funções sempre têm exatamente uma lista de parâmetros
  • métodos podem ter uma lista de parâmetros implícita, funções não podem
  • métodos podem ter parâmetros opcionais com argumentos padrão, funções não podem
  • métodos podem ter parâmetros repetidos, funções não podem
  • os métodos podem ter parâmetros nominais, funções não podem
  • métodos podem ser chamados com argumentos nomeados, funções não podem
  • funções são objetos, métodos não são

Assim, o substituto proposto simplesmente não funciona, pelo menos para esses casos.

    
por 27.09.2014 / 02:13
fonte
3

Voltando à questão, se alguém não estiver interessado em tentar editar o código-fonte em uma máquina que é extremamente restrita à RAM ou minimizar o tempo para lê-lo em um disquete, o que há de errado com o uso de palavras-chave?

Certamente é melhor ler x=y+z do que store the value of y plus z into x , mas isso não significa que os caracteres de pontuação sejam inerentemente "melhores" que as palavras-chave. Se as variáveis i , j e k forem Integer e x forem Real , considere as seguintes linhas em Pascal:

k := i div j;
x := i/j;

A primeira linha executará uma divisão inteira de truncagem, enquanto a segunda executará uma divisão de números reais. A distinção pode ser feita muito bem porque Pascal usa div como seu operador de divisão de inteiros truncados, em vez de tentar usar um sinal de pontuação que já tenha outro propósito (divisão de números reais).

Embora existam alguns contextos nos quais pode ser útil tornar uma definição de função concisa (por exemplo, um lambda que é usado como parte de outra expressão), as funções geralmente devem se destacar e serem facilmente reconhecidas visualmente como funções. Embora seja possível tornar a distinção muito mais sutil e usar apenas caracteres de pontuação, qual seria o objetivo? Dizer Function Foo(A,B: Integer; C: Real): String deixa claro qual é o nome da função, quais parâmetros ela espera e o que ela retorna. Talvez se pudesse encurtá-lo por seis ou sete caracteres, substituindo Function por alguns caracteres de pontuação, mas o que seria ganho?

Outra coisa a notar é que há uma maioria de frameworks uma diferença fundamental entre uma declaração que sempre associará um nome a um método particular ou a uma ligação virtual específica, e uma que cria uma variável que inicialmente identifica um método ou ligação, mas poderia ser alterado em tempo de execução para identificar outro. Como esses conceitos são muito semanticamente muito diferentes na maioria das estruturas processuais, faz sentido que eles tenham uma sintaxe diferente.

    
por 27.09.2014 / 00:13
fonte
3

As razões que posso pensar são:

  • É mais fácil para o compilador saber o que declaramos.
  • É importante que saibamos (de maneira trivial) se isso é uma função ou uma variável. As funções geralmente são caixas pretas e não nos importamos com sua implementação interna. Eu não gosto de inferência de tipo em tipos de retorno no Scala, porque acredito que é mais fácil usar uma função que tenha um tipo de retorno: é frequentemente a única documentação fornecida.
  • E o mais importante é a estratégia seguinte à multidão usada na criação de linguagens de programação. C ++ foi criado para roubar programadores C, e Java foi projetado de uma forma que não assusta programadores C ++, e C # para atrair programadores Java. Mesmo o C #, que eu acho que é uma linguagem muito moderna, com uma incrível equipe por trás dele, copiou alguns erros do Java ou mesmo do C.
por 26.09.2014 / 22:14
fonte
2

Bem, a razão pode ser que essas linguagens não são funcionais o suficiente, por assim dizer. Em outras palavras, você raramente define funções. Assim, o uso de uma palavra-chave extra é aceitável.

Em idiomas da herança ML ou Miranda, OTOH, você define funções na maior parte do tempo. Olhe para algum código de Haskell, por exemplo. É literalmente principalmente uma sequência de definições de funções, muitas delas possuem funções locais e funções locais dessas funções locais. Assim, uma palavra-chave divertida em Haskell seria um erro e exigiria uma declaração de avaliação em uma linguagem imperativa para começar com atribuir . Provavelmente, a designação é, de longe, a afirmação mais frequente.

    
por 27.09.2014 / 18:48
fonte
1

Isso pode ser útil em linguagens dinâmicas onde o tipo não é tão importante, mas não é legível em linguagens tipificadas estáticas onde sempre você deseja saber o tipo de sua variável. Além disso, em linguagens orientadas a objetos, é muito importante conhecer o tipo de sua variável, a fim de saber quais operações ela suporta.

No seu caso, uma função com 4 variáveis seria:

(int, long, double, String => int) addOne = (x, y, z, s) => {
  return x + 1;
}

Quando olho para o cabeçalho da função e vejo (x, y, z, s), mas não conheço os tipos dessas variáveis. Se eu quiser saber o tipo de z , que é o terceiro parâmetro, terei que olhar o início da função e começar a contar 1, 2, 3 e depois ver que o tipo é double . No primeiro modo, eu olho diretamente e vejo double z .

    
por 26.09.2014 / 21:24
fonte
1

Pessoalmente, não vejo nenhuma falha fatal na sua ideia; você pode achar que é mais complicado do que você esperava expressar certas coisas usando sua nova sintaxe, e / ou você pode achar que precisa revisá-lo (adicionando vários casos especiais e outros recursos, etc.), mas duvido que você se encontre precisando abandonar completamente a idéia.

A sintaxe que você propôs parece mais ou menos como uma variante de alguns dos estilos de notação às vezes usados para expressar funções ou tipos de funções em matemática. Isso significa que, como todas as gramáticas, provavelmente será mais atraente para alguns programadores do que para outros. (Como matemático, acontece que eu gosto disso.)

No entanto, você deve observar que na maioria das linguagens, a sintaxe def (ou seja, a sintaxe tradicional) não se comporta de maneira diferente de uma atribuição de variável padrão.

  • Na família C e C++ , as funções geralmente não são tratadas como "objetos", ou seja, blocos de dados digitados a serem copiados e colocados na pilha e outros itens. (Sim, você pode ter ponteiros de função, mas eles ainda apontam para código executável, não para "dados" no sentido típico).
  • Na maioria dos idiomas OO, há tratamento especial para métodos (por exemplo, funções de membro); isto é, não são apenas funções declaradas dentro do escopo de uma definição de classe. A diferença mais importante é que o objeto no qual o método está sendo chamado é normalmente passado como um primeiro parâmetro implícito para o método. O Python torna isso explícito com self (que, a propósito, não é realmente uma palavra-chave; você poderia tornar qualquer identificador válido o primeiro argumento de um método).

Você precisa considerar se sua nova sintaxe precisa (e esperamos que intuitivamente) representa o que o compilador ou o interpretador está realmente fazendo. Pode ajudar a ler, digamos, a diferença entre lambdas e métodos em Ruby; isso lhe dará uma idéia de como o paradigma de funções-apenas-dados difere do típico paradigma OO / procedural.

    
por 26.09.2014 / 23:46
fonte
1

Para algumas funções de idiomas não são valores. Em tal linguagem para dizer que

val f = << i : int  ... >> ;

é uma definição de função, enquanto

val a = 1 ;

declara uma constante, é confuso porque você está usando uma sintaxe para significar duas coisas.

Outros idiomas, como ML, Haskell e Scheme, tratam as funções como valores de primeira classe, mas fornecem ao usuário uma sintaxe especial para declarar constantes com valor de função. * Eles estão aplicando a regra que "uso encurta o formulário". Ou seja Se uma construção for comum e detalhada, você deverá fornecer ao usuário uma abreviação. É deselegante dar ao usuário duas sintaxes diferentes que significam exatamente a mesma coisa; às vezes a elegância deve ser sacrificada para a utilidade.

Se, na sua linguagem, as funções são de 1a classe, então por que não tentar encontrar uma sintaxe que seja suficientemente concisa para que você não seja tentado a encontrar um açúcar sintático?

- Editar -

Outra questão que ninguém mencionou (ainda) é a recursão. Se você permitir

{ 
    val f = << i : int ... g(i-1) ... >> ;
    val g = << j : int ... f(i-1) ... >> ;
    f(x)
}

e você permite

{
    val a = 42 ;
    val b = a + 1 ;
    a
} ,

segue-se que você deve permitir

{
    val a = b + 1 ; 
    val b = a - 1 ;
    a
} ?

Em uma linguagem preguiçosa (como Haskell), não há problema aqui. Em uma linguagem com essencialmente nenhuma verificação estática (como LISP), não há nenhum problema aqui. Mas em um idioma ansiosamente verificado estaticamente, você tem que ter cuidado sobre como as regras da verificação estática são definidas, se você deseja permitir as duas primeiras e proibir a última.

- Fim da edição -

* Pode-se argumentar que Haskell não pertence a essa lista. Ele fornece duas maneiras de declarar uma função, mas ambas são, em certo sentido, generalizações da sintaxe para declarar constantes de outros tipos

    
por 27.09.2014 / 23:06
fonte
0

Existe uma razão muito simples para ter essa distinção na maioria das linguagens: é necessário distinguir avaliação e declaração . Seu exemplo é bom: por que não gostar de variáveis? Bem, expressões de variáveis são imediatamente avaliadas.

Haskell tem um modelo especial onde não há distinção entre avaliação e declaração, e é por isso que não há necessidade de uma palavra-chave especial.

    
por 26.09.2014 / 22:48
fonte
0

As funções são declaradas diferentemente de literais, objetos, etc. na maioria das linguagens porque são usadas de maneira diferente, depuradas de forma diferente e representam diferentes fontes potenciais de erro.

Se uma referência de objeto dinâmico ou um objeto mutável é passado para uma função, a função pode alterar o valor do objeto conforme ele é executado. Esse tipo de efeito colateral pode tornar difícil seguir o que uma função fará se estiver aninhada em uma expressão complexa, e isso é um problema comum em linguagens como C ++ e Java.

Considere a possibilidade de depurar algum tipo de módulo do kernel em Java, onde cada objeto possui uma operação toString (). Embora seja possível que o método toString () deva restaurar o objeto, talvez seja necessário desmontar e remontar o objeto para converter seu valor em um objeto String. Se você estiver tentando depurar os métodos que toString () chamará (em um cenário hook-and-template) para fazer seu trabalho e acidentalmente destacar o objeto na janela de variáveis da maioria dos IDE, ele pode travar o depurador. Isso ocorre porque o IDE tentará toString () o objeto que chama o mesmo código que você está no processo de depuração. Nenhum valor primitivo jamais faz porcaria assim porque o significado semântico dos valores primitivos é definido pela linguagem, não pelo programador.

    
por 27.09.2014 / 02:10
fonte