Les premiers pas
La première chose que vous devez savoir est comment démarrer Algae. Si votre système est correctement configuré, il suffit de taper la commande :
algae |
Cela fait apparaître Algae en mode interactif ; une prompt s'affiche et il attend que vous commenciez à taper des instructions. Une manuel est disponible en ligne via la fonction info ; tapez info() pour l'afficher. Vous pouvez accéder à une rubrique spécifique en la nommant comme paramètre. Par exemple, info("operators") vous amène directement à la description des opérateurs d'Algae.
Une instruction Algae décrit les opérations à effectuer. Par exemple, l'instruction :
- 1+sin(2)
indique à Algae d'ajouter 1 au sinus de 2. Les instructions peuvent être terminées par une nouvelle ligne, un point-virgule ou un point d'interrogation; les résultats sont affichés à moins qu'un point-virgule ne soit utilisé.
Les instructions
- printf ("Bonjour le monde\n");
affiche un petit message d'espoir "Bonjour le monde" sur le terminal. C'est exactement comme le langage de programmation C, jusqu'à la séquence d'échappement `\n' pour indiquer une nouvelle ligne. Si vous connaissez le C, vous remarquerez probablement de nombreuses autres similitudes entre sa syntaxe et celle d'Algae.
Comme la plupart des langages de programmation informatiques, Algae possède des variables auxquelles des valeurs peuvent être attribuées. Dans Algae, ces variables n'ont pas besoin d'être déclarées avant d'être utilisées et peuvent être créées ou détruites au cours d'une session. Si vous tapez l'instruction x=1, la variable x sera créée si elle n'existe pas déjà. Si x avait déjà une valeur, l'affectation à x détruit son contenu précédent.
Les valeurs prises par les variables sont appelées entités, ayant diverses classes telles que `scalar', `vector', `matrix', `table',... Une fonction intégrée appelée class renvoie la classe de son paramètre, donc si vous faites l'affectation x=1, l'instruction class(x) renvoie `"scalar"'.
Notez qu'un scalaire n'est pas la même chose qu'un vecteur à un élément ou qu'une matrice un par un. Les fonctions intégrées scalaire, vecteur et matrice peuvent être utilisées pour convertir de l'un à l'autre. Par exemple, matrix(7) renvoie une matrice avec une ligne et une colonne, son élément unique ayant la valeur 7. Algae effectue souvent ces conversions entre les classes automatiquement. Dans le code :
- x = [ 3, 2, 1 ];
- y = sort (x);
la fonction de tri convertit d'abord son paramètre de matrice x en vecteur, puis renvoie un autre vecteur avec les mêmes éléments mais triés par ordre croissant. (Une expression entre parenthèses définit une matrice).
Outre sa classe, une entité peut avoir d'autres attributs étant entreposés en tant que membres. Par exemple, le nombre de lignes d'une matrice est entreposé dans un membre appelé "nr". Les membres sont référencés avec l'opérateur "point", donc si M est une matrice, alors M.nr renvoie sa taille de ligne. La plupart des entités ont un ou plusieurs membres prédéfinis (tels que nr dans les matrices) que vous ne pouvez pas modifier directement. Vous pouvez créer de nouveaux membres simplement par affectation.
Une fonction appelée show affiche des informations sur une entité et ses membres. Une autre fonction, members, renvoie un vecteur contenant les noms de tous les membres de son paramètre.
Lorsqu'une variable ou un membre inexistant est référencé, la valeur spéciale NULL est renvoyée. Par exemple, la ligne :
- a = 1; a.nr
(qui se compose de deux instructions) affiche "NULL", car les scalaires ne démarrent pas avec un numéro de membre.
La constante NULL peut être utilisée sur le côté droit des affectations, supprimant ainsi la valeur précédente d'une entité. En fait, l'entité existe toujours, mais elle a la valeur NULL. Vous pouvez effectuer un certain nombre d'autres opérations sur NULL, comme dans :
- if (x != NULL) { x.class? }
Toutes les entités de tableau (y compris les scalaires) ont un membre appelé type, pouvant avoir l'une de ces valeurs : «integer», «real», «complex» ou «character». La constante 1 est un entier, mais 1.0 est de type réel. Algae n'a pas de constante complexe comme Fortran : vous devez utiliser une expression telle que sqrt(-1). Les utilisateurs effectuent souvent l'affectation i=sqrt(-1) lorsqu'ils démarrent Algae pour la première fois, puis utilisent des expressions telles que 1+2*i pour leurs nombres complexes.
Le type «character» fait référence à une chaîne de caractères. Il est spécifié à l'aide de guillemets doubles comme nous l'avons fait pour «Bonjour le monde» ci-dessus. Un scalaire de caractère comme «1» est différent d'un scalaire entier comme 1, et une expression comme 1+"1" n'est pas autorisée.
Un vecteur est un tableau unidimensionnel de valeurs. Par exemple, x=1,2,3 spécifie un vecteur à trois éléments. Les éléments d'un vecteur sont numérotés à partir de un, et le nombre total d'éléments est donné par son membre ne. Tous les éléments ont le même type.
En fait, le caractère virgule est l'opérateur «append» d'Algae. Vous pouvez mettre plusieurs expressions ensemble dans un vecteur, comme dans :
- v = 1, sin(2), 3+4;
Une expression de «sous-vecteur» peut être utilisée pour spécifier un ou plusieurs éléments particuliers. Pour cela, il suffit de suivre le vecteur d'un spécificateur entre parenthèses. Par exemple, v[3] donne un scalaire ayant la valeur du troisième élément de v. Si le spécificateur est un scalaire, le résultat est un scalaire; sinon, le résultat est un vecteur.
Un exemple plus compliqué est :
- aset = 3, 9, 15;
- x = v[aset][2];
Ici, x obtient la valeur du neuvième élément de v. Vous pouvez également affecter à un sous-vecteur, donc v[1]=0 définit le premier élément de v à zéro.
Les vecteurs ont un membre eid (il signifie element id) contenant des étiquettes pour ses éléments. Vous n'avez pas besoin d'étiquettes (eid peut être NULL), mais elles peuvent être très utiles. D'une part, elles peuvent vous aider à éviter les erreurs en ne vous permettant pas d'effectuer certaines opérations à moins que les étiquettes ne correspondent. Si vous essayez d'ajouter deux vecteurs et qu'ils ont tous deux des étiquettes, alors les étiquettes doivent être identiques.
Vous pouvez également utiliser des étiquettes au lieu de numéros d'éléments dans une expression de sous-vecteur. Vous le faites en utilisant des chaînes de caractères comme spécificateur. Par exemple, le code :
- weight = 172, 216, 188;
- weight.eid = "Tom", "Dick", "Harry";
définit le poids du vecteur avec des étiquettes de chaîne de caractères. Ensuite, weight["Dick"] donne le poids de Dick de 216.
Les vecteurs peuvent être générés en utilisant l'opérateur deux-points. L'expression 1:5:2 donne un vecteur dont le premier élément est 1, le dernier élément ne dépasse pas 5 et a une différence de 2 entre chaque élément successif. En d'autres termes, 1:5:2 est identique à 1,3,5. Si vous omettez le deuxième deux-points et le troisième opérande, Algae déduit un 1 pour le troisième opérande. Ainsi, si n=100, alors 1:n est le vecteur contenant tous les entiers de 1 à 100.
Une matrice est un tableau bidimensionnel de valeurs. L'expression [1,2;3,4;5,6] spécifie une matrice avec trois lignes et deux colonnes - les lignes sont données sous forme de vecteurs et sont séparées par des points-virgules.
Les expressions de sous-matrice fonctionnent comme les expressions de sous-vecteur, mais avec un point-virgule pour séparer le spécificateur de ligne du spécificateur de colonne. L'expression M[3;2,3] donne un vecteur contenant les éléments de M dans sa troisième ligne et ses deuxième et troisième colonnes. Si les deux spécificateurs sont des scalaires, le résultat est un scalaire. Si un seul spécificateur est un scalaire, le résultat est un vecteur. Sinon, le résultat est une matrice. Les membres nr et nc donnent le nombre de lignes et de colonnes d'une matrice.
Les matrices ont des étiquettes de ligne et de colonne. Elles sont entreposées dans les membres rid et cid, respectivement. Comme pour les vecteurs, les chaînes de caractères utilisées comme spécificateurs dans les expressions de sous-matrice font référence aux étiquettes.
Une table est une entité contenant simplement une collection d'autres entités. Par exemple, les instructions :
- x = 1; y = "foo", "bar";
- t = { x; y };
aboutit à un tableau t contenant le scalaire x et le vecteur y. Au lieu de cela, nous pourrions écrire :
- t = { x = 1; y = "foo", "bar" };
et obtenir la même table. Dans le dernier cas, cependant, x et y n'existent qu'à l'intérieur de la table.
Vous pouvez mettre n'importe quelle classe d'entité dans une table, même une autre table. Les membres sont référencés avec l'opérateur "point", tout comme les autres entités. La ligne a={u=1;v=2}; a.u+a.v imprime la valeur 3. Vous pouvez ajouter deux tables (les membres de la table de droite sont insérés dans la table de gauche) et soustraire deux tables (les membres de la table de gauche ayant le même nom qu'un membre de la table de droite sont supprimés).
Algae exécute normalement les instructions dans l'ordre dans lequel elle les reçoit, mais les instructions de contrôle de flux if, for, while et try peuvent changer cela. Le code :
- if (x > 0) { y = 1/x; }
est un exemple d'instruction if. Les parenthèses sont obligatoires autour de l'expression de test. Si cette expression est "true", alors les instructions la suivant sont exécutées. L'instruction if peut également avoir une partie elseif, une partie else ou les deux, donc :
- if (x > 0)
- {
- printf ("positif");
- elseif (x < 0)
- printf ("negatif");
- else
- printf ("zéro");
- }
affiche le signe de x. (Mais vous devriez plutôt utiliser la fonction sign.)
Tant que seuls des scalaires sont impliqués, les opérateurs relationnels, d'égalité et logiques d'Algae ne vous surprendront probablement pas. Une expression vaut 1 si elle est vraie et 0 si elle est fausse. Les opérateurs relationnels sont >, >=, <, <=. Les opérateurs d'égalité sont == et !=. Les opérateurs logiques sont &, | et !. Deux opérateurs logiques supplémentaires, && et ||, sont spéciaux ; ils sont décrits plus loin.
Ces opérateurs peuvent vous surprendre lorsque des vecteurs et des matrices sont impliqués. Comme la plupart des opérateurs d'Algae, ils fonctionnent élément par élément. Par exemple, si A et B sont tous deux des matrices, alors l'expression A==B a plusieurs caractéristiques :
- A et B doivent avoir la même taille.
- Leurs étiquettes doivent correspondre.
- L'expression renvoie une matrice de même taille que A et B, dont chaque élément est soit 1 soit 0 selon que les éléments correspondants de A et B sont égaux.
Vous pouvez donc voir que A==B ne vous donne pas une simple réponse vraie ou fausse, mais plutôt une matrice de réponses.
L'instruction if, cependant, a besoin d'une simple réponse vraie ou fausse pour décider d'exécuter ou non ses instructions. Elle le fait en reconnaissant certaines entités comme fausses, toutes les autres étant vraies. Les entités «fausses» sont les suivantes :
- NULL.
- Entités numériques dans lesquelles chaque élément est zéro.
- Entités de caractères dans lesquelles chaque élément est "".
- Vecteurs et matrices sans éléments.
- Tables sans membres.
Ce qui met les nouveaux utilisateurs en difficulté, c'est une déclaration comme :
- if (A == B) { done = 1; }
Si A et B sont des matrices, alors A==B renvoie une matrice de uns et de zéros. L'instruction if interprète cela comme "vrai" si même l'un de ses éléments est différent de zéro. En d'autres termes, l'instruction ci-dessus commence par "Si un élément de A est égal à l'élément correspondant de B, alors ..." La difficulté est que l'opération "d'égalité" élément par élément n'est pas la même chose qu'un test d'égalité de deux tableaux. Si ce dernier test est ce que vous voulez vraiment, vous devriez alors utiliser la fonction equal à la place.
D'un autre côté, l'instruction :
- if (A != B) { done = 0; }
fonctionne dans les deux sens. L'expression A!=B renvoie une matrice ayant des éléments non nuls où les éléments correspondants de A et B sont inégaux. Ainsi, cette expression sert également de test de l'inégalité des deux tableaux.
Les opérateurs && ("et") et || ("ou") sont spéciaux de deux manières : ils n'effectuent pas d'opération élément par élément comme les autres opérateurs de cette section, et ils "court-circuitent" en sautant l'évaluation du deuxième opérande si le résultat est déjà établi par le premier opérande.
Chaque opérande de && et || est évalué pour "vérité" de la même manière que le test if. Pour &&, si le premier opérande est évalué à "faux", le deuxième opérande n'est pas évalué et le résultat de l'opération est 0. Pour ||, si le premier opérande est évalué à "vrai", le deuxième opérande n'est pas évalué et le résultat de l'opération est 1.
Par exemple, dans l'expression :
- x != NULL && x.type == "integer"
x est d'abord vérifié pour voir s'il est NULL. Si c'est le cas, alors le premier opérande de && est 0 et c'est aussi le résultat de l'expression entière. Dans ce cas, la référence membre x.type n'est jamais évaluée. C'est pratique, car sinon ce serait une erreur.
Une instruction if telle que :
- if (x < tol) { x = tol; }
pourrait plutôt s'écrire comme :
- x < tol && (x = tol);
Les parenthèses sont obligatoires dans la deuxième version, car la priorité de = est inférieure à celle de &&. Bien qu'elles accomplissent la même chose, la première version est recommandée ; elle est plus facile à lire et s'exécute un peu plus rapidement.
Algae possède deux instructions de contrôle de flux effectuant des boucles : while et for. L'instruction while exécute un ensemble d'instructions encore et encore, tant qu'une condition donnée est vraie. Par exemple, le code :
- a=0; b=1;
- while (b < 10000)
- {
- c = b;
- b = a+b;
- a = c;
- }
- c?
calcule et affiche le plus grand nombre de Fibonacci inférieur à 10 000. L'interpréteur vérifie que b<10 000, exécute les instructions du bloc while, puis répète. La première fois que l'expression b<10 000 est évaluée comme fausse, la boucle se termine.
L'instruction for provoque également une boucle, mais d'une manière différente. En supposant que v est un vecteur numérique, le code :
- for (i in 1:v.ne) { v[i] = 1 / v[i]; }
inverse chacun de ses membres. À l'intérieur des parenthèses, le mot clef in sépare un identifiant à gauche et une expression vectorielle à droite. Dans cet exemple, l'expression vectorielle est 1:v.ne qui contient les entiers de 1 à la longueur de v. La boucle for définit i égal au premier élément, 1, puis exécute l'instruction v[i]=1.0/v[i];. Ensuite, i est défini égal au deuxième élément et l'instruction est exécutée à nouveau. Ce cycle se répète jusqu'à ce que tous les éléments de 1:v.ne soient utilisés.
L'exemple précédent illustre également un sujet important concernant les boucles while et for. Essentiellement, les mêmes résultats seraient obtenus avec l'instruction v=1/v. Cela nécessite évidemment moins de saisie et est plus facile à lire. La différence vraiment importante, cependant, est qu'elle est beaucoup plus efficace. Avec la boucle for, toutes les opérations (affectation, division,...) sont effectuées par l'interpréteur. Bien qu'Algae soit rapide, il ne peut pas rivaliser avec le même travail en C, comme l'est v=1/v. Sur mon ordinateur, c'est environ 60 fois plus rapide.
Il est parfois pratique d'interrompre l'exécution des boucles while et for. L'instruction continue provoque le démarrage immédiat d'une autre itération de la boucle. Si nous voulions inverser les éléments non nuls d'un vecteur, nous pourrions écrire :
- for (i in 1:v.ne)
- {
- if (v[i] == 0) { continue; }
- v[i] = 1 / v[i];
- }
L'instruction break va encore plus loin, en quittant complètement la boucle. Par exemple, nous aurions pu écrire notre routine Fibonacci comme suit :
- a=0; b=1;
- while (1)
- {
- c = b;
- if ((b = a+b) > 10000) { break; }
- a = c;
- }
- c?
Les instructions continue et break affectent uniquement l'exécution de la boucle englobante la plus interne.
Il est important de noter que continue et break sont des instructions, pas des expressions. Le code :
- x < 0 && break;
n'est pas valide, car les opérandes de && doivent être des expressions.
L'instruction try est l'instruction de contrôle de flux restante. Elle est utilisée pour modifier la réponse d'Algae à quelque chose appelé une exception. Lorsqu'une exception se produit, nous disons qu'elle a été «déclenchée». De nombreuses conditions d'erreur (division par zéro, manque de mémoire,...) provoquent la levée d'une exception. Vous pouvez également déclencher une exception directement en appelant la fonction exception.
Lorsqu'une exception est déclenchée, l'action par défaut d'Algae est d'arrêter l'exécution. Si elle s'exécute de manière interactive, elle revient au prompt de ligne de commande ; sinon, elle se ferme complètement. Cette action peut être modifiée en utilisant l'instruction try. Si une exception est déclenchée dans le bloc try, l'exécution continue au point suivant immédiatement ce bloc. Par exemple, l'instruction :
- try { i += 1; }
incrémente i de un si possible. Si une erreur se produit (disons que i était NULL), alors Algae passe simplement à l'exécution de la ligne suivante. Bien que ce ne soit probablement pas une bonne idée, nous pourrions utiliser ceci à la place de break dans l'exemple de Fibonacci précédent :
- a=0; b=1;
- try
- {
- while (1)
- {
- c = b;
- if ((b = a+b) > 10000) { exception (); }
- a = c;
- }
- }
- c?
Il existe cependant une réelle différence entre ces approches. Dans la version utilisant l'instruction break, le résultat n'est affiché que lorsque la valeur correcte est atteinte. Dans la version try, le résultat est affiché quelle que soit la raison pour laquelle l'exception a été levée : si Algae recevait un signal d'interruption au milieu de la boucle, il sortirait du bloc try et afficherait alors une valeur incorrecte.
L'instruction try comporte une clause catch facultative, ressemblant à la clause else d'une instruction if. Toutes les instructions suivant catch ne sont exécutées que si une exception est levée dans la partie précédente du bloc try. De plus, les exceptions se produisant dans le bloc catch ne sont pas traitées spécialement. Par exemple :
- try
- {
- v = 1/v;
- catch
- message ("Je ne peux pas faire ça, Sylvain.");
- exception ();
- }
afficherait un message s'il avait des difficultés à effectuer l'inverse, puis arrêterait l'exécution. Si aucune exception ne se produit avant la clause catch, l'exécution continue après la fin de l'instruction try.
La syntaxe du langage de programmation ne permet pas de positionner une instruction break ou continue de telle sorte qu'elle fasse sortir l'exécution du bloc try.
Comme d'autres langages informatiques, Algae possède des fonctions. Certaines fonctions (comme sin et printf) sont intégrées, ce qui signifie qu'elles font partie du fichier exécutable d'Algae. D'autres, appelées fonctions utilisateur, sont celles écrites dans le langage de programmation Algae. Une fonction est appelée en donnant son nom suivi d'une liste de paramètres entre parenthèses. Les paramètres sont séparés par des points-virgules et leurs valeurs sont transmises à la fonction. Étant donné que seules les valeurs sont transmises, la fonction ne peut pas modifier les variables que vous lui transmettez.
Par exemple, supposons que vous appeliez la fonction shady :
- x = 1;
- y = shady (x);
La valeur renvoyée par shady est attribuée à y. Vous ne pouvez pas savoir en la regardant quelle classe d'entité (scalaire, matrice,...) shady renvoie. En fait, cela peut même changer d'un appel à l'autre. Rassurez-vous, la valeur de x est toujours 1, peu importe ce qui s'est passé dans shady.
Les fonctions d'Algae sont des entités, tout comme les scalaires et les matrices. Cela signifie que vous pouvez effectuer des opérations sur elles comme vous le faites avec d'autres entités. Par exemple, l'instruction :
- my_sin = sin
crée une nouvelle fonction appelée my_sin fonctionnant exactement comme la fonction sin d'origine. Bien sûr :
- sin = NULL;
supprime complètement la fonction sin, ce qui n'est probablement pas une très bonne idée dans la plupart des cas.
Les fonctions peuvent être définies lors d'une session interactive ou simplement incluses à partir de fichiers. Par exemple, envisagez d'écrire une fonction appelée «findit» recherchant une valeur donnée dans les éléments d'un vecteur et renverra les emplacements où elle l'a trouvée. La fonction suivante devrait faire l'affaire :
- findit = function (s; v)
- {
- local (w; i);
- w = vector ();
- for (i in 1:v.ne)
- {
- if (v[i] == s) { w = w, i; }
- }
- return w;
- }
(La fonction intégrée find fait ce travail beaucoup plus efficacement.) L'instruction locale déclare ses arguments comme ayant une portée locale. Par exemple, l'affectation à w n'aurait aucun effet en dehors de cette fonction. Sans la déclaration locale, l'affectation modifierait la valeur de w globalement.
L'instruction return provoque l'arrêt de l'exécution de la fonction et transmet son expression comme valeur de retour de la fonction. Les appels récursifs à une fonction ne posent aucun problème. Par exemple, nous pourrions écrire une fonction pour calculer des factorielles comme suit :
- fact = function (n)
- {
- if (n < 2) { return 1.0; else return n * fact (n-1); }
- }
-
Cette fonction a un léger problème. (Plusieurs, en fait, si l'on considère qu'elle ne vérifie pas les erreurs.) Si nous décidons plus tard de changer son nom en tapant factorial=fact, elle appelle toujours la fonction fact en interne. Maintenant, si nous sommes vraiment méchants, nous pouvons changer fact comme dans fact=sin ; maintenant factorial donne de mauvaises réponses. La façon de gérer cela est d'appeler la fonction self lorsque vous faites un appel de fonction récursif, comme dans :
- fact = function (n)
- {
- if (n < 2) { return 1.0; else return n * self (n-1); }
- }
Le mot clef self fait référence à la fonction en cours. Outre les appels de fonction récursifs, il est également utile pour conserver les données locales à une fonction. Par exemple, considérons une fonction renvoyant la «forme» de son paramètre :
- shape = function (x)
- {
- return self.(class(x)) (x);
- };
-
- shape.scalar = function (x) { return NULL; };
- shape.vector = function (x) { return x.ne; };
- shape.matrix = function (x) { return x.nr, x.nc; };
Cette fonction de forme détermine la classe de son paramètre, puis appelle la fonction membre appropriée. (La fonction de forme standard fournit en outre une vérification des erreurs.)