La plupart des langages de programmation (à la fois typés dynamiquement et statiquement) ont des mots-clés et / ou une syntaxe spéciaux qui ont une apparence très différente de la déclaration de variables pour la déclaration de fonctions. Je vois les fonctions comme déclarant simplement 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, IMO, 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 / la 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 ont une syntaxe spéciale pour les fonctions (Java, C ++, Python, JavaScript, C #, PHP, Ruby).
Même en Scala, qui prend en charge les deux méthodes (et comporte 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 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 mon propre langage des jouets et je me demande s’il existe des pièges si je conçois mon langage de cette manière et s’il existe des raisons historiques ou techniques pour lesquelles ce modèle n’est pas largement suivi?
(int => int) addOne = (x) => {
c'est beaucoup plus "spécial" et "complexe" queint addOne(int) {
...Réponses:
Je pense que la raison en est que la plupart des langues populaires proviennent ou ont été influencées par la famille de langues C, par opposition aux langues fonctionnelles et à leur racine, le lambda calcul.
Et dans ces langages, les fonctions ne sont pas simplement une autre valeur:
const
,readonly
oufinal
pour interdisez mutation), mais les fonctions ne peuvent pas être réaffectés.D'un point de vue plus technique, le code (composé de fonctions) et les données sont séparés. Elles occupent généralement différentes parties de la mémoire et on y accède différemment: le code est chargé une seule fois, puis exécuté (mais non lu ni écrit), alors que les données sont souvent allouées et désallouées en permanence et sont écrites et lues mais jamais exécutées.
Et comme C était censé être "proche du métal", il est également logique de refléter cette distinction dans la syntaxe du langage.
L'approche "fonction n'est qu'une valeur" qui constitue la base de la programmation fonctionnelle n'a gagné du terrain que récemment dans les langages communs, comme en témoigne l'introduction tardive de lambdas en C ++, C # et Java (2011, 2007, 2014).
la source
C'est parce qu'il est important que les humains reconnaissent 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 incompréhensible est très bien pour une machine à interpréter, mais ce serait presque impossible à comprendre et à entretenir pour les humains.
C'est vraiment la même raison que celle pour laquelle nous avons des boucles while et for, switch et if else, etc., même si toutes finissent par se résumer à une instruction de comparaison et de saut. La raison en est parce que c'est là pour le bénéfice des humains qui maintiennent et comprennent le code.
Avoir vos fonctions en tant qu '"une autre entité nommée" comme vous le proposez rendra votre code plus difficile à visualiser, et donc à comprendre.
la source
whatsisname
été davantage une réponse au premier point (et une alerte sur le danger de supprimer ces sécurités), alors que votre commentaire est davantage lié à la seconde partie de la question. Il est en effet possible de changer cette syntaxe (et comme vous l'avez décrit, cela a déjà été fait à maintes reprises ...) mais cela ne conviendra pas à tout le monde (comme oop ne convient pas à tout le monde non plus.Vous serez peut-être intéressé d'apprendre que, dès la préhistoire, un langage appelé ALGOL 68 utilisait une syntaxe proche de celle 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) à l'aide de la syntaxe
Concrètement, votre exemple se lirait
Reconnaître la redondance en ce que le type initial peut être lu à partir de la RHS de la déclaration, et comme un type de fonction commence toujours par
PROC
, cela pourrait (et le serait généralement) contracté avecmais notez que le
=
reste toujours avant la liste de paramètres. Notez également que si vous vouliez une variable de fonction (à laquelle une autre valeur du même type de fonction pourrait ultérieurement être affectée), elle=
devrait être remplacée par:=
, donnant l'une ou l'autre des options suivantes: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 complètement développée seraitIl est facile de s’habituer à cette forme syntaxique particulière, mais elle n’a manifestement pas eu un tel succès dans les 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 je le fais, f = n ⟼ n + 1 sur f ( n ) = n + 1.En passant, la discussion ci-dessus met en évidence une différence importante entre les variables et les fonctions: les définitions de fonction associent généralement un nom à une valeur de fonction spécifique, qui ne peut plus être modifiée, alors que les définitions de variable introduisent généralement un identifiant avec une valeur initiale , mais peut changer plus tard. (Ce n'est pas une règle absolue; des variables de fonction et des constantes non-fonction se produisent dans la plupart des langages.) De plus, dans les langages compilés, la valeur liée à une définition de fonction est généralement une constante de compilation, de sorte que les appels à la fonction peuvent être effectués. compilé en utilisant une adresse fixe dans le code. En C / C ++, c'est même une exigence. l'équivalent de l'ALGOL 68
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 fonctions. Mais elles dépendent de la sémantique linguistique et la justification ne s’applique pas à toutes les langues.
la source
Vous avez cité Java et Scala comme exemples. Cependant, vous avez oublié un fait important: ce ne sont pas des fonctions, ce sont des méthodes. Les méthodes et les fonctions sont fondamentalement différentes. Les fonctions sont des objets, les méthodes appartiennent à des 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 remplaçant proposé ne fonctionne tout simplement pas, du moins pour ces cas.
la source
Les raisons auxquelles je peux penser sont:
la source
Si on ne cherche pas à modifier le code source sur une machine extrêmement contrainte en RAM, ou à minimiser le temps nécessaire pour 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
questore 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 variablesi
,j
etk
sontInteger
etx
sontReal
, considérons les lignes suivantes en Pascal: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 faite gentiment parce que Pascal utilise
div
comme opérateur de division de entier entier tronqué, plutôt que d'essayer d'utiliser un signe de ponctuation qui a déjà un autre objectif (division de 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 utilisé dans le cadre d'une autre expression), les fonctions sont généralement supposées se distinguer et être facilement reconnaissables visuellement en tant que fonctions. Alors qu'il serait peut-être possible de faire la distinction beaucoup plus subtile et de n'utiliser que des caractères de ponctuation, à quoi servirait-il? Dire
Function Foo(A,B: Integer; C: Real): String
permet de préciser le nom de la fonction, ses paramètres et ses résultats. Peut-être pourrait-on raccourcir le texte de six ou sept caractères en le remplaçantFunction
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 page qui crée une variable qui identifie initialement une méthode ou une liaison particulière, mais peut être modifié au moment de l' exécution pour en identifier un autre. Étant donné que ces concepts sont très sémantiquement très différents dans la plupart des cadres de procédures, il est logique qu’ils aient une syntaxe différente.
la source
void f() {}
est plus court que l'équivalent lambda en C ++ (auto f = [](){};
), C # (Action f = () => {};
) et Java (Runnable f = () -> {};
). La concision de lambdas provient de l'inférence de type et de l'omissionreturn
, mais je ne pense pas que cela soit lié à ce que cette question demande.La raison pourrait en ê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 de l'héritage ML ou Miranda, OTOH, vous définissez des fonctions la plupart du temps. Regardez un 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é amusant en haaskell serait une erreur et nécessiterait une déclaration de détermination dans un langage impératif pour commencer avec assign . L'affectation de cause est probablement de loin la déclaration la plus fréquente.
la source
Personnellement, je ne vois aucune faille fatale dans votre idée; vous trouverez peut-être plus délicat que vous ne le pensiez d'exprimer certaines choses à l'aide de votre nouvelle syntaxe et / ou vous devez peut-être le réviser (en ajoutant divers cas spéciaux et d'autres fonctionnalités, etc.), mais je doute que vous vous retrouviez vous-même. besoin d'abandonner entièrement 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 sera probablement plus attrayant pour certains programmeurs que pour d’autres. (En tant que mathématicien, j'aime bien.)
Cependant, il faut noter que dans la plupart des langues, la
def
syntaxe de style (la syntaxe traditionnelle) ne se comporte différemment d'une affectation de variable standard.C
etC++
, les fonctions ne sont généralement pas traitées comme des "objets", c’est-à-dire des morceaux de données typées à copier et à mettre dans la pile, etc. (Oui, vous pouvez avoir des pointeurs de fonction, mais ceux-ci pointent toujours sur le code exécutable, pas sur "données" dans le sens typique.)self
(qui, soit dit en passant, n'est pas réellement un mot-clé; vous pourriez faire de n'importe quel identifiant valide le premier argument d'une méthode).Vous devez vous demander si votre nouvelle syntaxe représente avec précision (et intuitivement, idéalement) ce que le compilateur ou l'interpréteur est en train de faire. Il peut être utile de lire, par exemple, la différence entre lambdas et méthodes en Ruby; cela vous donnera une idée de la différence entre votre paradigme de fonctions - données - juste données et le paradigme OO / procédural typique.
la source
Pour certaines langues, les fonctions ne sont pas des valeurs. Dans un tel langage pour dire que
est une définition de fonction, alors que
déclare une constante, est déroutant parce que vous utilisez une syntaxe pour signifier deux choses.
D'autres langages, tels que ML, Haskell et Scheme, traitent les fonctions comme des valeurs de première classe, mais fournissent à l'utilisateur une syntaxe spéciale pour déclarer des constantes à valeur de fonction. * Ils appliquent la règle selon laquelle "l'utilisation raccourcit la forme". Par exemple, si une construction est à la fois commune et prolixe, vous devez donner un raccourci à l'utilisateur. Il est inutile 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 langue, les fonctions sont de 1ère classe, pourquoi ne pas essayer de trouver une syntaxe suffisamment concise pour ne pas être tenté de trouver un sucre syntaxique?
-- Modifier --
Un autre problème que personne n'a encore évoqué est la récursivité. Si vous permettez
et vous permettez
est-ce qu'il s'ensuit que vous devriez autoriser
Dans un langage paresseux (comme Haskell), il n'y a pas de problème ici. Dans une langue ne contenant essentiellement pas de contrôles statiques (comme LISP), il n’ya pas de problème ici. Toutefois, dans un langage cohérent vérifié statiquement, vous devez faire attention à la définition des règles de contrôle statique si vous souhaitez autoriser les deux premiers et interdire les derniers.
- Fin de l'édition -
* On pourrait soutenir que Haskell ne fait pas partie de cette liste. Il fournit deux manières de déclarer une fonction, mais les deux sont, en un sens, des généralisations de la syntaxe pour déclarer des constantes d'autres types.
la source
Cela peut être utile sur les langages dynamiques où le type n'est pas si important, mais ce n'est pas lisible dans les langages à typage statique où vous voulez toujours connaître le type de votre variable. De plus, dans les langages orientés objet, il est très important de connaître le type de votre variable afin de savoir quelles opérations sont prises en charge.
Dans votre cas, une fonction avec 4 variables serait:
Quand je regarde l'en-tête de la fonction et que je vois (x, y, z, s) mais que je ne connais pas les types de ces variables. Si je veux savoir quel type
z
est le troisième paramètre, il faudra que je regarde le début de la fonction et commence à compter 1, 2, 3 et ensuite voir que le type estdouble
. Dans le premier sens, je regarde directement et voisdouble z
.la source
var addOne = (int x, long y, double z, String s) => { x + 1 }
dans un langage non moronique typé statiquement de votre choix (exemples: C #, C ++, Scala). Même l'inférence de type local par ailleurs très limitée utilisée par C # est tout à fait suffisante pour cela. Donc, cette réponse ne fait que critiquer une syntaxe spécifique qui est douteuse au départ et qui n'est réellement utilisée nulle part (bien que la syntaxe de Haskell pose un problème très similaire).Il existe une raison très simple d’avoir une telle distinction dans la plupart des langues: il est nécessaire de 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'est pas nécessaire d'utiliser un mot clé spécial.
la source
Les fonctions sont déclarées différemment des littéraux, des objets, etc. dans la plupart des langages 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 mutable est transmise à une fonction, celle-ci peut modifier la valeur de l'objet en cours d'exécution. Ce type d'effet secondaire peut rendre difficile la tâche de suivre le fonctionnement d'une fonction si elle est imbriquée dans une expression complexe. Il s'agit d'un problème courant dans les langages tels que C ++ et Java.
Envisagez de déboguer une sorte de module du noyau en Java, où chaque objet exécute une opération toString (). Bien que l'on puisse s'attendre à ce que la méthode toString () restaure l'objet, il peut être nécessaire de le désassembler et de le réassembler afin de convertir sa valeur en objet String. Si vous essayez de déboguer les méthodes que toString () appellera (dans un scénario de modèle) pour effectuer son travail et mettra accidentellement en évidence l'objet dans la fenêtre de variables de la plupart des IDE, le débogueur peut se bloquer. En effet, l'EDI 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 ce genre de foutaise, car la signification sémantique de ces valeurs est définie par le langage et non par le programmeur.
la source