Java >> Tutoriel Java >  >> Java

La différence entre les déclarations et les expressions

Au fur et à mesure que je m'intéresse aux langages de programmation - et aux langages en général - je trouve que la théorie ne correspond pas toujours à la réalité. Par exemple, je viens d'apprendre la différence entre les déclarations et les expressions et comment cette différence n'est pas toujours explicite dans les langages de programmation modernes.

Contexte

En tant que doctorant et assistant d'enseignement diplômé, je me suis beaucoup concentré sur ce qu'il faut pour être un bon professeur. Pour ce faire, j'ai appris auprès de différentes facultés leurs expériences et leurs philosophies. Récemment, j'ai appris la différence entre les déclarations et les expressions, alors j'ai pensé que ce serait amusant de partager avec vous.

Curieusement, j'ai en fait appris la distinction à la dure en m'entraînant pour enseigner un cours sur les bases du logiciel. Dans le cadre de cette formation, j'ai dû effectuer tous les devoirs de programmation afin de pouvoir obtenir des commentaires de l'instructeur. À un moment donné, l'instructeur m'a mentionné qu'il n'aimait pas la syntaxe Java suivante :

a[++i]

Dans ce cas, nous avons un tableau auquel nous accédons via ++i . En d'autres termes, nous incrémentons i puis accédez à a à cet index, le tout sur une seule ligne. Vous voyez des problèmes ? Si ce n'est pas le cas, ne vous inquiétez pas ! C'est le sujet de l'article d'aujourd'hui.

Terminologie

Dès le départ, j'aimerais différencier deux termes :expression et déclaration. Ces termes formeront la base de l'argument derrière pourquoi a[++i] est considéré comme une mauvaise pratique.

Expressions

En informatique, lorsque nous parlons d'expressions, nous faisons référence à tout ce qui peut être évalué pour produire une valeur. Naturellement, nous pouvons considérer n'importe quelle donnée en elle-même comme une expression car les données s'évaluent toujours par elles-mêmes :

4
"Hi!"
x
'w'
true
9.0

Bien entendu, les expressions peuvent être composées d'expressions :

4 + 2
"Hi," + " friend!"
x * y
'w' + 4
true == !false
9.0 / 3

Dans chacun de ces scénarios, nous utilisons des opérateurs pour imbriquer nos expressions, nous obtenons donc quelque chose qui pourrait ressembler à la grammaire de langage suivante :

<expr>: number 
      | (<expr>)
      | <expr> * <expr>
      | <expr> + <expr> 

Ici, nous avons créé une grammaire idiote qui définit une expression comme un nombre, une expression entre parenthèses, une expression multipliée par une expression ou une expression plus une expression. Comme vous pouvez probablement l'imaginer, il existe de nombreuses façons d'écrire une expression. La seule règle est que l'expression doit renvoyer une valeur.

Déclarations

En revanche, les instructions ne renvoient rien. Au lieu de cela, ils effectuent une action qui introduit une forme d'état (c'est-à-dire un effet secondaire). La liste suivante contient quelques exemples d'instructions :

x = 5
if (y) { ... }
while (true) { ... }
return s

Si nous regardons attentivement, nous pouvons remarquer que certaines déclarations contiennent des expressions. Cependant, les déclarations elles-mêmes n'évaluent rien.

Ce qui est intéressant avec les déclarations, c'est qu'elles dépendent de l'ordre. Pour donner un sens à une déclaration, il est important de comprendre le contexte qui l'a précédée.

En revanche, les expressions ne dépendent pas de l'état car elles ne produisent pas d'effets secondaires, de sorte que toute expression imbriquée peut être raisonnée directement. Par exemple, notez comment nous pouvons isoler n'importe quelle partie de l'expression suivante et évaluer son résultat :

((6 * 7) + (5 + 2 + 1)) > 17

Bien sûr, toute portée externe dépendra du résultat d'une portée interne, mais en évaluant (6 * 7) n'a aucun effet sur 17 . En conséquence, il est très facile de raisonner sur l'expression même lorsque des éléments de celle-ci changent. Bienvenue dans les bases de la programmation fonctionnelle, mais c'est un sujet d'une autre époque !

Quel est le problème ?

Malheureusement, alors que les définitions que j'ai fournies sont nettes, les langages de programmation modernes n'adhèrent pas toujours aux mêmes principes. Par exemple, est ++i une affirmation ou une expression ? Question piège :il peut s'agir des deux.

En Java, ++i et i++ peuvent être utilisées comme instructions autonomes pour modifier l'état du programme. Par exemple, ils sont souvent utilisés pour incrémenter une variable dans une boucle for. De plus, cependant, ils peuvent être utilisés comme expressions :

a[++i]
a[i++]
someFunction(i++)

En d'autres termes, ++i renvoie une valeur, et cette valeur est différente de i++ . Comme vous pouvez probablement l'imaginer, cette ambiguïté entre les déclarations et les expressions peut se manifester par des bugs désagréables. Par exemple, à votre avis, que fait le programme suivant ?

i = 0
while (i < 5) {
  print(i)
  i = i++
}

Sans entrer dans les détails, cet extrait de code peut faire beaucoup de choses différentes. En Java, il imprimera en fait zéro indéfiniment malgré l'incrémentation claire de i en 4ème ligne. Il s'avère que le suffixe ++ l'opérateur renvoie l'ancienne valeur de i après avoir augmenté sa valeur de un. En d'autres termes, i est incrémenté puis remis à zéro.

