La plupart des langages de programmation (langages typés dynamiquement et statiquement) ont un mot-clé et/ou une syntaxe spéciaux qui sont très différents de la déclaration de variables pour déclarer des fonctions. Je vois les fonctions tout comme déclarer une autre entité nommée:
Par exemple en Python:
x = 2
y = addOne(x)
def addOne(number):
return number + 1
Pourquoi pas:
x = 2
y = addOne(x)
addOne = (number) =>
return number + 1
De même, dans un langage comme Java:
int x = 2;
int y = addOne(x);
int addOne(int x) {
return x + 1;
}
Pourquoi pas:
int x = 2;
int y = addOne(x);
(int => int) addOne = (x) => {
return x + 1;
}
Cette syntaxe semble une manière plus naturelle de déclarer quelque chose (que ce soit une fonction ou une variable) et un mot clé de moins comme def
ou function
dans certaines langues. Et, OMI, il est plus cohérent (je regarde au même endroit pour comprendre le type d'une variable ou d'une fonction) et rend probablement l'analyseur/grammaire un peu plus simple à écrire.
Je sais que très peu de langages utilisent cette idée (CoffeeScript, Haskell) mais la plupart des langages courants ont une syntaxe spéciale pour les fonctions (Java, C++, Python, JavaScript, C #, PHP, Ruby).
Même dans Scala, qui prend en charge les deux façons (et a une inférence de type), il est plus courant d'écrire:
def addOne(x: Int) = x + 1
Plutôt que:
val addOne = (x: Int) => x + 1
OMI, au moins à Scala, c'est probablement la version la plus facilement compréhensible mais cet idiome est rarement suivi:
val x: Int = 1
val y: Int = addOne(x)
val addOne: (Int => Int) = x => x + 1
Je travaille sur ma propre langue de jouet et je me demande s'il y a des pièges si je conçois ma langue de cette manière et s'il y a des raisons historiques ou techniques ce modèle n'est pas largement suivi?
Je pense que la raison en est que la plupart des langages populaires proviennent ou ont été influencés par la famille C des langages par opposition aux langages fonctionnels et à leur racine, le lambda calcul.
Et dans ces langages, les fonctions sont pas juste une autre valeur:
const
, readonly
ou final
pour interdire la mutation), mais les fonctions ne peuvent pas être réaffectées.D'un point de vue plus technique, le code (qui est composé de fonctions) et les données sont séparés. Ils occupent généralement différentes parties de la mémoire et sont accessibles différemment: le code est chargé une seule fois puis exécuté uniquement (mais pas lu ou écrit), tandis que les données sont souvent constamment allouées et désallouées et sont écrites et lues, mais jamais exécutées.
Et puisque C était censé être "proche du métal", il est logique de refléter cette distinction dans la syntaxe du langage également.
L'approche "la fonction n'est qu'une valeur" qui constitue la base de la programmation fonctionnelle n'a gagné du terrain dans les langages courants que relativement récemment, comme en témoigne l'introduction tardive des lambdas en C++, C # et Java (2011, 2007, 2014).
C'est parce qu'il est important pour humains de reconnaître que les fonctions ne sont pas simplement "une autre entité nommée". Parfois, il est logique de les manipuler en tant que tels, mais ils peuvent toujours être reconnus en un coup d'œil.
Peu importe ce que l'ordinateur pense de la syntaxe, car un blob de caractères incompréhensible est bien pour une machine à interpréter, mais ce serait presque impossible pour les humains à comprendre et à maintenir.
C'est vraiment la même raison que pour laquelle nous avons des boucles while et for, switch et if else, etc., même si toutes se résument finalement à une instruction de comparaison et de saut. La raison en est que c'est là pour le bénéfice des humains qui maintiennent et comprennent le code.
Le fait d'avoir vos fonctions comme "une autre entité nommée" de la façon dont vous proposez rendra votre code plus difficile à voir et donc plus difficile à comprendre.
Vous pourriez être intéressé d'apprendre que, remontant à l'époque préhistorique, un langage appelé ALGOL 68 utilisait une syntaxe proche de ce que vous proposez. Reconnaissant que les identificateurs de fonction sont liés à des valeurs comme les autres identificateurs, vous pouvez dans ce langage déclarer une fonction (constante) en utilisant la syntaxe
type de fonctionnom = (liste de paramètres) type de résultat: corps ;
Concrètement, votre exemple se lirait
PROC (INT)INT add one = (INT n) INT: n+1;
Reconnaissant la redondance en ce que le type initial peut être lu à partir du RHS de la déclaration, et étant un type de fonction commençant toujours par PROC
, cela pourrait (et serait généralement) contracté à
PROC add one = (INT n) INT: n+1;
mais notez que le =
vient toujours avant la liste des paramètres. Notez également que si vous vouliez une fonction variable (à laquelle une autre valeur du même type de fonction pourrait être attribuée ultérieurement), le =
doit être remplacé par :=
, donnant l'un des
PROC (INT)INT func var := (INT n) INT: n+1;
PROC func var := (INT n) INT: n+1;
Cependant, dans ce cas, les deux formes sont en fait des abréviations; puisque l'identifiant func var
désigne une référence à une fonction générée localement, la forme entièrement développée serait
REF PROC (INT)INT func var = LOC PROC (INT)INT := (INT n) INT: n+1;
Il est facile de s’habituer à cette forme syntaxique particulière, mais elle n’avait manifestement pas un large public dans d’autres langages de programmation. Même les langages de programmation fonctionnels comme Haskell préfèrent le style f n = n+1
avec =
suivant la liste des paramètres. Je suppose que la raison est principalement psychologique; après tout, même les mathématiciens ne préfèrent pas souvent, comme moi, f = n ⟼ n + 1 sur f = (n) = n + 1.
Soit dit en passant, la discussion ci-dessus met en évidence une différence importante entre les variables et les fonctions: les définitions de fonction lient généralement un nom à une valeur de fonction spécifique, qui ne peut pas être modifiée ultérieurement, tandis que les définitions de variable introduisent généralement un identifiant avec un initial valeur, mais qui peut changer plus tard. (Ce n'est pas une règle absolue; les variables de fonction et les constantes non fonction se produisent dans la plupart des langues.) De plus, dans les langues compilées, la valeur liée dans une définition de fonction est généralement une constante de temps de compilation, de sorte que les appels à la fonction peuvent être compilé en utilisant une adresse fixe dans le code. En C/C++, c'est même une exigence; l'équivalent de l'ALGOL 68
PROC (REAL) REAL f = IF mood=sunny THEN sin ELSE cos FI;
ne peut pas être écrit en C++ sans introduire un pointeur de fonction. Ce type de limitations spécifiques justifie l'utilisation d'une syntaxe différente pour les définitions de fonction. Mais ils dépendent de la sémantique des langues, et la justification ne s'applique pas à toutes les langues.
Vous avez mentionné Java et Scala comme exemples. Cependant, vous avez ignoré un fait important: ce ne sont pas des fonctions, ce sont des méthodes. Les méthodes et les fonctions sont - fondamentalement différent. Les fonctions sont des objets, les méthodes appartiennent aux objets.
Dans Scala, qui a à la fois des fonctions et des méthodes, il existe les différences suivantes entre les méthodes et les fonctions:
Donc, votre remplacement proposé ne fonctionne tout simplement pas, du moins pour ces cas.
Les raisons auxquelles je peux penser sont:
Pour inverser la question, si l'on n'est pas intéressé à essayer de modifier le code source sur une machine qui est extrêmement limitée en RAM, ou à minimiser le temps de le lire sur une disquette, quel est le problème avec l'utilisation de mots clés?
Certes, il est plus agréable de lire x=y+z
Que store the value of y plus z into x
, Mais cela ne signifie pas que les caractères de ponctuation sont intrinsèquement "meilleurs" que les mots clés. Si les variables i
, j
et k
sont Integer
et x
est Real
, tenez compte des lignes suivantes en Pascal:
k := i div j;
x := i/j;
La première ligne effectuera une division entière tronquée, tandis que la seconde effectuera une division en nombre réel. La distinction peut être bien faite parce que Pascal utilise div
comme opérateur de division d'entier tronqué, plutôt que d'essayer d'utiliser un signe de ponctuation qui a déjà un autre but (division en nombre réel).
Bien qu'il existe quelques contextes dans lesquels il peut être utile de rendre une définition de fonction concise (par exemple, un lambda qui est utilisé dans le cadre d'une autre expression), les fonctions sont généralement censées se démarquer et être facilement reconnaissables visuellement en tant que fonctions. Bien qu'il soit possible de rendre la distinction beaucoup plus subtile et de n'utiliser que des caractères de ponctuation, quel serait l'intérêt? Dire Function Foo(A,B: Integer; C: Real): String
indique clairement le nom de la fonction, les paramètres qu'elle attend et ce qu'elle retourne. On pourrait peut-être le raccourcir de six ou sept caractères en remplaçant Function
par des caractères de ponctuation, mais que gagnerait-il?
Une autre chose à noter est qu'il existe dans la plupart des cadres une différence fondamentale entre une déclaration qui associera toujours un nom à une méthode particulière ou à une liaison virtuelle particulière, et une qui crée une variable qui identifie initialement une méthode ou une liaison particulière, mais pourrait être modifié à l'exécution pour en identifier un autre. Comme ce sont des concepts très sémantiquement très différents dans la plupart des cadres procéduraux, il est logique qu'ils aient une syntaxe différente.
Eh bien, la raison pourrait être que ces langages ne sont pas assez fonctionnels, pour ainsi dire. En d'autres termes, vous définissez plutôt rarement des fonctions. Ainsi, l'utilisation d'un mot clé supplémentaire est acceptable.
Dans les langues du patrimoine ML ou Miranda, OTOH, vous définissez la plupart du temps des fonctions. Regardez du code Haskell, par exemple. C'est littéralement principalement une séquence de définitions de fonctions, beaucoup d'entre elles ont des fonctions locales et des fonctions locales de ces fonctions locales. Par conséquent, un mot-clé fun dans Haskell serait une erreur aussi grande que d'exiger une déclaration d'assingment dans un langage impératif pour commencer par assign. L'affectation des causes est probablement de loin la déclaration la plus fréquente.
Personnellement, je ne vois aucun défaut fatal dans votre idée; vous trouverez peut-être que c'est plus compliqué que vous ne le pensiez d'exprimer certaines choses en utilisant votre nouvelle syntaxe, et/ou vous devrez peut-être la réviser (en ajoutant divers cas spéciaux et d'autres fonctionnalités, etc.), mais je doute que vous vous retrouviez besoin d'abandonner complètement l'idée.
La syntaxe que vous avez proposée ressemble plus ou moins à une variante de certains des styles de notation parfois utilisés pour exprimer des fonctions ou des types de fonctions en mathématiques. Cela signifie que, comme toutes les grammaires, il plaira probablement plus à certains programmeurs qu'à d'autres. (En tant que mathématicien, j'aime ça.)
Cependant, vous devez noter que dans la plupart des langues, la syntaxe de style def
(c'est-à-dire la syntaxe traditionnelle) fait se comporte différemment d'une affectation de variable standard.
C
et C++
famille, les fonctions ne sont généralement pas traitées comme des "objets", c'est-à-dire des blocs de données typées à copier et à mettre sur la pile et ainsi de suite. (Oui, vous pouvez avoir des pointeurs de fonction, mais ceux-ci pointent toujours vers du code exécutable, pas vers des "données" au sens typique.)self
(qui, en passant, n'est pas réellement un mot-clé; vous pouvez faire de n'importe quel identifiant valide le premier argument d'une méthode).Vous devez déterminer si votre nouvelle syntaxe représente avec précision (et, espérons-le, intuitivement) ce que le compilateur ou l'interpréteur fait réellement. Cela peut aider à lire, disons, la différence entre les lambdas et les méthodes dans Ruby; cela vous donnera une idée de la façon dont votre paradigme des fonctions ne sont que des données diffère du paradigme OO/procédural typique.
Pour certaines langues, les fonctions ne sont pas des valeurs. Dans une telle langue pour dire que
val f = << i : int ... >> ;
est une définition de fonction, alors que
val a = 1 ;
déclare une constante, est source de confusion car vous utilisez une syntaxe pour signifier deux choses.
D'autres langages, tels que ML, Haskell et Scheme, traitent les fonctions comme des valeurs de 1ère classe, mais fournissent à l'utilisateur une syntaxe spéciale pour déclarer les constantes de valeur de fonction. * Ils appliquent la règle selon laquelle "l'utilisation raccourcit la forme". C'est à dire. si une construction est à la fois courante et verbeuse, vous devez donner à l'utilisateur un raccourci. Il n'est pas élégant de donner à l'utilisateur deux syntaxes différentes qui signifient exactement la même chose; parfois l'élégance doit être sacrifiée à l'utilité.
Si, dans votre langage, les fonctions sont de 1ère classe, alors pourquoi ne pas essayer de trouver une syntaxe suffisamment concise pour ne pas être tenté de trouver un sucre syntaxique?
-- Éditer --
Un autre problème que personne n'a soulevé (encore) est la récursivité. Si vous permettez
{
val f = << i : int ... g(i-1) ... >> ;
val g = << j : int ... f(i-1) ... >> ;
f(x)
}
et vous permettez
{
val a = 42 ;
val b = a + 1 ;
a
} ,
est-ce que vous devez autoriser
{
val a = b + 1 ;
val b = a - 1 ;
a
} ?
Dans une langue paresseuse (comme Haskell), il n'y a pas de problème ici. Dans une langue avec pratiquement aucun contrôle statique (comme LISP), il n'y a pas de problème ici. Mais dans un langage avide à vérification statique, vous devez faire attention à la façon dont les règles de la vérification statique sont définies, si vous voulez autoriser les deux premières et interdire la dernière.
- Fin de l'édition -
* On pourrait faire valoir que Haskell n'appartient pas à cette liste. Il fournit deux façons de déclarer une fonction, mais les deux sont, dans un sens, des généralisations de la syntaxe pour déclarer des constantes d'autres types
Cela peut être utile sur les langages dynamiques où le type n'est pas si important, mais ce n'est pas si lisible dans les langages typés statiques où vous voulez toujours connaître le type de votre variable. De plus, dans les langages orientés objet, il est assez important de connaître le type de votre variable, afin de savoir quelles opérations elle prend en charge.
Dans votre cas, une fonction à 4 variables serait:
(int, long, double, String => int) addOne = (x, y, z, s) => {
return x + 1;
}
Quand je regarde l'en-tête de fonction et vois (x, y, z, s) mais je ne connais pas les types de ces variables. Si je veux connaître le type de z
qui est le troisième paramètre, je devrai regarder le début de la fonction et commencer à compter 1, 2, 3 puis voir que le type est double
. Dans l'ancien, je regarde directement et vois double z
.
Il y a une raison très simple d'avoir une telle distinction dans la plupart des langues: il faut distinguer évaluation et déclaration. Votre exemple est bon: pourquoi ne pas aimer les variables? Eh bien, les expressions de variables sont immédiatement évaluées.
Haskell a un modèle spécial où il n'y a pas de distinction entre évaluation et déclaration, c'est pourquoi il n'y a pas besoin d'un mot-clé spécial.
Les fonctions sont déclarées différemment des littéraux, des objets, etc. dans la plupart des langues car elles sont utilisées différemment, déboguées différemment et posent différentes sources d'erreur potentielles.
Si une référence d'objet dynamique ou un objet modifiable est passé à une fonction, la fonction peut modifier la valeur de l'objet lors de son exécution. Ce type d'effet secondaire peut rendre difficile de suivre ce qu'une fonction fera si elle est imbriquée dans une expression complexe, et c'est un problème courant dans des langages comme C++ et Java.
Pensez à déboguer une sorte de module de noyau en Java, où chaque objet a une opération toString (). Bien que l'on puisse s'attendre à ce que la méthode toString () restaure l'objet, elle peut avoir besoin de démonter et de réassembler l'objet afin de traduire sa valeur en un objet String. Si vous essayez de déboguer les méthodes que toString () appellera (dans un scénario de hook et modèle) pour faire son travail, et mettre accidentellement en surbrillance l'objet dans la fenêtre des variables de la plupart des IDE, il peut planter le débogueur. C'est parce que le IDE va essayer de toString () l'objet qui appelle le code même que vous êtes en train de déboguer. Aucune valeur primitive ne fait jamais de merde comme ça parce que la signification sémantique de primitive Les valeurs sont définies par le langage et non par le programmeur.