Les conséquences de l'ambiguïté entre les déclarations et les expressions sont immenses et se répercutent également sur les fonctions et les procédures.

Mais attendez, il y a plus

Souvent, des termes tels que méthodes, fonctions, procédures et sous-programmes sont tous utilisés de manière interchangeable. En fait, vous constaterez probablement que je différencie à peine les termes sur mon propre site. Cela dit, il existe au moins une différence subtile entre les fonctions et les procédures, alors parlons-en.

Fonctions

Comme les fonctions mathématiques, les fonctions de programmation renvoient une valeur à partir d'une entrée :

int getLength(String s) { ... }
double computeAreaOfSquare(double length) { ... }
double computePotentialEnergy(double m, double g, double h) { ... } 

En d'autres termes, le type de retour d'une fonction ne peut pas être rien (c'est-à-dire void). Par conséquent, les fonctions sont similaires aux expressions :elles renvoient une valeur sans aucun effet secondaire. En fait, ils fonctionnent souvent à la place des expressions :

(getLength(s1) * 2) > getLength(s2)

Par définition, une fonction serait alors une expression.

Procédures

En revanche, les procédures ne renvoient pas de valeur. Au lieu de cela, ils effectuent une action :

void scale(Square sq, double sc) { ... }
void insertElementAt(int[] list, int index, int element) { ... }
void mutateString(char[] str) { ... }

Par conséquent, les procédures sont plus proches des déclarations en ce sens qu'elles ne produisent que des effets secondaires. Naturellement, ils ne peuvent pas être utilisés comme expressions :

mutateString(s) * 4 // What?

Par définition, une procédure serait alors une instruction.

Brouiller les lignes

Comme pour les expressions et les déclarations, les langages de programmation modernes ont brouillé les frontières entre les fonctions et les procédures. Dans certains cas, il n'est même pas possible de séparer les deux.

Considérez Java qui a un pass-by-value système. Si nous voulons concevoir une structure de données, nous implémentons souvent des actions comme add , remove , push , pop , enqueue , dequeue , etc. Ces actions sont intuitives car elles fonctionnent comme nous nous attendons à ce qu'elles fonctionnent. Par exemple, si nous voulons ajouter un élément à une pile, nous allons appeler push avec une copie de l'élément en entrée.

Maintenant, imaginons que nous voulions implémenter l'une des méthodes de suppression (c'est-à-dire pop ). Comment s'y prendre sans brouiller les frontières entre fonction et procédure ? Clairement, pop a un effet secondaire :il supprime l'élément supérieur de la pile. Idéalement, cependant, nous aimerions également pouvoir renvoyer cette valeur. Étant donné que Java est passé par valeur, nous ne pouvons pas renvoyer une référence à l'élément à l'appelant via l'un de nos paramètres. En d'autres termes, nous sommes coincés à créer une fonction avec des effets secondaires.

En conséquence, notre pop La méthode peut être utilisée soit comme une expression, soit comme une instruction. Lorsqu'il est utilisé dans une expression, il devient soudainement difficile de raisonner sur ce que fait cette expression car des parties de cette expression peuvent voir différents états de la pile. De plus, des appels successifs à la même expression peuvent donner des résultats différents car l'état de la pile change à chaque appel.

Cela dit, il existe un moyen de contourner ce problème. Nous pourrions créer une paire de méthodes, une fonction et une procédure, pour obtenir l'élément supérieur de la pile (peek ) et supprimez cet élément (pop ). L'idée ici est de maintenir la séparation entre les fonctions pures et les procédures. En d'autres termes, nous pouvons utiliser peek lorsque nous voulons savoir quelle valeur se trouve en haut de la pile sans modifier la pile. Ensuite, nous pouvons utiliser pop pour supprimer cet élément supérieur.

Bien sûr, introduire une fonction pure et une procédure à la place d'une fonction avec des effets secondaires nécessite un peu de discipline qui peut ou non payer. C'est à vous de décider si cela en vaut la peine.

Discussion

Pour moi, l'apprentissage de la distinction entre les déclarations et les expressions a déclenché une réaction en chaîne de questions sur la conception du langage. Après tout, des millions de personnes dans le monde codent sans se soucier de ces détails, alors ma question est :est-ce vraiment important ?

Dernièrement, j'ai remarqué une tendance à la programmation fonctionnelle (FP), et je me demande si c'est une conséquence de toute la dette technique qui s'est accumulée à partir des lignes floues entre les expressions et les déclarations. Si non, cette tendance à la PF est-elle vraiment un battage publicitaire ? Après tout, FP n'est pas nouveau. Par exemple, Lisp a plus de 60 ans, ce qui est une éternité dans la communauté technologique. Qu'en pensez-vous ?

Pendant que vous êtes ici, consultez certains de ces articles connexes :

  • Hello World en Lisp
  • Le comportement de 'i =i++' en Java
  • Ciseaux à papier de roche utilisant l'arithmétique modulaire

De plus, si vous souhaitez développer le site, j'ai une liste de diffusion où vous recevrez des e-mails hebdomadaires sur les nouveaux articles. Alternativement, vous pouvez devenir un membre à part entière qui vous donnera accès au blog. En tout cas, merci d'avoir pris le temps de lire mon travail !

Modifier  :À l'époque où j'avais l'habitude d'activer les commentaires sur ce blog, quelqu'un a partagé quelques mots gentils :


Balise Java