Think Julia

| Ben Lauwens | Allen B. Downey |

Traducteur

Thierry Lepoint

« Think Julia — Comment penser comme un informaticien » est la traduction en français du livre ThinkJulia — How to Think Like a Computer scientist de Ben Lauwens et Allen B. Downey (publié en 2018). Il s'agit d'un livre d'introduction au langage Julia.

Commentez .

- Titre : Think Julia

- Auteurs : Ben Lauwens, Allen B. Downey

- Traducteur : Thierry Lepoint

- Parution : 10 juin 2021

- Licence : Le contenu de cet article est rédigé par Thierry Lepoint et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- Droits associés à la version française

- Préface de l'édition française

- Traductions en espagnol et en portugais

- Remerciements

- Remerciements Developpez.com

- Copyright

- Dédicace

- Préface

- Pourquoi choisir Julia ?

- À qui ce livre s'adresse-t-il ?

- Conventions typographiques

- Utilisation des exemples de code

- Remerciements

- Liste des contributeurs

- 1. Mode de fonctionnement d'un programme

- 1-1. Qu'est-ce qu'un programme ?

- 1-2. Exécution de Julia

- 1-3. Un premier programme

- 1-4. Opérateurs arithmétiques

- 1-5. Valeurs et types

- 1-6. Langages formels et naturels

- 1-7. Débogage

- 1-8. Glossaire

- 1-9. Exercices

- 1-9-1. Exercice 1-1

- 1-9-2. Exercice 1-2

- 2. Variables, expressions et déclarations

- 2-1. Déclaration d'affectation

- 2-2. Noms de variables

- 2-3. Expressions et déclarations

- 2-4. Mode « script »

- 2-4-1. Exercice 2-1

- 2-5. Priorité des opérateurs

- 2-6. Opérations sur les chaînes de caractères

- 2-7. Commentaires

- 2-8. Débogage

- 2-9. Glossaire

- 2-10. Exercices

- 2-10-1. Exercice 2-3

- 2-10-2. Exercice 2-3

- 3. Fonctions

- 3-1. Appel de fonction

- 3-2. Fonctions mathématiques

- 3-3. Composition

- 3-4. Ajout de nouvelles fonctions

- 3-5. Définition et usages

- 3-5-1. Exercice 3-1

- 3-6. Flux d'exécution

- 3-7. Paramètres et arguments

- 3-8. Les variables et les paramètres sont locaux

- 3-9. Diagrammes de pile

- 3-10. Fonction avec retour et fonction vide

- 3-11. Pourquoi utiliser des fonctions ?

- 3-12. Débogage

- 3-13. Glossaire

- 3-14. Exercices

- 3-14-1. Exercice 3-2

- 3-14-2. Exercice 3-3

- 3-14-3. Exercice 3-4

- 4. Étude de cas : Conception d'une interface

- 4-1. Turtles

- 4-1-1. Exercice 4-1

- 4-2. Répétitions simples

- 4-3. Exercices

- 4-3-1. Exercice 4-2

- 4-3-2. Exercice 4-3

- 4-3-3. Exercice 4-4

- 4-3-4. Exercice 4-5

- 4-3-5. Exercice 4-6

- 4-3-6. Exercice 4-7

- 4-4. Encapsulation

- 4-5. Généralisation

- 4-6. Conception d'une interface

- 4-7. Refonte (ou refactoring)

- 4-8. Plan de développement

- 4-9. Documentation interne

- 4-10. Débogage

- 4-11. Glossaire

- 4-12. Exercices

- 4-12-1. Exercice 4-8

- 4-12-2. Exercice 4-9

- 4-12-3. Exercice 4-10

- 4-12-4. Exercice 4-11

- 4-12-5. Exercice 4-12

- 5. Conditions et récursion

- 5-1. Division euclidienne (entière) et modulo

- 5-2. Expressions booléennes

- 5-3. Opérateurs logiques

- 5-4. Exécution conditionnelle

- 5-5. Exécution alternative

- 5-6. Enchaînement de conditions

- 5-7. Imbrication de conditions

- 5-8. Récursion

- 5-9. Diagrammes de pile pour les fonctions récursives

- 5-9-1. Exercice 5-1

- 5-10. Récursion infinie

- 5-11. Saisie au clavier

- 5-12. Débogage

- 5-13. Glossaire

- 5-14. Exercices

- 5-14-1. Exercice 5-2

- 5-14-2. Exercice 5-3

- 5-14-3. Exercice 5-4

- 5-14-4. Exercice 5-5

- 5-14-5. Exercice 5-6

- 6. Fonctions avec retour

- 6-1. Valeurs retournées

- 6-1-1. Exercice 6-1

- 6-2. Développement progressif

- 6-2-1. Exercice 6-2

- 6-3. Composition

- 6-4. Fonctions booléennes

- 6-4-1. Exercice 6-3

- 6-5. Davantage de récursion

- 6-6. Un acte de confiance

- 6-7. Un exemple supplémentaire

- 6-8. Types et vérification

- 6-9. Débogage

- 6-10. Glossaire

- 6-11. Exercices

- 6-11-1. Exercice 6-4

- 6-11-2. Exercice 6-5

- 6-11-3. Exercice 6-6

- 6-11-4. Exercice 6-7

- 6-11-5. Exercice 6-8

- 7. Itération

- 7-1. Réaffectation

- 7-2. Mise à jour des variables

- 7-3. while

- 7-3-1. Exercice 7-1

- 7-4. break

- 7-5. continue

- 7-6. Racines carrées

- 7-7. Algorithmes

- 7-8. Débogage

- 7-9. Glossaire

- 7-10. Exercices

- 7-10-1. Exercice 7-2

- 7-10-2. Exercice 7-3

- 7-10-3. Exercice 7-4

- 8. Chaînes

- 8-1. Caractères

- 8-2. Une chaîne est une séquence

- 8-3. length

- 8-4. Parcours d'une chaîne

- 8-4-1. Exercice 8-1

- 8-4-2. Exercice 8-2

- 8-5. Segments de chaînes

- 8-5-1. Exercice 8-3

- 8-6. Les chaînes sont persistantes

- 8-7. Interpolation des chaînes

- 8-8. Recherche dans les chaînes

- 8-8-1. Exercice 8-4

- 8-9. Boucle et compteur

- 8-9-1. Exercice 8-5

- 8-10. Bibliothèque des chaînes

- 8-11. L'opérateur ∈

- 8-12. Comparaison de chaînes

- 8-13. Débogage

- 8-13-1. Exercice 8-6

- 8-14. Glossaire

- 8-15. Exercices

- 8-15-1. Exercice 8-7

- 8-15-2. Exercice 8-8

- 8-15-3. Exercice 8-9

- 8-15-4. Exercice 8-10

- 8-15-5. Exercice 8-11

- 9. Étude de cas : jeux de mots

- 9-1. Lecture de listes de mots

- 9-2. Exercices

- 9-2-1. Exercice 9-1

- 9-2-2. Exercice 9-2

- 9-2-3. Exercice 9-3

- 9-2-4. Exercice 9-4

- 9-2-5. Exercice 9-5

- 9-2-6. Exercice 9-6

- 9-3. Recherche

- 9-4. Boucles sur les indices

- 9-5. Débogage

- 9-6. Glossaire

- 9-7. Exercices 9-7

- 9-7-1. Exercice

- 9-7-2. Exercice 9-8

- 9-7-3. Exercice 9-9

- 10. Tableaux

- 10-1. Un tableau est une séquence

- 10-2. Les tableaux sont non persistants

- 10-3. Parcourir un tableau

- 10-4. Segments de tableau

- 10-5. Bibliothèque de fonctions associées aux tableaux

- 10-6. Mise en correspondance (mapping), filtre et réduction

- 10-7. Syntaxe avec points

- 10-8. Suppression et insertion d'éléments

- 10-9. Tableaux et chaînes

- 10-10. Objets et valeurs

- 10-11. Aliasing

- 10-12. Tableaux en tant qu'argument

- 10-13. Débogage

- 10-14. Glossaire

- 10-15. Exercices

- 10-15-1. Exercice 10-1

- 10-15-2. Exercice 10-2

- 10-15-3. Exercice 10-3

- 10-15-4. Exercice 10-4

- 10-15-5. Exercice 10-5

- 10-15-6. Exercice 10-6

- 10-15-7. Exercice 10-7

- 10-15-8. Exercice 10-8

- 10-15-9. Exercice 10-9

- 10-15-10. Exercice 10-10

- 10-15-11. Exercice 10-11

- 10-15-12. Exercice 10-12

- 11. Dictionnaires

- 11-1. Dictionnaire et « mise en correspondance » (mapping)

- 11-2. Dictionnaires en tant que collections de compteurs

- 11-2-1. Exercice 11-1

- 11-3. Boucles et dictionnaires

- 11-4. Recherche inverse

- 11-5. Dictionnaires et tableaux

- 11-6. Mémos

- 11-7. Variables globales

- 11-8. Débogage

- 11-9. Glossaire

- 11-10. Exercices

- 11-10-1. Exercice 11-2

- 11-10-2. Exercice 11-3

- 11-10-3. Exercice 11-4

- 11-10-4. Exercice 11-5

- 11-10-5. Exercice 11-6

- 11-10-6. Exercice 11-7

- 12. Tuples

- 12-1. Les tuples sont persistants

- 12-2. Affectation des tuples

- 12-3. Les tuples en tant que valeurs retournées

- 12-4. Tuples et arguments multiples

- 12-4-1. Exercice 12-1

- 12-5. Tableaux et tuples

- 12-6. Dictionnaires et tuples

- 12-7. Séquences de séquences

- 12-8. Débogage

- 12-9. Glossaire

- 12-10. Exercices

- 12-10-1. Exercice 12-2

- 12-10-2. Exercice 12-3

- 12-10-3. Exercice 12-4

- 12-10-4. Exercice 12-5

- 13. Étude de cas : structures de données — choix

- 13-1. Analyse de la fréquence des mots

- 13-1-1. Exercice 13-1

- 13-1-2. Exercice 13-2

- 13-1-3. Exercice 13-3

- 13-1-4. Exercice 13-4

- 13-2. Nombres aléatoires

- 13-2-1. Exercice 13-5

- 13-3. Histogramme des mots

- 13-4. Mots les plus communs

- 13-5. Paramètres optionnels

- 13-6. Soustraction de dictionnaires

- 13-6-1. Exercice 13-6

- 13-7. Mots aléatoires

- 13-7-1. Exercice 13-7

- 13-8. Analyse de Markov

- 13-8-1. Exercice 13-8

- 13-9. Structure de données

- 13-10. Débogage

- 13-11. Glossaire

- 13-12. Exercices

- 13-12-1. Exercice 13-9

- 14. Fichiers

- 14-1. Persistance

- 14-2. Lire et écrire

- 14-3. Formatage

- 14-4. Noms de fichiers et chemins

- 14-5. Levée des exceptions

- 14-6. Bases de données

- 14-7. Sérialisation

- 14-8. Objets de commande

- 14-9. Modules

- 14-9-1. Exercice 14-1

- 14-10. Débogage

- 14-11. Glossaire

- 14-12. Exercices

- 14-12-1. Exercice 14-2

- 14-12-2. Exercice 14-3

- 14-12-3. Exercice 14-4

- 15. Structures et objets

- 15-1. Types composites

- 15-2. Les structures sont persistantes

- 15-3. Structures non persistantes

- 15-4. Rectangles

- 15-5. Instances en argument

- 15-5-1. Exercice 15-1

- 15-6. Instances en tant que valeurs retournées

- 15-7. Copies

- 15-7-1. Exercice 15-2

- 15-8. Débogage

- 15-9. Glossaire

- 15-10. Exercices

- 15-10-1. Exercice 15-3

- 15-10-2. Exercice 15-4

- 16. Structures et fonctions

- 16-1. Heures, minutes et secondes

- 16-1-1. Exercice 16-1

- 16-1-2. Exercice 16-2

- 16-2. Fonctions pures

- 16-3. Modificateurs

- 16-3-1. Exercice 16-3

- 16-3-2. Exercice 16-4

- 16-4. Prototypage ou planification ?

- 16-4-1. Exercice 16-5

- 16-5. Débogage

- 16-6. Glossaire

- 16-7. Exercices

- 16-7-1. Exercice 16-6

- 16-7-2. Exercice 16-7

- 17. Dispatch multiple

- 17-1. Déclarations de types

- 17-2. Méthodes

- 17-2-1. Exercice 17-1

- 17-3. Exemples supplémentaires

- 17-4. Constructeurs

- 17-5. show

- 17-5-1. Exercice 17-2

- 17-6. Surcharge d'opérateurs

- 17-7. Dispatch multiple

- 17-7-1. Exercice 17-3

- 17-8. Programmation générique (généricité)

- 17-9. Interface et implémentation

- 17-10. Débogage

- 17-11. Glossaire

- 17-12. Exercices

- 17-12-1. Exercice 17-4

- 17-12-2. Exercice 17-5

- 18. Sous-typage

- 18-1. Cartes

- 18-2. Variables globales

- 18-3. Comparaison de cartes

- 18-3-1. Exercice 18-1

- 18-4. Tests unitaires

- 18-5. Paquets de cartes

- 18-6. Ajouter, supprimer, mélanger et trier

- 18-6-1. Exercice 18-2

- 18-7. Types abstraits et sous-typage

- 18-8. Types abstraits et fonctions

- 18-9. Diagrammes de types

- 18-10. Débogage

- 18-11. Encapsulation de données

- 18-11-1. Exercice 18-3

- 18-12. Glossaire

- 18-13. Exercices

- 18-13-1. Exercice 18-4

- 18-13-2. Exercice 18-5

- 18-13-3. Exercice 18-6

- 19. Bonus : à propos de la syntaxe

- 19-1. Tuples nommés

- 19-2. Fonctions

- 19-2-1. Fonctions anonymes

- 19-2-2. Arguments nommés

- 19-2-3. Fermetures

- 19-3. Blocs

- 19-3-1. Bloc let

- 19-3-2. Blocs do

- 19-4. Structure de contrôle

- 19-4-1. Opérateur ternaire

- 19-4-2. Évaluation en court-circuit

- 19-4-3. Tâches ou coroutines

- 19-5. Types

- 19-5-1. Types primitifs

- 19-5-2. Types paramétriques

- 19-5-3. Unions de types

- 19-6. Méthodes

- 19-6-1. Méthodes paramétriques

- 19-6-2. Objets foncteurs

- 19-7. Constructeurs

- 19-8. Conversions et promotions

- 19-8-1. Conversion

- 19-8-2. Promotion

- 19-9. Métaprogrammation

- 19-9-1. Expressions

- 19-9-1-1. eval

- 19-9-2. Macros

- 19-9-3. Fonctions générées

- 19-10. Valeurs manquantes

- 19-11. Appel de code C et Fortran

- 19-12. Glossaire

- 20. Bonus : bibliothèque de base et standard

- 20-1. Mesures de performance

- 20-2. Collections et structures de données

- 20-2-1. Exercice 20-1

- 20-3. Mathématiques

- 20-4. Chaînes

- 20-5. Tableaux

- 20-6. Interfaces

- 20-7. Utilitaires interactifs

- 20-8. Débogage

- 20-9. Glossaire

- 21. Débogage

- 21-1. Erreurs de syntaxe

- 21-1-1. Je continue à faire des changements, mais sans effet

- 21-2. Erreurs d'exécution

- 21-2-1. Mon programme ne fait absolument rien

- 21-2-2. Mon programme est « suspendu »

- 21-2-2-1. Boucle infinie

- 21-2-2-2. Récursion infinie

- 21-2-2-3. Flux d'exécution

- 21-2-3. Quand j'exécute mon programme, j'obtiens une exception

- 21-2-4. J'ai ajouté beaucoup de déclarations d'affichage ; je suis inondé de sorties

- 21-3. Erreurs sémantiques

- 21-3-1. Mon programme ne fonctionne pas

- 21-3-2. J'ai une expression embroussaillée qui ne fait pas ce que j'attends

- 21-3-3. J'ai une fonction qui ne retourne pas ce que j'attends

- 21-3-4. Je suis vraiment, vraiment coincé et j'ai besoin d'aide

- 21-3-5. Non, j'ai vraiment besoin d'aide

- Annexe A : entrées Unicode

- Annexe B : Installation de Julia

- Situation au 1er janvier 2021

- Julia en mode local

- Installation de binaires sous Ubuntu

- Installation de Visual Studio Codium

- Installation de modules

- Ancienne méthode

- Nouvelle méthode

- Carnets de travail : Jupyter et Pluto

- Jupyter

- Pluto

- Julia en nuage

- Aide et documentation

- Annexe C : notes de traduction

- Notes de bas de page

- Modifications du texte

- Modifications des exemples de programmes

- Ajouts de figures

- Modifications de tableaux

- Annexe C : Notes de traduction

- Bibliographie

- Index des fonctions extrinsèques

- Index

Droits associés à la version française

En vertu de la licence Creative Commons (CC BY-NC-SA 3.0 FR ; Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 FR), vous êtes autorisé à partager (copier, distribuer et communiquer le matériel par tous moyens et sous tous formats) et adapter (remixer, transformer et créer à partir du matériel) selon les conditions suivantes :

- Attribution — Vous devez créditer le document, intégrer un lien vers la licence (en l'occurrence : CC BY-NC-SA 3.0 FR) et indiquer si des modifications ont été effectuées au document. Vous devez indiquer ces informations par tous les moyens raisonnables, sans toutefois suggérer que le présent auteur vous soutient ou soutient la façon dont vous avez utilisé son travail.

- Pas d’utilisation commerciale — Vous n'êtes pas autorisé à faire un usage commercial du présent document, tout ou partie du matériel le composant.

- Partage dans les mêmes conditions — Dans le cas où vous effectuez un remix, que vous transformez, ou créez à partir du matériel composant le document original, vous devez diffuser le document modifié dans les mêmes conditions, c'est-à-dire avec la même licence avec laquelle le document original a été diffusé.

Vous n'êtes pas autorisé à appliquer des conditions légales ou des mesures techniques qui restreindraient légalement autrui à utiliser le document dans les conditions décrites par la licence.

Préface de l'édition française

Initialement publié en 2012 par Alan Edelman, Stefan Karpinski, Jeff Bezanson et Viral B. Shah, Julia est un langage de programmation libre (licence MIT) dont les prémices datent de 2009. Le code est arrivé à maturité en 2018 avec la version 1.0. Cependant, c'est seulement durant la période de confinement associée à la covid-19, au début mars 2020, que j'ai été véritablement attiré par Julia du fait qu'il s'agit d'un langage dont la syntaxe, aisée à comprendre, conduit rapidement, entre autres, à la résolution de problèmes scientifiques (mais aussi économiques et financiers), à l'apprentissage machine, à la manipulation de données de masse et à l'intelligence artificielle.

Au tout début mars, j'ai donc cherché un livre permettant d'apprendre le langage Julia. Parmi plusieurs ouvrages [1-5], l'un m'a séduit par sa qualité, sa rigueur et la progressivité de l'apprentissage sans s'encombrer de détails inutiles : Think Julia — How to Think Like a Computer Scientist de Ben Lauwens et Allen B. Downey [1]. Comme Julia demande à être diffusé aussi largement que possible compte tenu de ses qualités d'une part et que de l'autre il n'existe pas de livre en français pour « débutant », j'ai rapidement réalisé que s'imposait une traduction de Think Julia — How to Think Like a Computer Scientist — lui-même « portage » de Think Python — How to Think Like a Computer Scientist d'Allen B. Downey. Quelques aménagements ont été apportés ; ils sont mentionnés dans l'annexe C.

Traductions en espagnol et en portugais

Le lecteur trouvera également, sous ce lien, une traduction en espagnol réalisée par Pamela Alejandra Bustamante Faúndez (Ph.D. à la Pontificia Universidad Católica de Chile) ainsi que, sous ce lien, une traduction en portugais effectuée par Abel Soares Siqueira, Gustavo Sarturi, João Okimoto, Kally Chung. Abel Soares Siqueira est professeur à l'Université fédérale du Paraná (UFPR), située à Curitiba, Brésil. Gustavo Sarturi est titulaire d'une licence en mathématiques industrielles de l'Universidade Federal do Paraná (UFPR). João Okimoto est étudiant en informatique à l'UFPR et Kally Chung y est professeur d'ingénierie mécanique.

La coordination des traductions est assurée par Christian Peel.

Remerciements

Ma gratitude s'adresse aux développeurs du langage Julia travaillant inlassablement à l'efficacité et au rayonnement du langage ainsi qu'aux développeurs de la Free Software Foundation qui ont contribué aux logiciels , , , Inkscape et Gimp ainsi qu'aux fontes Times Roman et TeX Gyre DejaVu Math, sans lesquels ce document n'aurait pu être réalisé.

Enfin, mes plus vifs remerciements vont aux relecteurs et correcteurs aussi avisés que scrupuleux : Maryse Debrichy et Claude Just.

Élouges, le 13 janvier 2021.

Thierry Lepoint
Site web : Chimie et Julia

Remerciements Developpez.com

Nous tenons à remercier Thierry Lepoint de nous avoir autorisés à publier sur Developpez.com son travail de traduction de ce livre.

Copyright

Copyright © 2018 Allen Downey, Ben Lauwens. Tous droits réservés.

Think Julia est disponible sous la licence Creative Commons Attribution-NonCommercial 3.0 Unported License(1). Les auteurs maintiennent une version en ligne à l'adresse ThinkJulia.jl/latest/book.html.

Ben Lauwens est professeur de mathématiques à l'École royale militaire - Koninklijke Militaire School (RMA Belgium). Il est titulaire d'un doctorat en ingénierie et d'une maîtrise de la KU Leuven et de la RMA, ainsi que d'une licence de la RMA.

Allen B. Downey est professeur d'informatique à l'Olin College of Engineering. Il a enseigné au Wellesley College, au Colby College et à l'U.C. Berkeley. Il est titulaire d'un doctorat en informatique de l'U.C. Berkeley et d'un master et d'une licence du MIT.

Une version papier de ce livre est publiée par O'Reilly Media et peut être achetée sur Amazon.

Dédicace

Pour Emeline, Arnaud et Tibo.

Préface

Ben Lauwens : « En janvier 2018, j'ai commencé la préparation d'un cours de programmation destiné aux étudiants sans expérience en programmation. Je voulais utiliser Julia, mais j'ai découvert qu'il n'existait aucun livre ayant pour but d'apprendre à programmer avec Julia comme premier langage de programmation. Il existe de merveilleux tutoriels qui expliquent les concepts-clés de Julia, mais aucun d'entre eux n'accorde suffisamment d'attention à l'apprentissage concernant la façon de penser comme un programmeur.

Je connaissais le livre « Think Python » d'Allen Downey, qui contient tous les ingrédients-clés pour apprendre à programmer correctement. Cependant, ce livre était basé sur le langage de programmation Python. Ma première ébauche de notes de cours était un melting-pot de toutes sortes d'ouvrages de référence, mais, plus j'y travaillais, plus le contenu commençait à ressembler aux chapitres de « Think Python ». Bientôt, l'idée de développer mes notes de cours comme un portage de ce livre pour Julia s'est concrétisée.

Tout le matériel était disponible sous forme de carnets Jupyter dans un dépôt GitHub. Après avoir posté un message sur le site Julia Discourse concernant les progrès de mon cours, les réactions se sont avérées très positives. Un livre sur les concepts de base de la programmation avec Julia comme premier langage de programmation était apparemment un chaînon manquant dans l'univers de Julia. J'ai contacté Allen pour lui demander si je pouvais lancer un portage officiel de « Think Python » vers Julia et sa réponse a été immédiate : « Allez-y ! » Il m'a mis en contact avec son éditeur chez O'Reilly Media et, un an plus tard, je mettais la dernière main à ce livre.

C'était un parcours semé d'embûches. En août 2018, Julia 1.0 est sorti et, comme tous mes collègues programmeurs de Julia, j'ai dû procéder à une migration du code. Tous les exemples du livre ont été testés lors de la conversion des fichiers source en fichiers AsciiDoc compatibles avec O'Reilly. La chaîne d'outils et le code des exemples ont dû être rendus conformes à la version 1.0 de Julia. Heureusement, il n'y a pas de conférence à donner en août…

J'espère que ce livre vous plaira et qu'il vous aidera à apprendre à programmer et à penser comme un informaticien, au moins un peu. »

Pourquoi choisir Julia ?

Julia a été initialement publié en 2012 par Alan Edelman, Stefan Karpinski, Jeff Bezanson et Viral B. Shah. Il s'agit d'un langage de programmation libre et open source (sous licence MIT).

Le choix d'un langage de programmation est toujours subjectif. Les caractéristiques suivantes de Julia sont déterminantes :

- Julia est développé comme un langage de programmation performant ;

- Julia utilise le dispatch multiple, ce qui permet au programmeur de choisir parmi différents modèles de programmation adaptés à l'application ;

- Julia est un langage à typage dynamique qui peut facilement être utilisé de manière interactive ;

- Julia a une jolie syntaxe de haut niveau, facile à apprendre ;

- Julia est un langage de programmation à typage optionnel dont les types de données (définis par l'utilisateur) rendent le code plus clair et plus robuste ;

- Julia dispose d'une bibliothèque standard étendue et de nombreux paquets tiers sont disponibles.

Julia est un langage de programmation unique, car il résout le problème des « deux langages ». Aucun autre langage de programmation n'est nécessaire pour écrire un code performant. Cela ne signifie pas que cela se fait automatiquement. Il incombe au programmeur d'optimiser le code qui provoque un ralentissement à l'exécution, mais cela peut se faire dans Julia même.

À qui ce livre s'adresse-t-il ?

Ce livre est destiné à tous ceux qui veulent apprendre à programmer. Aucune connaissance préalable formelle n'est requise.

De nouveaux concepts sont introduits progressivement et des sujets plus avancés sont décrits dans les chapitres suivants.

Think Julia peut être utilisé pour un cours d'un semestre au niveau lycée ou université.

Conventions typographiques

italique les nouveaux termes ou des notions-clés sont indiqués en italique. Lorsqu'un terme anglais est repris, il est indiqué en italique.(2)

renvois les renvois à des chapitres, sections, sous-sections (voire sous-sous-sections), les renvois aux notes de bas de page et à la bibliographie ainsi que les liens hypertextes sont en gris et cliquables (sur ordinateur à l'aide de la souris [symbole ]).

mots chasse fixe petit fonte utilisée pour le code des programmes.

mots chasse fixe bleu fonte faisant référence dans le texte aux éléments des programme tels que les noms de variables ou de fonctions, les bases de données, les types de données, les variables d'environnement, les déclarations et les mots-clés.

Plusieurs éléments sont indiqués pour attirer l'attention du lecteur :

Cette indication donne un conseil ou une suggestion.

Cette mention apporte une information générale.

Cette information signale une mise en garde ou une situation où la prudence s'impose.

Utilisation des exemples de code

Tout le code utilisé dans ce livre est disponible dans un dépôt Git sur GitHub : BenLauwens/ThinkJulia.jl. Git est un système de contrôle de version qui permet de garder une trace des fichiers qui composent un projet. Une collection de fichiers sous le contrôle de Git est appelée un « dépôt ». GitHub est un service d'hébergement qui fournit un stockage pour les dépôts Git et une interface web pratique.(3)

Compte tenu de modifications récentes (postérieures à la publication de Think Julia [10]) concernant directement l'environnement de développement intégré de Julia (Atom/Juno) et de l'abandon de JuliaBox, le traducteur se permet de renvoyer le lecteur à l'annexe B pour l'installation de Julia, de ses modules, de Visual Studio Codium et l'usage de carnets (notebooks) d'utilisation sur le web ou en mode local.

Remerciements

Je tiens à remercier Allen B. Downey d'avoir rédigé Think Python, How to Think Like a Computer Scientist et de m'avoir permis d'effectuer un portage de son livre en Julia. Son enthousiasme est contagieux !

J'aimerais également remercier les relecteurs/correcteurs de ce livre, qui ont fait de nombreuses suggestions utiles : Tim Besard, Bart Janssens et David P. Sanders.

Merci à Melissa Potter de O'Reilly Media, qui a contribué à améliorer ce livre. Vous m'avez forcé à faire les choses correctement et à rendre ce livre aussi original que possible.

Merci à Matt Hacker de O'Reilly Media, qui m'a aidé avec la chaîne d'outils Atlas et qui a résolu quelques problèmes de syntaxe.

Merci à tous les étudiants ayant travaillé avec une première version de ce livre et à tous les contributeurs (cités ci-dessous) qui ont envoyé des corrections et des suggestions.

Liste des contributeurs

Si vous avez une suggestion ou une correction, veuillez envoyer un courriel à Ben Lauwens ou ouvrir un numéro sur GitHub. Si je fais une modification sur la base de vos commentaires, je vous ajouterai à la liste des contributeurs (sauf si vous demandez à être omis).

Faites-moi savoir avec quelle version du livre vous travaillez et avec quel format. Si vous incluez au moins une partie de la phrase dans laquelle l'erreur apparaît, cela facilitera la recherche. Les numéros de page et de section sont également utiles quoique peu aisés à utiliser.

- Scott Jones a signalé le changement de nom de Void en Nothing, ce qui a lancé la migration vers Julia 1.0.

- Robin Deits a trouvé quelques fautes de frappe dans le chapitre 2 (Variables, expressions et déclarations).

- Mark Schmitz a suggéré d'activer la mise en évidence syntaxique.

- Zigu Zhao a détecté des bogues dans le chapitre 8 (Chaînes).

- Oleg Soloviev a détecté une erreur dans l'URL d'ajout du paquet ThinkJulia (chapitre 4).

- Aaron Ang a trouvé des problèmes de rendu et de dénomination.

- Sergey Volkov a détecté un lien brisé dans le chapitre 7 (Itération).

- Sean McAllister a suggéré de mentionner l'excellent paquet BenchmarkTools.

- Carlos Bolech a envoyé une longue liste de corrections et de suggestions.

- Krishna Kumar a corrigé l'exemple de Markov dans le chapitre 18 (Sous-typage).

1 - Mode de fonctionnement d'un programme

Le but de ce livre est d'apprendre au lecteur à penser comme un informaticien. Cette façon de penser combine certaines des meilleures caractéristiques des mathématiques, de l'ingénierie et des sciences naturelles. Comme les mathématiciens, les informaticiens utilisent des langages formels pour expliciter des idées (en particulier les calculs). Tout comme les ingénieurs, ils conçoivent des structures, assemblent des composants dans divers systèmes et évaluent le meilleur compromis entre différentes possibilités. Comme les scientifiques, ils observent le comportement de systèmes complexes, formulent des hypothèses et vérifient leurs prédictions.

La compétence la plus importante pour un informaticien est la résolution de problèmes, c'est-à-dire la capacité à formuler ces problèmes, à penser de manière créative aux solutions et à exprimer la meilleure solution de manière claire et précise. Il s'avère que le processus d'apprentissage de la programmation est une excellente occasion de mettre en pratique les compétences nécessaires à la résolution de problèmes. C'est pourquoi ce chapitre s'intitule « Mode de fonctionnement d'un programme ».

À un certain niveau, le lecteur apprendra à programmer, une compétence utile en soi. À un autre niveau, la programmation pourra être exploitée comme un moyen d'atteindre un but. Au fur et à mesure, cette fin deviendra plus claire.

1-1 - Qu'est-ce qu'un programme ?

Un programme est une séquence d'instructions qui établit la manière d'effectuer un calcul. Le calcul peut être mathématique, comme la résolution d'un système d'équations ou la recherche des racines d'un polynôme, mais il peut aussi être symbolique, comme la recherche et le remplacement de texte dans un document, ou encore graphique, comme le traitement d'une image ou la lecture d'une vidéo.

Les détails sont différents selon les langages, mais quelques instructions élémentaires apparaissent dans presque chaque langage :

Données en entrée : il s'agit d'incorporer des données en provenance du clavier, d'un fichier, du réseau ou d'un autre appareil ;

Sortie : il s'agit d'afficher les données à l'écran, les enregistrer dans un fichier, les envoyer sur le réseau, etc. ;

Mathématiques : il s'agit d'effectuer des opérations mathématiques de base comme l'addition, la multiplication, etc. ;

Exécution conditionnelle : il s'agit de vérifier certaines conditions et, en conséquence, de lancer le code approprié ;

Répétition : il s'agit d'effectuer une action de manière répétée, généralement avec une variation donnée.

C'est à peu près tout. Chaque programme que nous avons utilisé, aussi complexe soit-il, est composé d'instructions qui ressemblent à ce qui est décrit précédemment. Nous considérerons donc la programmation comme le processus de décomposition d'une tâche ample et complexe en sous-tâches de plus en plus petites, jusqu'à ce que les sous-tâches soient suffisamment simples pour être exécutées à l'aide de ces instructions de base.

1-2 - Exécution de Julia

Après avoir lancé Visual Studio Codium (voir l'annexe B), nous arrivons sur une page telle que celle de la figure 1.2.1.

Le REPL (Read-Eval-Print Loop) de Julia est un programme qui lit et exécute le code Julia. Le REPL est exécutable en ouvrant un terminal sur JuliaBox et en saisissant julia sur la ligne de commande. Quand le REPL démarre, nous devrions voir l'information telle que reprise dans la figure 1.2.1.

Les premières lignes contiennent des informations sur le REPL, il se peut donc que ce soit un peu différent. Il faut vérifier que le numéro de version est au moins 1.0.0.

La dernière ligne est une invite qui indique que le REPL est prêt pour que nous puissions saisir du code.

FIGURE 1.2.1 – REPL  et invite de saisie (julia>).

Si nous saisissons une ligne de code et appuyons sur ENTER, le REPL affiche le résultat :

1. julia> 1 + 1

2. 2

1-3 - Un premier programme

Traditionnellement, le premier programme à écrire dans un nouveau langage informatique s'appelle « Hello, World! », bien qu'il n'affiche que l'expression Hello, World!. Avec Julia, le code se présente comme ceci :

1. julia> println("Hello, World!")

2. Hello, World!

Il s'agit d'un exemple d'instruction permettant un affichage (à l'écran) avec retour à la ligne. Les guillemets droits marquent le début et la fin du texte à afficher ; ils n'apparaissent pas dans le résultat. Les parenthèses indiquent que println est une fonction (voir le chapitre 3).

1-4 - Opérateurs arithmétiques

L'étape suivante concerne l'arithmétique. Julia fournit des opérateurs de multiplication, de division, d'addition et de soustraction. Les opérateurs +, - et * effectuent des additions, des soustractions et des multiplications, comme dans les exemples suivants :

1. julia> 40 + 2

2. 42

3. julia> 43 - 1

4. 42

5. julia> 6 * 7

6. 42

L'opérateur / effectue la division :

1. julia> 84 / 2

2. 42.0

Pourquoi le résultat est-il 42.0 au lieu de 42 ? Ceci est expliqué dans la section 1.5. Enfin, l'opérateur ^ élève un nombre à une puissance donnée :

1. julia> 6^2 + 6

2. 42

1-5 - Valeurs et types

Une valeur est un des éléments de base avec lesquels un programme fonctionne, comme une lettre ou un nombre. Certaines des valeurs que nous avons vues jusqu'ici sont 2, 42.0 et "Hello, World!".

Ces valeurs appartiennent à différents types : 2 est un nombre entier, 42.0 un nombre à virgule flottante et "Hello, World!" une chaîne de caractères.

S'il y a un doute sur le type d'une valeur, le REPL peut fournir des renseignements :

1. julia> typeof(2)

2. Int64

3. julia> typeof(42.0)

4. Float64

5. julia> typeof("Hello, World!")

6. String

Les nombres entiers appartiennent au type Int64, les chaînes de caractères au type String et les nombres à virgule flottante au type Float64.

Qu'en est-il des valeurs comme "2" et "42.0" ? Elles ressemblent à des nombres, mais comme elles sont entre guillemets, il s'agit de chaînes de caractères :

1. julia> typeof("2")

2. String

3. julia> typeof("42.0")

4. String

Lorsque des nombres constitués de nombreux chiffres sont saisis, il peut être tentant d'utiliser des virgules entre les groupes de chiffres, comme pour exprimer un million en notation anglo-saxonne : 1,000,000. Ceci n'est pas un nombre entier autorisé en langage Julia, bien que cette représentation soit licite :

1. julia> 1,000,000

2. (1, 0, 0)

Évidemment, ce n'est pas du tout le résultat espéré. Julia analyse 1,000,000 comme une suite d'entiers, à savoir 1, 0 et 0, séparés par des virgules. Ce type de séquences sera analysé plus tard. Il est cependant possible d'obtenir le résultat recherché en utilisant 1_000_000.

1-6 - Langages formels et naturels

Les langages naturels sont ceux que les êtres humains utilisent quotidiennement, comme l'anglais, l'espagnol, le français, etc. Ils n'ont pas été conçus d'un bloc par leurs utilisateurs parce qu'ils ont évolué naturellement et progressivement.

A contrario, les langages formels sont conçus pour des applications spécifiques. Par exemple, la notation utilisée par les mathématiciens est un langage formel particulièrement efficace pour établir les relations entre les nombres et les symboles. Les chimistes utilisent un langage formel pour représenter la structure chimique des molécules et les réactions. Les langages de programmation sont des langages formels qui ont été conçus pour effectuer des calculs.

Le cœur des langages formels est constitué de règles syntaxiques strictes qui organisent la structure des énoncés. Par exemple, en mathématiques, l'énoncé 3+3=6 a une syntaxe correcte, mais 3+=3$6 n'a pas de sens. En chimie, H_2O est une formule syntaxiquement correcte, cependant que 2Zz ne l'est pas.

Les règles syntaxiques se déclinent en deux types, relatives aux signes d'une part et à la structure, de l'autre. Les signes sont les éléments de base de tout langage, tels que les mots, les nombres et les éléments chimiques. Un des problèmes avec 3+=3$6 est que $ n'est en principe pas un signe autorisé en mathématiques. De même, 2Zz n'est pas licite, car il n'y a pas d'élément de symbole Zz. Le deuxième type de règle syntaxique concerne la manière dont les signes sont combinés. L'équation 3+=3 est incorrecte, car, même si + et = sont des signes autorisés, leur succession n'est pas définie en mathématiques et n'a dès lors pas de sens. De même, dans une formule chimique, l'indice vient éventuellement après le nom d'un élément, jamais avant.

Voici un exemple curieux : l@ phrase de françai$ bien structurée avec des s*gnes mal utilisés. Tous les signes de cette phrase sont valides, mais la structure de la phrase est erronée.

La lecture d'une phrase en français ou une déclaration dans une langue formelle suppose d'en comprendre la structure (bien que, dans une langue naturelle, tout lecteur procède ainsi, inconsciemment). Ce processus s'appelle l'analyse syntaxique. Bien que les langues formelles et naturelles aient de nombreuses caractéristiques en commun — les signes, la structure et la syntaxe —, il existe quelques différences notoires :

Ambiguïté : les langues naturelles sont chargées d'ambiguïté, que les humains gèrent en utilisant des indices contextuels ainsi que d'autres informations. Les langues formelles sont conçues pour être presque ou totalement dépourvues d'ambiguïté, ce qui signifie que toute déclaration a une — et une seule — signification, quel que soit le contexte ;

Redondance : pour pallier l'ambiguïté et réduire les malentendus, les langues naturelles sont caractérisées par de nombreuses redondances. De ce fait, elles sont souvent verbeuses. Les langues formelles sont moins redondantes et plus concises ;

Littéralité : les langues naturelles sont remplies d'expressions idiomatiques et de métaphores. Dans l'expression « le franc est tombé », il y a fort à parier qu'il n'y a pas de franc en jeu et que rien ne tombe (cette expression idiomatique signifie que quelqu'un a compris quelque chose après une période de confusion). Les langues formelles signifient exactement ce qu'elles disent. Comme nous grandissons tous en parlant des langues naturelles, il est parfois difficile de s'adapter aux langues formelles. La différence entre le langage formel et le langage naturel s'apparente à la différence entre la poésie et la prose, mais en plus :

1. Poésie : les mots sont utilisés pour leurs sons ainsi que pour leur signification. L'ensemble du poème crée un effet ou une réponse émotionnelle. L'ambiguïté est non seulement courante, mais souvent délibérée,

2. Prose : le sens littéral des mots est important et la structure apporte du sens. La prose se prête davantage à l'analyse que la poésie, mais reste néanmoins souvent ambiguë ;

Programmes : la signification d'un programme informatique est sans ambiguïté et littérale, elle peut être entièrement comprise par l'analyse des signes et de la structure.

Les langages formels sont plus denses que les langues naturelles, ce qui nécessite plus de temps et de concentration pour les interpréter. De plus, la structure est fondamentale. Il n’est donc pas toujours judicieux de lire de haut en bas et de gauche à droite. Au lieu de cela, le lecteur apprend à analyser le programme mentalement, à identifier les signes et à interpréter la structure. Enfin, les détails sont importants. De petites erreurs d'orthographe ou de ponctuation, dont il est possible de s'affranchir dans les langues naturelles, peuvent entraîner des incompréhensions dans un langage formel.

1-7 - Débogage

Tous les programmeurs commettent des erreurs. Pour des raisons quelque peu fantaisistes, les erreurs de programmation sont appelées « bogues » et le processus pour les débusquer est appelé « débogage » (debugging). La programmation, et surtout le débogage, font parfois surgir des émotions fortes. Confronté à un bogue difficile, un programmateur peu aguerri peut se sentir en colère, découragé ou frustré. Il est prouvé que de très nombreuses personnes réagissent naturellement face aux ordinateurs comme si ces derniers étaient des personnes. Lorsque les ordinateurs fonctionnent bien, nous les considérons comme des coéquipiers, mais, lorsqu'ils se montrent obstinés, nous leur répondons de la même manière que si nous répondions à des personnes obstinées.

Se préparer à ces réactions aide à les surmonter. Une approche possible consiste à considérer l'ordinateur comme un employé doté de certains atouts tels la rapidité et la précision, mais aussi de faiblesses particulières, comme le manque d'empathie et l'incapacité à saisir la situation dans son ensemble. Le travail d'un programmeur consiste à être un bon gestionnaire : trouver des moyens de tirer parti des points forts et d'atténuer les points faibles, trouver des moyens d'effacer ses émotions pour s'attaquer au problème, sans laisser ses réactions interférer avec sa capacité à travailler efficacement.

Apprendre à déboguer peut être frustrant, mais un bon débogueur possède une compétence précieuse très utile pour de nombreuses activités au-delà même de la programmation. À la fin de chaque chapitre, existe une section comme celle-ci, comportant diverses suggestions supposées utiles pour le débogage.

1-8 - Glossaire

résolution des problèmes : processus consistant à formuler un problème, à trouver une solution et à l'exprimer.

programme : séquence d'instructions qui explicite un calcul.

REPL : programme qui lit les données d'entrée, les exécute et produit des résultats de manière répétée.

invite : caractères affichés par le REPL pour indiquer qu'il est prêt à recevoir les données de l'utilisateur.

déclaration : instruction qui amène le REPL à afficher une valeur à l'écran.

opérateur : symbole qui permet l'exécution d'un calcul simple comme l'addition, la multiplication de nombres ou la concaténation de chaînes de caractères.

valeur : une des unités de base des données, comme un nombre ou une chaîne de caractères, qu'un programme manipule.

type : une catégorie de valeurs. Les types vus jusqu'à présent sont les nombres entiers (Int64), les nombres à virgule flottante (Float64) et les chaînes de caractères (String).

Int : type qui représente des nombres entiers.

Float : type qui représente les nombres avec un point décimal.

Char : type qui représente des caractères uniques.

langage naturel : toute langue que parlent les Hommes et qui a évolué naturellement.

langage formel : tout langage que les humains ont conçu à des fins spécifiques, comme la représentation de principes mathématiques ou de programmes informatiques. Tous les langages de programmation sont des langages formels.

syntaxe : règles qui organisent la structure d'un programme.

signe : élément de base de la structure syntaxique d'un programme, analogue à un mot dans une langue naturelle.

structure : façon dont les signes sont combinés.

parse (analyse) : examen d'un programme et analyse de la structure syntaxique.

bug (ou bogue) : erreur dans un programme.

débogage : processus de recherche et de correction des bogues.

1-9 - Exercices

Lire ce contenu devant un ordinateur afin d'essayer les exemples au fur et à mesure constitue une bonne idée.

1-9-1 - Exercice 1-1

Chaque fois qu'une nouvelle fonctionnalité est expérimentée, il est indiqué de commettre volontairement des erreurs. Par exemple, dans le programme « Hello, World! », que se passe-t-il en cas d'omission d'un des guillemets ? Et quand les deux sont omis ? Et s'il y avait des fautes d'orthographe ?

Ce genre d'expérience aide à se souvenir de ce qui est lu, ainsi que lors de l'exercice de programmation. En effet, c'est de cette manière qu'un programmeur apprend à connaître la signification des messages d'erreur. Il est préférable de faire des erreurs maintenant et volontairement plutôt que plus tard et accidentellement.

1. Dans une déclaration d'affichage, que se passe-t-il en cas d'omission d'une des parenthèses ou des deux ?

2. À l'affichage d'une chaîne de caractères, que se passe-t-il si un des guillemets est omis ou les deux ?

3. Un signe moins peut être utilisé pour obtenir un nombre négatif comme -2. Que se passe-t-il si un signe plus précède un nombre ? Qu'en est-il de 2++2 ?

4. En notation mathématique, les zéros de tête sont acceptés, comme dans 02. Que se passe-t-il lorsqu'une telle notation est utilisée en Julia ?

5. Que se passe-t-il lorsqu'il n'y a pas d'opérateur entre deux valeurs ?

1-9-2 - Exercice 1-2

Lançons le REPL Julia et utilisons-le comme calculatrice :

1. Combien de secondes y a-t-il dans 42 minutes 42 secondes ?

2. Sachant que 1 mile terrestre correspond à 1,61 km, combien de miles y a-t-il dans 10 kilomètres ?

3. Si 10 kilomètres sont parcourus en 37 minutes 48 secondes, quel est le rythme moyen (temps par kilomètre en minutes et secondes) ? Quelle est la vitesse moyenne en miles terrestres par heure ?

2 - Variables, expressions et déclarations

La possibilité de manipuler des variables constitue une des grandes forces des langages de programmation. Une variable est un nom (lettre ou mot) qui fait référence à une valeur.

2-1 - Déclaration d'affectation

Une déclaration d'affectation crée une nouvelle variable et lui attribue une valeur :

1. julia> message = "À présent, quelque chose de nouveau"

2. "À présent, quelque chose de nouveau"

3. julia> n = 17

4. 17

5. julia> π_val = 3.141592653589793

6. 3.141592653589793

Dans cet exemple, la première affectation associe une chaîne à une nouvelle variable nommée message. La deuxième associe l'entier 17 à n. La troisième apparie la valeur approximative de π à la variable π_val (pour obtenir le symbole π, il convient de saisir \pi et de presser la touche TAB).

Une manière courante de représenter les variables sur papier consiste à écrire le nom avec une flèche pointant vers sa valeur. Ce type de figure est appelé diagramme d'état, car il montre dans quel état se trouve chacune des variables. La figure 2.1.1 représente le diagramme d'état de l'encadré précédent.

FIGURE 2.1.1 – Exemple d'un diagramme d'état.

2-2 - Noms de variables

Généralement, les programmeurs choisissent pour leurs variables des noms qui sont expressifs et, au besoin, ils associent un élément de documentation à la variable afin d'améliorer la compréhension du code.

La longueur des noms des variables n'est pas limitée. Ces noms peuvent contenir presque tous les caractères Unicode (voir la section 8.1), mais ils ne peuvent pas commencer par un nombre. Les lettres majuscules sont autorisées, quoiqu'il soit conventionnel de n'utiliser que des minuscules pour les noms de variables. Les caractères Unicode peuvent être saisis en complétant les abréviations de type dans le REPL, et ce, par des tabulations.

Le caractère de soulignement _ peut apparaître dans un nom. Il est souvent utilisé dans les noms comportant plusieurs mots.

Si un nom inapproprié est attribué à une variable, Julia retourne une erreur de syntaxe :

1. julia> 76trombones = ("Belle fanfare")

2. ERROR: syntax: "76" is not a valid function argument name

3. julia> more@ = 1000000

4. ERROR: syntax: extra token "@" after end of expression

5. julia> struct = "Advanced Theoretical Zimurgy"

6. ERROR: syntax: unexpected "="

76trombones est incorrect parce que la variable commence par un chiffre. more@ est erroné parce cette formulation contient un caractère non autorisé, @. Mais qu'est-ce qui ne va pas avec la troisième déclaration ? Il s'avère que struct est un des mots-clés de Julia. Le REPL utilise des mots-clés pour reconnaître la structure d'un programme. Or, ceux-ci ne peuvent pas être utilisés comme noms de variables.

Julia comporte les mots-clés suivants repris dans le tableau 2.1.

TABLE 2.1 - Liste des mots-clés de Julia.

abstract type

baremodule

begin

break

catch

const

continue

do

else

elseif

end

export

finally

for

function

global

if

import

importall

in

let

local

macro

module

mutable struct

primitive type

quote

return

struct

try

using

where

while

Il est inutile de mémoriser cette liste. Dans la plupart des environnements de développement, les mots-clés sont affichés dans une couleur déterminée. Si un mot de cette liste est attribué à une variable, un message d'alerte/d'erreur apparaîtra.

2-3 - Expressions et déclarations

Une expression est une combinaison de valeurs, de variables et d'opérateurs. Une valeur en soi est considérée comme une expression, de même qu'une variable, de sorte que les expressions suivantes sont toutes autorisées :

1. julia> 42

2. 42

3. julia> n

4. 17

5. julia> n + 25

6. 42

Lorsqu'une expression est saisie à l'invite, le REPL l'évalue pour trouver sa valeur. Dans cet exemple, n a la valeur 17 et n + 25 prend la valeur 42.

Une déclaration (ou une instruction) est une unité de code qui produit un effet, comme la création d'une variable ou l'affichage d'une valeur.

1. julia> n = 17

2. 17

3. julia> println(n)

4. 17

La première ligne est une déclaration d'affectation qui associe une valeur à n. La deuxième ligne est une instruction qui conduit à l'affichage de la valeur de n.

Lorsqu'une instruction est saisie, le REPL l'exécute, ce qui signifie qu'il effectue tout ce que contient l'instruction.

2-4 - Mode « script »

Jusqu'à présent, nous avons pratiqué Julia en mode interactif, ce qui signifie que nous interagissons directement avec le REPL. Le mode interactif est un bon moyen de démarrer, mais, lors d’un usage intensif, cette technique devient rapidement fastidieuse.

L'autre possibilité consiste à enregistrer le code dans un fichier. On se réfère à ce dernier en disant qu'il s'agit d'un script. Ensuite, l'utilisateur lance Julia en mode script pour exécuter le code qu'il contient. Par convention, les scripts en Julia portent des noms se terminant par .jl.

Si nous savons comment créer et exécuter un script, nous sommes prêts (sinon, il faudra utiliser en local Jupyter ou Pluto, par exemple ; voir l'annexe B). À ce stade, il existe deux méthodes :

- soit ouvrir un fichier texte (avec Vim, Emacs, etc.), y écrire le code du script et l'enregistrer avec une extension .jl. Le script peut être exécuté dans un terminal bash (ou analogue) pour autant que le dossier courant soit le répertoire contenant le script à exécuter :

1. name@computer:~$ julia  nom_du_programme.jl

- soit recourir à un environnement de développement intégré comme VSCodium (voir l'annexe B). En supposant que le script est enregistré dans un répertoire dénommé Prgm_Julia. Dans VS Codium, il suffit de cliquer sur File → Open File et de sélectionner le script. Celui-ci apparaît, colorisé, la partie supérieure de Codium. Il convient de cliquer sur une des lignes du code puis de cliquer sur l'icône de la barre de commandes supérieure afin de le lancer.

Quoi qu'il en soit, parce que Julia propose les deux modes (REPL et exécution de script), il est possible de tester des bouts de code en mode interactif avant de les insérer dans un script. Cependant, il existe des différences entre le mode interactif et le mode script qui peuvent être surprenantes. Par exemple, utilisons Julia comme calculatrice et saisissons :

1. julia> miles = 26.2

2. 26.2

3. julia> miles * 1.61

4. 42.182

La première ligne attribue une valeur à miles et affiche la valeur. La deuxième ligne est une expression. Par conséquent, le REPL l'évalue et affiche le résultat(4).

Néanmoins, l'exécution du même code dans un script ne produit aucun résultat. En mode script, une expression, à elle seule, n'a aucun effet visible. Julia évalue bel et bien l'expression, mais n'affiche pas la valeur, sauf à lui demander de le faire :

1. miles = 26.2

2. println(miles * 1.61)

Ce comportement peut être déroutant au début.

Un script contient généralement une séquence d'instructions. S'il y a plus d'une déclaration, les résultats apparaissent un à un, à mesure de l'exécution des déclarations.

Par exemple, le code suivant :

1. println(1)

2. x = 2

3. println(x)

affiche :

1. 1

2. 2

Les déclarations d'affectation ne produisent aucun résultat.

2-4-1 - Exercice 2-1

Vérifiez que ceci est bien compris en saisissant les déclarations suivantes dans le REPL de Julia et observez le résultat :

1. 5

2. x + 5

3. x + 1

Maintenant, écrivez les mêmes déclarations dans un script et exécutez-le. Quel est le résultat ? Modifiez le script en transformant chaque expression en une instruction d'affichage, puis exécutez-le à nouveau.

2-5 - Priorité des opérateurs

Lorsqu'une expression contient plus d'un opérateur, l'ordre d'évaluation dépend de la préséance des opérateurs entre eux. Pour les opérateurs mathématiques, Julia suit la convention mathématique. L'acronyme PEMDAS constitue un moyen mnémotechnique :

1. Les Parenthèses ont la plus haute priorité et peuvent être utilisées pour forcer une expression à être évaluée dans l'ordre souhaité. Comme les expressions entre parenthèses sont évaluées en premier, 2*(3-1) vaut 4 et (1+1)^(5-2) vaut 8. L'usage de parenthèses contribue à rendre une expression plus facile à lire, comme dans (minute * 100) / 60, même si cela ne change pas le résultat ;

2. L'Exponentiation a la priorité suivante, donc 1+2^3 vaut 9 (et non 27), et 2*3^2 donne 18 (et non 36) ;

3. La Multiplication et la Division ont la préséance sur l'Addition et la Soustraction. Ainsi, 2*3-1 a pour valeur 5 (et non 4) et 6+4/2 est évalué comme 8 (et non 5) ;

4. Les opérateurs ayant la même priorité sont évalués de gauche à droite (sauf l'exponentiation). Ainsi, dans l'expression degrés / 2 * π, la division se fait en premier et le résultat est multiplié par π. Pour diviser par 2π, il est possible d'utiliser des parenthèses degrés / (2 * π), d'écrire degrés / 2 / π ou encore degrés / 2π.

Il n'est pas simple de se souvenir de la préséance des opérateurs et une expression complexe peut rapidement devenir un casse-tête, source d'erreurs. L'usage de parenthèses relève de la prudence.

2-6 - Opérations sur les chaînes de caractères

En général, les opérations mathématiques sur des chaînes de caractères ne sont pas permises, même si ces dernières ressemblent à des nombres. Les opérations suivantes sont donc illicites :

1. "2" - "1"    "un œuf" / "le plat"    "top" + "charm"

Cependant, il existe deux exceptions : * et ^.

L'opérateur * effectue la concaténation des chaînes de caractères, ce qui signifie qu'il joint les chaînes en les reliant bout à bout. Par exemple :

1. julia> first_str = "paruline "

2. "paruline "

3. julia> second_str = "à "

4. "à "

5. julia> third_str = "gorge "

6. "gorge "

7. julia> fourth_str = "jaune"

8. "jaune"

9. julia> first_str * second_str * third_str * fourth_str

10. "paruline à gorge jaune"

L'opérateur ^ procède également sur des chaînes de caractères. Il effectue des répétitions. Par exemple, "Spam"^3 retourne "SpamSpamSpam". Si une des valeurs est une chaîne, l'autre doit être un nombre entier.

Cette utilisation de * et ^ est logique par analogie avec la multiplication et l'exponentiation. Tout comme 4^3 est équivalent à 4*4*4, nous nous attendons à ce que "Spam"^3 soit identique à "Spam" * "Spam" * "Spam", ce qui est bien le cas.

2-7 - Commentaires

Plus les programmes deviennent volumineux et complexes, plus ils sont difficiles à lire. Les langages formels sont denses et il est souvent difficile de regarder un morceau de code et de comprendre ce qu'il représente.

C'est pourquoi il est bon d'ajouter des notes à nos programmes pour expliquer en langage naturel ce que réalise le code. Ces notes sont appelées des commentaires et elles commencent par le symbole # :

1. # calcule le pourcentage d'heure écoulé

2. pourcentage = (minute * 100) / 60

Dans ce cas, le commentaire apparaît sur une ligne séparée. Cependant, les commentaires peuvent se trouver à la fin d'une ligne :

1. pourcentage = (minute * 100) / 60    # calcule le pourcentage d'heure écoulé

Tout ce qui suit le caractère # jusqu'à la fin de la ligne est ignoré. Un commentaire n'altère pas l'exécution d'un programme.

Les commentaires sont toujours utiles lorsqu'ils documentent des caractéristiques non évidentes du code. Il est raisonnable de supposer que le lecteur peut comprendre ce que fait le code. Il est plus utile d'expliquer pourquoi.

Par exemple, ce commentaire est redondant avec le code et, par conséquent, inutile :

1. v = 5    # attribue 5 à v

Cependant, le commentaire suivant contient une information réellement pertinente :

1. v = 5    # vitesse exprimée en m/s

Des noms de variables ad hoc peuvent réduire le besoin d'écrire des commentaires. Les noms longs, généralement explicites, peuvent rendre des expressions complexes difficiles à lire. Tout est une question de compromis.

2-8 - Débogage

Trois types d'erreurs se produisent dans un programme : des erreurs de syntaxe, des erreurs d'exécution et des erreurs sémantiques. Ce point sera approfondi au chapitre 21. Il est utile de les distinguer afin de les repérer le plus rapidement possible.

Erreur de syntaxe La « syntaxe » fait référence à la structure d'un programme et aux règles relatives à cette structure. Par exemple, les parenthèses doivent venir par paires, donc (1 + 2) est licite, mais 8) constitue une erreur de syntaxe.
S'il y a une erreur de syntaxe dans un programme, Julia affiche un message d'erreur et arrête l'exécution. Au cours des premières semaines d'étude, il est fréquent de passer du temps à rechercher les erreurs de syntaxe. Avec l'expérience, ce type d'erreur se manifestera plus rarement et leur détection sera de plus en plus rapide.

Erreur d'exécution Le deuxième type d'erreur concerne l'exécution. On dit « erreur d'exécution » parce qu'elle n'apparaît qu'après le démarrage du programme. Ces erreurs sont également appelées exceptions parce qu'elles indiquent généralement que quelque chose d'exceptionnel (et de malencontreux) s'est produit.
Les erreurs d'exécution sont rares dans les programmes simples associés aux premiers chapitres. Il s'écoulera un certain temps avant d'en rencontrer.

Erreur sémantique Le troisième type d'erreur concerne la « sémantique ». Il s'agit d'un problème de sens. Si un programme contient une erreur sémantique, il s'exécute sans émettre le moindre message d'erreur. Cependant, le programme produit un résultat autre que celui attendu, bien qu'il accomplisse exactement ce qu'il lui est demandé.
L'identification des erreurs sémantiques peut être délicate, car elle oblige à travailler à rebours en examinant la sortie d'un programme et en essayant de comprendre comment il procède.

2-9 - Glossaire

variable nom qui fait référence à une valeur.

affectation déclaration qui attribue une valeur à une variable.

diagramme d'état représentation graphique d'un ensemble de variables et des valeurs auxquelles elles se réfèrent.

mot-clé mot réservé utilisé par un langage de programmation. Les mots-clés tels que if, function, while, etc., ne peuvent pas être employés comme noms de variables.

opérande une des valeurs sur lesquelles un opérateur exerce une action.

expression combinaison de variables, d'opérateurs et de valeurs qui représente un seul résultat.

évaluer simplifier une expression en effectuant les opérations afin d'obtenir une valeur unique.

déclaration section de code qui représente un ordre ou une action. Jusqu'à présent, les déclarations que nous avons vues opèrent des affectations (assignments en anglais) et des déclarations d'affichage.

exécuter lire une déclaration et faire ce qu'elle exprime.

mode interactif façon d'utiliser le REPL de Julia en saisissant un code à l'invite.

mode script façon d'utiliser Julia pour lire le code d'un script et l'exécuter.

script programme enregistré dans un fichier.

priorité d'un opérateur règles de préséance relative à l'ordre dans lequel les expressions impliquant des opérateurs mathématiques et des opérandes multiples sont évaluées.

concaténer joindre deux éléments bout à bout.

commentaire informations contenues dans un programme qui sont destinées à d'autres programmeurs (ou à toute personne lisant le code source) et qui n'ont aucun effet sur l'exécution de ce programme.

erreur de syntaxe erreur dans un programme qui rend impossible l'analyse (et donc l'interprétation).

erreur d'exécution (appelée également exception) erreur qui est détectée pendant que le programme est en cours d'exécution.

sémantique signification ou « sens » d'un programme.

erreur sémantique erreur dans un programme qui l'amène à faire autre chose que ce pourquoi il a été conçu.

2-10 - Exercices

2-10-1 - Exercice 2-3

Comme souligné dans le chapitre 1, chaque fois que vous apprenez une nouvelle fonction, vous devez l'essayer en mode interactif (REPL) et commettre des erreurs pour évaluer ce qui ne fonctionne pas.

1. Nous avons vu que n = 42 est licite. Qu'en est-il de 42 = n ?

2. Que se passe-t-il avec x = y = 1 ?

3. Dans certains langages, chaque énoncé se termine par un point-virgule (;). Que se passe-t-il si vous mettez un point-virgule à la fin d'une déclaration en Julia ?

4. Que se passe-t-il si vous mettez un point à la fin d'une déclaration ?

5. En notation mathématique, vous pouvez multiplier x et y comme ceci : x y. Que se passe-t-il si vous essayez cela en Julia ? Quid avec 5x ?

2-10-2 - Exercice 2-3

Entraînez-vous à utiliser le REPL de Julia comme calculatrice :

1. Le volume d'une sphère de rayon est donné par la formule . Quel est le volume d'une sphère de rayon  ?

2. Supposons que le prix de couverture d'un livre soit de 24,95 €, mais que les librairies obtiennent une remise de 40 %. Les frais d'expédition sont de 3 € pour le premier exemplaire et de 75 centimes pour chaque exemplaire supplémentaire. Quel est le prix de gros total pour 60 exemplaires ?

3. Si quelqu'un quitte son habitation à 6 h 52 du matin et qu'il court 1 km au rythme de 8 min 15 s par km, puis 3 km au rythme de 7 min 12 s par km et encore 1 km à un rythme de 8 min 15 s par km, à quelle heure rentrera-t-il pour le petit-déjeuner ?

3 - Fonctions

Dans le contexte de la programmation, une fonction est une séquence d'instructions effectuant un calcul. La définition d'une fonction doit contenir le nom de la fonction et une suite d'instructions. La fonction étant définie, il y est fait appel en utilisant son nom.

3-1 - Appel de fonction

Nous avons déjà rencontré un appel de fonction :

1. julia> println("Hello, World!")

2. Hello, World!

Le nom de la fonction (interne(5) à Julia) est println. L'expression entre parenthèses s'appelle l'argument de la fonction.

Il est courant de dire qu'une fonction « prend » un argument et « retourne » un résultat. Le résultat est également appelé valeur de retour.

Julia fournit des fonctions qui convertissent des valeurs d'un type à un autre. La fonction parse prend une chaîne de caractères et la convertit en n'importe quel type de nombre, si elle le peut. Sinon, Julia retourne un message d'erreur :

1. julia> parse(Int64, "32")

2. 32

3. julia> parse(Float64, "3.14159")

4. 3.14159

5. julia> parse(Int64, "Hello")

6. ERROR: ArgumentError: invalid base 10 digit 'H' in "Hello"

La fonction trunc peut convertir des nombres à virgule flottante en entiers, mais sans arrondi :

1. julia> trunc(Int64, 3.9999)

2. 3

3. julia> trunc(Int64, -2.3)

4. -2

La fonction float convertit les nombres entiers en nombres à virgule flottante :

1. julia> float(32)

2. 32.0

La fonction string convertit son argument en chaîne de caractères :

1. julia> string(32)

2. "32"

3. julia> string(3.14159)

4. "3.14159"

3-2 - Fonctions mathématiques

Naturellement, avec Julia, la plupart des fonctions mathématiques usuelles sont directement disponibles :

1. ratio = signal_power / noise_power

2. decibels = 10 * log10(ratio)

Ce premier exemple utilise log10 pour calculer un rapport signal/bruit en décibels (en supposant que la puissance du signal et la puissance du bruit soient définies).

La fonction log calcule les logarithmes naturels (ou népériens, aussi appelés hyperboliques — ).

À présent, voyons un autre exemple :

1. radians = 0.7

2. height = sin(radians)

Ce deuxième cas avec la fonction sin illustre la manière de procéder pour le sinus d'un angle exprimé en radians. Un argument passé tel quel aux fonctions trigonométriques (sin, cos, tan, etc.) s'exprime en radians.

Pour convertir des degrés en radians, il faut diviser par 180 et multiplier par π :

1. julia> degrees = 45

2. 45

3. julia> radians = (degrees / 180) * π

4. 0.7853981633974483

5. julia> sin(radians)

6. 0.7071067811865475

La valeur de la variable π est une approximation en virgule flottante du nombre , avec une précision à 16 chiffres.

En recourant à la trigonométrie, on vérifie que le résultat précédent correspond à la racine carrée de 2 divisée par 2 :

1. julia> sqrt(2) / 2

2. 0.7071067811865475

3-3 - Composition

Jusqu'à présent, nous avons examiné les éléments d'un programme — variables, expressions et déclarations — isolément, sans évoquer la manière de les combiner.

Une caractéristique des plus utiles des langages de programmation provient de leur capacité à prendre de petits blocs de construction et à les combiner (les associer). Par exemple, l'argument d'une fonction peut être tout type d'expression, dont des opérateurs arithmétiques :

1. x = sin((degrees / 360) * 2 * π)

Cela fonctionne également avec des appels de fonction :

1. x = exp(log(x+1))

Presque partout où une valeur peut être injectée, il est possible de poser une expression arbitraire, à une contrainte près : le membre de gauche d'une déclaration d'affectation doit être un nom de variable. Toute autre expression à gauche conduit une erreur de syntaxe (nous verrons les exceptions à cette règle plus tard).

1. julia> minutes = heures * 60     # licite

2. 45

3. julia> heures * 60 = minutes      # illicite!

4. ERROR: syntax: "60" is not a valid function argument name

3-4 - Ajout de nouvelles fonctions

Jusqu'à présent, nous avons uniquement employé des fonctions internes à Julia. Cependant, ajouter de nouvelles fonctions est une des bases de la programmation. Une définition de fonction spécifie le nom d'une nouvelle fonction et la séquence d'instructions qui s'exécutent lorsque la fonction est appelée. Voici un exemple :

1. function printlyrics()

2. println("Ses fluctuat nec mergitur,")

3. println("C'était pas d'la littérature")

4. end

Le terme function est un mot-clé qui indique qu'une nouvelle fonction est définie. Le nom de la fonction est printlyrics. Les règles pour les noms de fonctions sont les mêmes que pour les noms de variables : ils peuvent contenir presque tous les caractères Unicode (voir la section 8.1), si ce n'est que le premier caractère ne peut pas être un chiffre. Par ailleurs, il n'est pas permis d'utiliser un mot-clé comme nom de fonction. En outre, il faut éviter qu'une variable et une fonction portent le même nom. Les parenthèses vides après le nom indiquent que cette fonction ne prend pas d'arguments.

La première ligne s'appelle l'en-tête ; le reste s'appelle le corps. Le corps se termine par le mot-clé end et peut contenir un nombre quelconque d'instructions. Pour une meilleure lisibilité, le corps de la fonction devrait être indenté.

Les guillemets doivent être des "guillemets droits" (idem pour les 'guillemets' simples). Les guillemets de ce type “ ” ou ‘ ’ ne sont pas admis en Julia.

Si une définition de fonction est saisie en mode interactif, le REPL s'indente automatiquement pour indiquer que la définition n'est pas complète :

1. julia> function printlyrics()

2. println("Ses fluctuat nec mergitur,")

La terminaison d'une fonction est indiquée par le mot-clé end.

La syntaxe pour appeler la nouvelle fonction est la même que pour les fonctions internes à Julia :

1. julia> printlyrics()

2. Ses fluctuat nec mergitur,

3. C’était pas d’la littérature

Dès qu'une fonction est définie, elle peut être utilisée au sein d'une autre fonction. Par exemple, pour répéter les vers précédents, nous pouvons écrire une fonction appelée repeatlyrics :

1. function repeatlyrics()

2. printlyrics()

3. printlyrics()

4. end

Lorsque repeatlyrics est appelée, nous obtenons :

1. julia> repeatlyrics()

2. Ses fluctuat nec mergitur,

3. C’était pas d’la littérature

4. Ses fluctuat nec mergitur,

5. C’était pas d’la littérature

3-5 - Définition et usages

En rassemblant les fragments de code, l'ensemble du programme ressemble à ceci :

1. function printlyrics()

2. println("Ses fluctuat nec mergitur,")

3. println("C'était pas d'la littérature")

4. end

6. function repeatlyrics()

7. printlyrics()

8. printlyrics()

9. end

11. repeatlyrics()

Ce programme contient deux définitions de fonctions : printlyrics et repeatlyrics. Les définitions de fonction sont exécutées comme les autres instructions, mais elles ont pour effet de créer des objets de fonction. Les instructions à l'intérieur de la fonction ne s'exécutent pas avant que la fonction soit appelée. La définition de la fonction ne produit aucune sortie.

Comme on peut s'y attendre, il faut créer une fonction avant de pouvoir l'exécuter. En d'autres termes, la définition de la fonction doit être exécutée avant que la fonction ne soit appelée.

3-5-1 - Exercice 3-1

Déplacez la dernière ligne de ce programme vers le haut, afin que l'appel de fonction apparaisse avant les définitions. Lancez le programme et voyez quel message d'erreur est retourné.

Ceci fait, déplacez l'appel de fonction vers le bas et déplacez la définition de printlyrics après la définition de repeatlyrics. Que se passe-t-il lors de l'exécution de ce programme ?

3-6 - Flux d'exécution

Pour s'assurer qu'une fonction est définie avant sa première utilisation, il faut connaître le flux d'exécution.

L'exécution commence toujours à la première instruction d'un programme. Les instructions sont exécutées séquentiellement, dans l'ordre, de haut en bas. Les définitions de fonction ne modifient pas le flux d'exécution du programme, mais il faut se rappeler que les instructions à l'intérieur de la fonction ne s'exécutent pas avant que la fonction soit appelée. Un appel de fonction équivaut à un détour dans le flux d'exécution. Au lieu de passer à l'instruction suivante, le flux effectue un saut jusqu'au corps de la fonction appelée, y exécute les instructions, puis revient pour reprendre là où il s'était arrêté.

Cela semble assez simple, jusqu'à ce qu'on se souvienne qu'une fonction peut en appeler une autre. Au milieu d'une fonction, un programme peut être conduit à exécuter les instructions se trouvant dans une autre fonction. Puis, pendant l'exécution de cette nouvelle fonction, le programme peut à nouveau devoir exécuter une autre fonction. Heureusement, Julia est doué pour conserver la trace de l'endroit où reprendre. Ainsi, chaque fois qu'une fonction se termine, le programme reprend où il s'était arrêté au sein de la fonction qu'il a appelée.

En résumé, la lecture d'un programme ne se fait pas toujours linéairement de haut en bas. Généralement, pour la compréhension d'un programme, il s'avère préférable de suivre le déroulement de l'exécution.

3-7 - Paramètres et arguments

Certaines des fonctions que nous avons vues nécessitent des arguments. C'est le cas de la fonction sin, qui requiert un nombre en argument. D'autres fonctions en nécessitent plusieurs. Par exemple, parse en prend deux : un type de nombre et une chaîne de caractères.

À l'intérieur d'une fonction, les arguments sont affectés à des variables appelées paramètres. Le petit programme suivant décrit une fonction qui prend un argument pour calculer son carré (ceci à titre d'exemple numérique) :

1. julia> function élève_au_carré(t)

2. carré = t * t

3. println(carré)

4. end

6. julia> élève_au_carré(5)

7. 25

En l'occurrence, l'appel de la fonction [c'est-à-dire l'instruction élève_au_carré(5)] passe son argument (5) à t [de la function élève_au_carré(t)]. t est ensuite transféré comme paramètre à l'intérieur de la fonction élève_au_carré(t). Le résultat de la multiplication (t * t) est affecté à la variable carré. Celle-ci est passée en argument à la fonction println et affichée.

L'intérêt de ce type de pratique est de pouvoir réutiliser la fonction ultérieurement. Par exemple, en vertu de ce qui a été discuté en section 2.6, élève_au_carré("lu") permet d'obtenir le mot lulu.

Il est également possible d'utiliser une variable en tant qu'argument :

1. julia> nom = "lu"

2. "lu"

3. julia> élève_au_carré(nom)

4. lulu

Le nom de la variable que nous passons en argument (nom) n'a rien à voir avec le nom du paramètre (t). Peu importe comment la valeur a été nommée dans l'appel. Nous appelons t tout qui arrive en argument dans la fonction élève_au_carré.

Voici un autre exemple de fonction simple :

1. function printtwice(eluard)

2. println(eluard)

3. println(eluard)

4. end

Avec cet appel :

1. printtwice(π)

Julia retourne :

1. π

2. π

3-8 - Les variables et les paramètres sont locaux

Une variable créée à l'intérieur d'une fonction est locale. Ceci signifie qu'elle n'existe qu'à l'intérieur de la fonction. Par exemple :

1. function cattwice(part1, part2)

2. concat = part1 * part2

3. printtwice(concat)

4. end

Cette fonction prend deux arguments, les concatène et affiche le résultat. Voici un exemple d'utilisation :

1. julia> line1 = "Facile est beau "

2. "Facile est beau "

3. julia> line2 = "sous tes paupières"

4. "sous tes paupières"

5. julia> cattwice(line1, line2)

6. Facile est beau sous tes paupières

7. Facile est beau sous tes paupières

Lorsque cattwice se termine, la variable concat est détruite. Si nous essayons de l'afficher, nous obtenons une exception :

1. julia> println(concat)

2. ERROR: UndefVarError: concat not defined

Ceci illustre le fait que les paramètres sont locaux. Par exemple (voir la section 3.7), hors de la fonction élève_au_carré, t n'existe pas.

3-9 - Diagrammes de pile

Pour savoir quelles variables utiliser et à quel endroit, il est parfois utile de dessiner un diagramme de pile. Comme les diagrammes d'état, les diagrammes de pile montrent la valeur de chaque variable. En outre, ils montrent la fonction à laquelle chaque variable appartient. Chaque fonction est représentée par un cadre. Un cadre est un espace fermé avec le nom d'une fonction en vis-à-vis et les paramètres et variables de la fonction à l'intérieur.

Le diagramme de pile de l'exemple précédent (avec cattwice) est présenté dans la figure 3.9.1.

FIGURE 3.9.1 – Diagramme de pile.

Les cadres sont disposés dans une pile qui indique quelle fonction est appelée. Dans cet exemple, la fonction printtwice a été appelée par cattwice et cattwice l'a été par Main. Main est un nom spécial désignant le cadre supérieur. Lorsqu'une variable est créée en dehors de toute fonction, elle appartient à Main. Chaque paramètre fait référence à la même valeur que son argument correspondant. Ainsi, part1 a la même valeur que line1, part2 a la même valeur que line2 et eluard a la même valeur que concat.

Si une erreur se produit pendant un appel de fonction, Julia imprime le nom de la fonction, le nom de la fonction qui l'a appelée et le nom de la fonction qui a appelé tout cela jusqu'à Main.

Par exemple, si nous essayions d'accéder à concat à partir de printtwice, nous obtiendrions une erreur UndefVarError :

1. ERROR: UndefVarError: concat not defined

2. Stacktrace:

3. [1] printtwice at ./REPL[1]:2 [inlined]

4. [2] cattwice (::String, ::String) at ./REPL[2::]

Cette liste de fonctions est appelée trace de pile ou stacktrace. Elle indique dans quel fichier du programme l'erreur s'est produite ainsi que sur quelle ligne. Elle indique quelles fonctions étaient en cours d'exécution à ce moment-là et, également, la ligne de code qui a causé l'erreur.

L'ordre des fonctions dans la trace de pile est l'inverse de celui des cadres dans le diagramme de pile. La fonction en cours d'exécution se trouve au sommet de la pile.

3-10 - Fonction avec retour et fonction vide

Certaines des fonctions que nous avons utilisées, comme les fonctions mathématiques, retournent des résultats. Appelons-les « fonctions avec retour ». D'autres fonctions effectuent une action, mais ne retournent pas de valeur. On les appelle des fonctions nulles ou vides (void functions). Dans l'exemple suivant :

1. function printtwice(brassens)

2. println(brassens)

3. println(brassens)

4. end

la fonction printtwice vue précédemment (section 3.7) ne retourne aucune valeur, bien qu'elle effectue deux actions.

En appelant une fonction avec retour, l'intention est d'exploiter le résultat obtenu. Par exemple, il est tout à fait possible d'affecter le résultat à une variable ou de l'utiliser comme partie d'une expression :

1. x = cos(radians)

2. golden = (sqrt(5) + 1) / 2

Lorsqu'une fonction est appelée en mode interactif, Julia affiche le résultat :

1. sqrt(5)

2. 2.23606797749979

Cependant, dans un script contenant une instruction telle que :

1. sqrt(5)

si une fonction avec retour est appelée seule, la valeur de retour est définitivement perdue. Ce script calcule bien la racine carrée de 5, mais, comme il n'enregistre ni n'affiche le résultat, il n'est guère utile.

Les fonctions vides ou nulles peuvent afficher un message à l'écran ou avoir quelque autre effet, mais elles n'ont pas de valeur de retour. Si le résultat est attribué à une variable, nous obtenons une valeur spéciale appelée nothing.

1. julia> resultat = élève_au_carré(5)

2. 25

3. julia> show(resultat)

4. nothing

Pour imprimer la valeur nothing, nous devons utiliser la fonction show qui agit comme println, mais en gérant la valeur nothing.

La valeur nothing n'est pas de la même nature que la chaîne « nothing ». C'est une valeur spéciale qui est caractérisée par son propre type :

1. julia> typeof(nothing)

2. Nothing

Nous commencerons à rédiger des fonctions avec retour au chapitre 6 et suivants.

3-11 - Pourquoi utiliser des fonctions ?

Il ne semble peut-être pas évident de comprendre pourquoi il est astucieux de diviser un programme en fonctions. Il existe plusieurs raisons à cela :

- la création d'une nouvelle fonction offre la possibilité de nommer un groupe d'instructions, ce qui rend tout programme plus facile à lire et à déboguer ;

- les fonctions rendent un programme plus concis en éliminant le code répétitif. Plus tard, s'il apporte une modification, le programmeur ne doit l'introduire qu'à un seul endroit ;

- diviser un programme de grande taille en fonctions permet de déboguer chaque partie une à une et les assembler ensuite en un tout opérationnel ;

- des fonctions bien conçues sont souvent utiles pour de nombreux programmes. Une fois écrite et déboguée, une fonction peut être réutilisée ;

- en Julia, les fonctions améliorent considérablement la performance.

3-12 - Débogage

Le débogage est une des compétences les plus importantes qu'un programmeur doit absolument acquérir. Bien qu'il puisse être frustrant, le débogage est une des parties les plus riches intellectuellement, les plus stimulantes et les plus intéressantes de la programmation. D'une certaine manière, le débogage ressemble au travail de détective. Confronté à des indices, le programmeur doit déduire les processus et les événements qui ont conduit aux résultats erronés observés.

Le débogage s'avère également similaire à une science expérimentale. Lorsque se présente une idée de ce qui coince, il convient de modifier le programme et l'essayer à nouveau. Si l'hypothèse est correcte, le résultat de la modification peut être prédit et, graduellement, on s'approche d'un programme opérationnel. Si l'hypothèse est fausse, il faut en formuler une nouvelle. Comme l'a fait remarquer Sherlock Holmes :

When you have eliminated the impossible, whatever remains, however improbable, must be the truth. (A. Conan Doyle — The Sign of Four.)
Lorsque vous avez éliminé l'impossible, ce qui reste, si improbable soit-il, est nécessairement la vérité. (Traduction de Jeanne de Polignac.)

Pour certains, la programmation et le débogage sont une seule et même notion : la programmation est le processus qui consiste à déboguer progressivement un programme jusqu'à ce qu'il exécute ce à quoi il est destiné. L'idée est de commencer avec un programme qui fonctionne et d'y apporter de petites modifications, en les déboguant au fur et à mesure.

Par exemple, Linux est un système d'exploitation qui contient des millions de lignes de code. Pourtant, il a commencé comme un simple programme que Linus Torvalds a utilisé pour explorer la puce Intel 80386. Selon Larry Greenfield dans The Linux User's Guide : « Un des premiers projets de Linus était un programme qui permettait de passer de l'impression "AAAA" à "BBBB". Ce programme a ensuite évolué vers le noyau Linux ».

3-13 - Glossaire

fonction séquence nommée d'instructions qui effectue une opération utile. Les fonctions peuvent ou non prendre des arguments et peuvent ou non produire un résultat.

définition de fonction déclaration qui crée une nouvelle fonction, en précisant son nom, ses paramètres et les instructions qu'elle contient.

objet de fonction valeur créée par la définition d'une fonction. Le nom de la fonction est une variable qui fait référence à un objet de la fonction.

en-tête première ligne de la définition d'une fonction.

corps séquence d'instructions à l'intérieur d'une fonction.

paramètre nom utilisé à l'intérieur d'une fonction pour désigner la valeur passée en argument.

appel de fonction déclaration qui dirige l'exécution vers une fonction. Elle se compose du nom de la fonction suivi d'une liste d'arguments entre parenthèses.

argument valeur fournie à une fonction lorsque celle-ci est appelée. Cette valeur est attribuée au paramètre correspondant dans la fonction.

variable locale variable définie à l'intérieur d'une fonction. Une variable locale ne peut être utilisée qu'à l'intérieur de la fonction où elle est déclarée.

valeur de retour résultat du traitement d'une fonction. Si un appel de fonction est utilisé comme une expression, la valeur de retour est la valeur de l'expression.

fonction avec retour fonction qui retourne une valeur à l'appelant.

fonction void (fonction nulle ou vide) fonction qui retourne toujours nothing.

nothing valeur spéciale renvoyée par les fonctions vides (dites nulles).

composition (ou combinaison) utilisation d'une expression (ou d'une déclaration) dans le cadre d'une expression (ou d'une déclaration) plus large.

flux d'exécution ordre d'exécution des instructions.

diagramme de pile représentation graphique d'une pile de fonctions, de leurs variables et des valeurs auxquelles elles se réfèrent.

cadre rectangle dans un diagramme de pile qui représente un appel de fonction. Un cadre contient les variables locales et les paramètres de la fonction.

trace d'appel (ou stacktrace) liste des fonctions qui sont en cours d'exécution, imprimée lorsqu'une exception se produit. Ce terme a pour synonyme trace de pile, stack trace, stack backtrace ou encore stack traceback.

3-14 - Exercices

Ces exercices doivent être effectués en utilisant uniquement les déclarations et autres notions telles qu'étudiées jusqu'à présent.

3-14-1 - Exercice 3-2

Écrivez une fonction nommée rightjustify qui prend une chaîne de caractères nommée s comme paramètre et affiche la chaîne avec suffisamment d'espaces de tête pour que la dernière lettre de la chaîne(6) se trouve dans la colonne 70 de l'affichage.

1. julia> rightjustify("Julieta")

2. Julieta

Utilisez la concaténation et la répétition des chaînes de caractères. De plus, Julia fournit une fonction intégrée appelée length qui retourne la longueur d'une chaîne de caractères, de sorte que la valeur de length("Julieta") est 7.

3-14-2 - Exercice 3-3

Un objet de fonction est une valeur que vous pouvez attribuer à une variable ou passer en argument. Par exemple, dotwice est une fonction qui prend un objet de fonction comme argument et l'appelle deux fois :

1. function dotwice(f)

2. f()

3. f()

4. end

Voici un exemple qui utilise dotwice pour appeler deux fois une fonction intitulée printspam.

1. function printspam()

2. println("spam")

3. end

5. dotwice(printspam)

Saisissez cet exemple dans un script et testez-le.

1. Modifiez la fonction dotwice pour qu'elle prenne deux arguments, un objet fonction ainsi qu'une valeur, et qu'elle appelle la fonction deux fois en passant la valeur comme argument.

2. Dans votre script, copiez la définition de printtwice donnée plus haut dans ce chapitre (voir la section 3.7).

3. Utilisez la version modifiée de dotwice pour appeler printtwice deux fois, en passant "spam" comme argument.

4. Définissez une nouvelle fonction appelée dofour qui prend un objet de fonction et une valeur, puis qui appelle la fonction quatre fois en passant la valeur comme paramètre. Il ne doit y avoir que deux énoncés dans le corps de cette fonction et non quatre.

3-14-3 - Exercice 3-4

1. Rédigez une fonction grid qui affiche une grille telle que représentée dans la figure 3.14.1.

2. Écrivez une fonction qui dessine une grille similaire avec quatre lignes et quatre colonnes (ce problème est basé sur un exercice de Steve Oualline, Practical C Programming [6]).

Pour imprimer plus d'une valeur sur une ligne, vous pouvez imprimer une séquence de valeurs séparées par des virgules :

1. println("+", "-")

La fonction print ne passe pas à la ligne suivante :

1. print("+")

2. println("-")

Le résultat de ces deux dernières déclarations est "+ -" sur la même ligne. La sortie de la déclaration suivante commencerait à la ligne suivante.

FIGURE 3.14.1 – Grille associée à l'exercice 3.14.3 (partie 1).

4 - Étude de cas : Conception d'une interface

Ce chapitre présente une étude de cas illustrant la conception de fonctions travaillant collaborativement.

Il présente des graphiques « tortue » (Turtle), un moyen de créer des dessins à l'aide de programmes. Les graphiques « tortue » ne sont pas inclus dans la bibliothèque standard, donc le module ThinkJulia doit être ajouté à l'installation de Julia (avant de poursuivre, consultez l'annexe B, en particulier la sous-section B.2.3, qui traite de l'installation des modules Julia).

4-1 - Turtles

Un module est un fichier qui contient un ensemble de fonctions connexes. Julia fournit certains modules dans sa bibliothèque standard. Des fonctionnalités supplémentaires peuvent être ajoutées à partir d'une collection croissante de paquets. Le lecteur se référera à Julia Observer.

Les paquets peuvent être installés à partir du REPL en entrant dans le mode Pkg du REPL à l'aide de la touche ].

1. (@v1.5) pkg> add https://github.com/BenLauwens/ThinkJulia.jl

L'installation peut prendre du temps.

Avant de pouvoir utiliser les fonctions d'un module, nous devons importer ce dernier avec une déclaration d'utilisation :

1. using ThinkJulia

3. 🐢 = Turtle()

4. Turtle(0.0, 0.0, true, 0.0, (0.0, 0.0, 0.0))

Le module ThinkJulia fournit une fonction appelée Turtle qui crée un objet nommé Luxor.Turtle, que nous attribuons à une variable nommée 🐢 (:turtle: TAB). Une fois que nous avons créé une tortue, nous pouvons appeler une fonction pour la déplacer sur une feuille dessin. Par exemple, ce code fait avancer la tortue :

1. @svg begin

2. forward(🐢, 100)

3. end

4. width: 600.0

5. height: 600.0

6. filename: luxor-drawing-165611_608.svg

7. type: svg

En conséquence de quoi, un graphique — intitulé, en l'occurrence, luxor-drawing-165611_608.svg — s'affiche dans la partie Plots du REPL. Nous pouvons effectuer une recherche pour déterminer dans quel répertoire se trouve ce fichier (sous GNU/Linux : sudo updatedb et, ensuite en mode utilisateur, locate indiquera l'emplacement du fichier .svg). Le résultat devrait ressembler au dessin de la figure 4.1.1.

FIGURE 4.1.1 – Déplacement de la tortue en avant (du centre vers la droite).

Le mot-clé @svg permet à une macro de fonctionner, qui dessine une image SVG. Les macros sont une fonctionnalité importante, mais avancée de Julia. Les arguments de marche sont 🐢 et une distance exprimées en pixels. De ce fait, la taille réelle dépend de l'affichage de l'utilisateur.

turn est une autre fonction pouvant être sollicitée avec 🐢 comme premier argument et qui permet de réorienter la tortue. Le deuxième argument que prend turn est un angle exprimé en degrés.

De plus, chaque tortue tient un stylo, qui est soit vers le bas, soit vers le haut. Si le stylo est vers le bas, la tortue laisse une trace lorsqu'elle se déplace. A contrario, si le stylo est relevé, la tortue peut avancer, mais sans déposer de trace. Les fonctions penup et pendown signifient respectivement stylo baissé et stylo relevé.

Pour dessiner un angle droit, modifions l'appel de la macro :

1. @svg begin

2. forward(🐢, 100)

3. turn(🐢, -90)

4. forward(🐢, 100)

5. end

Nous devrions observer un dessin tel que représenté dans la figure 4.1.2.

FIGURE 4.1.2 – Un angle droit dessiné avec le module Turtle.

4-1-1 - Exercice 4-1

Modifiez la macro pour dessiner un carré. Ne continuez pas tant que vous n'arrivez pas au bon résultat.

4-2 - Répétitions simples

Il y a beaucoup de chances que vous ayez écrit ceci :

1. @svg begin

2. forward(🐢, 100)

3. turn(🐢, -90)

4. forward(🐢, 100)

5. turn(🐢, -90)

6. forward(🐢, 100)

7. turn(🐢, -90)

8. forward(🐢, 100)

9. end

Nous pouvons faire la même chose de manière plus concise avec une déclaration for :

1. julia> for i in 1:4

2. println("Hello!")

3. end

4. Hello!

5. Hello!

6. Hello!

7. Hello!

C'est l'utilisation la plus simple de la déclaration for. Nous en verrons davantage plus tard. Cependant, cela devrait suffire pour reformuler le programme de dessin de carré. Ne continuez pas tant que vous n'aurez pas réussi.

Voici une déclaration for qui dessine un carré :

1. 🐢 = Turtle()

2. @svg begin

3. for i in 1:4

4. forward(🐢, 100)

5. turn(🐢, -90)

6. end

7. end

La syntaxe d'une déclaration for est similaire à celle d'une définition de fonction. Elle comporte un en-tête et un corps qui se termine par le mot-clé end. Le corps peut contenir un nombre quelconque d'instructions. Une instruction for est également appelée boucle, car le flux d'exécution passe par le corps et revient ensuite en boucle vers le haut. Dans le cas présent, le corps est exécuté quatre fois.

Cette version est en fait un peu différente du précédent code de dessin de carré parce qu'elle effectue une rotation supplémentaire après avoir dessiné le dernier côté du carré. Cette rotation supplémentaire allonge le temps d'exécution, mais elle simplifie le code, parce que le programme exécute la même chose dans chaque boucle. Cette version a également pour effet de laisser la tortue dans la position de départ, tournée dans la direction initiale.

4-3 - Exercices

Voici une série d'exercices utilisant Turtle. Ils sont censés être amusants, mais ils ont aussi un but. Pendant que vous travaillez sur ces exercices, réfléchissez à l'intérêt qu'ils présentent.

Les sections suivantes présentent des solutions aux exercices. Ne regardez pas avant d'avoir terminé (ou du moins avant d'avoir vraiment essayé).

4-3-1 - Exercice 4-2

Écrivez une fonction appelée square qui prend un paramètre appelé t, qui est une tortue. Elle doit utiliser Turtle pour dessiner un carré.

4-3-2 - Exercice 4-3

Écrivez un appel de fonction qui passe t comme argument à square, puis exécutez à nouveau la macro.

4-3-3 - Exercice 4-4

Ajoutez à square un autre paramètre, nommé len. Modifiez le corps de manière à ce que la longueur des côtés soit len, puis modifiez l'appel de fonction pour fournir un deuxième argument. Exécutez à nouveau la macro. Testez le programme avec une plage de valeurs pour len.

4-3-4 - Exercice 4-5

Faites une copie de square et changez le nom en polygon. Ajoutez un autre paramètre nommé n et modifiez le corps pour qu'il dessine un polygone régulier à n côtés.

L'angle au centre des polygones réguliers à n-côtés vaut 360/n degrés. Par exemple, pour un dodécagone régulier, cet angle vaut 30°.

4-3-5 - Exercice 4-6

Écrivez une fonction appelée circle incluant comme paramètres une tortue t ainsi qu'un rayon r et qui dessine un cercle approximatif en appelant polygon avec une longueur et un nombre de côtés appropriés. Testez votre fonction avec une gamme de valeurs de r.

Calculez la circonférence du cercle et assurez-vous que len * n == circumference.

4-3-6 - Exercice 4-7

Développez une version plus générale de circle appelée arc qui prend un paramètre d'angle supplémentaire et qui détermine la fraction d'un cercle à dessiner. angle s'exprime en degrés. En conséquence, quand angle = 360, arc doit dessiner un cercle complet.

4-4 - Encapsulation

Le premier exercice demande de placer le code de dessin square dans une définition de fonction et d'appeler la fonction en passant la tortue comme paramètre. Voici une solution :

1. function square(t)

2. for i in 1:4

3. forward(t, 100)

4. turn(t, -90)

5. end

6. end

8. 🐢 = Turtle()

9. @svg begin

10. square(🐢)

11. end

Les déclarations forward et turn sont indentées deux fois pour montrer qu'elles se trouvent à l'intérieur de la boucle for figurant elle-même à l'intérieur de la définition de la fonction square.

À l'intérieur de la fonction square, t fait référence à la tortue 🐢, turn(t, -90) a donc le même effet que turn(🐢, -90). Dans ce cas, pourquoi ne pas appeler le paramètre 🐢 ? L'idée est que t peut être n'importe quelle tortue, pas seulement 🐢. Un autre animal qui effectue le même travail que 🐢 peut donc être créé et passé comme argument à square :

1. 🐫 = Turtle()

2. @svg begin

3. square(🐫)

4. end

Le procédé consistant à incorporer un morceau de code dans une fonction s'appelle l'encapsulation. Le premier avantage de l'encapsulation est d'associer un nom au code, qui sert en quelque sorte de documentation. Un autre avantage résulte du fait que lors de la réutilisation ultérieure du code, il devient plus concis d'appeler une fonction deux fois que d'en « copier-coller » le corps.

4-5 - Généralisation

L'étape suivante consiste à ajouter un paramètre len à square. Voici une solution :

1. function square(t, len)

2. for i in 1:4

3. forward(t, len)

4. turn(t, -90)

5. end

6. end

8. 🐢 = Turtle()

9. @svg begin

10. square(🐢, 100)

11. end

L'ajout d'un paramètre à une fonction s'appelle une généralisation. Dans la version précédente, le carré dessiné présente toujours la même taille. Dans la dernière version, sa taille peut varier.

L'étape suivante est également une généralisation. Au lieu de dessiner des carrés, polygon dessine des polygones réguliers avec un nombre quelconque de côtés. Voici une solution :

1. function polygon(t, n, len)

2. angle = 360 / n

4. for i in 1:4

5. forward(t, len)

6. turn(t, -angle)

7. end

8. end

10. 🐢 = Turtle()

11. @svg begin

12. polygon(🐢, 7, 70)

13. end

Ce code permet de dessiner un polygone à 7 côtés, chacun d'eux ayant une longueur de 70 pixels.

4-6 - Conception d'une interface

L'étape suivante consiste à écrire circle, qui prend un rayon, r, comme paramètre. Voici une solution simple qui utilise polygon pour dessiner un polygone à 50 côtés :

1. function circle(t, r)

2. circumference = 2 * π * r

3. n = 50

4. len = circumference / n

5. polygon(t, n, len)

6. end

La première ligne calcule la circonférence d'un cercle de rayon r en utilisant la formule . Le paramètre n est le nombre de segments dans l'approximation d'un cercle, len est donc la longueur de chaque segment. Ainsi, la fonction polygon dessine-t-elle un polygone à 50 côtés qui se rapproche d'un cercle de rayon r.

Une des limites de cette solution vient de ce que n est une constante, ce qui signifie que, pour les très grands cercles, les segments de droite sont trop longs. En revanche, pour les petits cercles, nous perdons du temps à dessiner de très petits segments. Une solution consisterait à généraliser la fonction en prenant n comme paramètre. Cela donnerait à l'utilisateur (celui qui appelle circle) plus de contrôle, mais l'interface serait moins propre.

L'interface d'une fonction est un résumé de son utilisation : quels sont les paramètres ? Que fait la fonction ? Quelle est la valeur retournée ? Une interface est « propre » si elle permet à l'utilisateur de faire ce qu'il souhaite sans avoir à s'occuper de détails inutiles.

Dans cet exemple, r est un membre de l'interface, car ce paramètre conditionne le cercle à dessiner. Le paramètre n est moins approprié dans la mesure où il concerne les détails relatifs à la façon dont le cercle doit être rendu.

Plutôt que d'encombrer l'interface, il est préférable de choisir une valeur appropriée de n en fonction de la circonférence :

1. function circle(t, r)

2. circumference = 2 * π * r

3. n = trunc(circumference / 3) + 3

4. len = circumference / n

5. polygon(t, n, len)

6. end

Désormais, le nombre de segments est un nombre entier proche de la valeur de circumference / 3 et la longueur de chaque segment vaut 3. Cette valeur est un bon compromis : assez petite pour que les cercles présentent une allure convenable, tout en étant assez grande pour être efficace et, qui plus est, acceptable pour toute taille de cercle. L'addition de 3 à n garantit que le polygone ait au moins trois côtés.

4-7 - Refonte (ou refactoring)

Quand nous avons écrit circle, il a été possible de réutiliser polygon, car un polygone à grand nombre de côtés est une bonne approximation d'un cercle. Si nous voulions tracer un arc, pourrait-on utiliser polygon ou circle ? Cela demande un peu de travail.

Une possibilité est de commencer avec une copie de polygon et de la transformer en arc. Le résultat ressemblerait à ceci :

1. function arc(t, r, angle)

2. arc_length = 2 * π * r * (angle / 360)

3. n = trunc(arc_len / 3) + 1

4. step_len = arc_len / n

5. step_angle = angle / n

6. for i in 1:n

7. forward(t, step_len)

8. turn(t, -step_angle)

9. end

10. end

La deuxième moitié de cette fonction ressemble à polygon. Malheureusement, on ne peut pas réutiliser polygon sans en changer l'interface. Nous pourrions généraliser polygon pour prendre un angle comme troisième argument. Cependant, polygon ne serait plus un nom approprié. En conséquence, renommons de manière plus générale cette fonction en polyline :

1. function polyline(t, n, len, angle)

2. for i in 1:n

3. forward(t, len)

4. turn(t, -angle)

5. end

6. end

À présent, nous pouvons réécrire polygon et arc pour tirer parti de polyline :

1. function polygon(t, n, len)

2. angle = 360 / n

3. polyline(t, n, len, angle)

4. end

6. function arc(t, r, angle)

7. arc_length = 2 * π * r * (angle / 360)

8. n = trunc(arc_len / 3) + 1

9. step_len = arc_len / n

10. step_angle = angle / n

11. polyline(t, n, step_len, step_angle)

12. end

Enfin, nous pouvons réécrire circle afin d'utiliser arc :

1. function circle(t, r)

2. arc(t, r, 360)

3. end

Ce processus, qui consiste à réorganiser un programme pour améliorer les interfaces et faciliter la réutilisation du code, s'appelle la refonte (ou refactoring). Dans le cas présent, nous avons remarqué un code similaire dans arc et polygon, nous l'avons donc « remanié » (refondu) en polyline.

Si nous avions planifié la programmation, nous aurions peut-être écrit polyline au premier jet et évité la refonte. Souvent au début d'un projet, on n'en connait pas assez pour concevoir les interfaces optimales. Lorsqu'on entame le codage, le problème est mieux cerné. Parfois, la refonte est le signe d'une compréhension approfondie d'un problème.

4-8 - Plan de développement

Un plan de développement est un processus de rédaction de programmes. Celui que nous avons utilisé dans cette étude de cas est constitué de deux étapes : l'encapsulation et la généralisation.

Les étapes de ce processus sont les suivantes :

1. Commencer par écrire un petit programme sans définition de fonction ;

2. Une fois le programme opérationnel, identifier une partie cohérente de celui-ci. L'encapsuler dans une fonction et le nommer ;

3. Généraliser la fonction en ajoutant les paramètres appropriés ;

4. Répéter les étapes 1 à 3 jusqu'à obtenir un ensemble de fonctions opérationnelles. Copier et coller le code de travail (pour éviter des saisies et déboguer à nouveau) ;

5. Chercher des possibilités d'améliorer le programme en le remaniant (processus de refonte). Par exemple, avec un code similaire à plusieurs endroits, envisager de le refondre dans une fonction générale idoine.

Cette manière de procéder présente quelques inconvénients — nous verrons les autres options plus tard —, mais il s'avère utile si le programmeur ignore comment diviser le programme en fonctions. Cette approche permet de concevoir un programme de manière progressive.

4-9 - Documentation interne

Une documentation interne brève (docstring) est un bloc de commentaires situé avant une fonction pour en expliciter l'interface :

1. """

2. Draws n line segments with the given length and

3. angle (in degrees) between them. t is a turtle.

4. """

5. function polyline(t, n, len, angle)

6. for i in 1:n

7. forward(t, len)

8. turn(t, -angle)

9. end

10. end

On peut accéder à la documentation dans le REPL en saisissant le caractère ? suivi du nom d'une fonction ou d'une macro et en appuyant sur ENTER :

1. help?> polyline

2. search

4. polyline(t, n, len, angle)

5. Draws n line segments with the given length and angle (in degrees) between them. t is a turtle.

La documentation brève consiste souvent en des chaînes précédées et suivies d'un triple ", également appelées chaînes multilignes, car les triples guillemets permettent à la chaîne de s'étendre sur plus d'une ligne.

Une documentation brève contient les informations essentielles qu'un utilisateur peut requérir pour utiliser correctement une fonction. Elle explique de manière concise ce que fait la fonction, sans toutefois entrer dans les détails techniques. Elle explique l'effet de chaque paramètre sur le comportement de la fonction et le type de chaque paramètre (quand cela n'est pas évident).

4-10 - Débogage

Une interface s'apparente à un « contrat » entre une fonction et un appelant. L'appelant accepte de fournir certains paramètres et la fonction consent à effectuer certains travaux.

Par exemple, polyline nécessite quatre arguments : t doit être de « type » Turtle, n un nombre entier, len un nombre positif et angle un nombre exprimé en degrés.

Ces exigences sont appelées conditions préalables (ou a priori), car elles sont supposées être vraies avant que la fonction ne commence à s'exécuter. A contrario, les conditions résultant de l'exécution de la fonction sont dites a posteriori. Ces conditions a posteriori comprennent l'effet prévu de la fonction (comme les segments de ligne de dessin) et tout effet secondaire (comme le déplacement de la tortue ou d'autres modifications). Les conditions préalables sont de la responsabilité de l'appelant. Si l'appelant viole une condition préalable (correctement documentée) et que la fonction ne donne pas le résultat attendu, le bogue se trouve chez l'appelant et non dans la fonction. Si les conditions préalables sont satisfaites et que les conditions a posteriori ne le sont pas, le bogue se trouve dans la fonction. Si les conditions préalables et les conditions a posteriori sont claires, elles peuvent aider au débogage.

4-11 - Glossaire

module fichier qui contient une collection de fonctions connexes et diverses définitions.

paquet bibliothèque externe avec des fonctionnalités supplémentaires.

déclaration d'utilisation déclaration qui lit un fichier de module et crée un objet de module.

boucle partie d'un programme qui peut être exécutée de manière répétitive.

encapsulation processus de transformation d'une séquence d'instructions en une définition de fonction.

généralisation processus consistant à remplacer un élément inutilement spécifique (comme un nombre) par élément suffisamment général (comme une variable ou un paramètre).

interface description de la manière d'utiliser une fonction, y compris le nom ainsi que la description des arguments et de la valeur de retour.

refonte (refactoring) processus de modification d'un programme pour améliorer les interfaces des fonctions et d'autres qualités du code.

plan de développement processus de conception et de rédaction de programmes.

documentation courte (docstring) chaîne qui apparaît juste avant la définition d'une fonction pour documenter l'interface de cette dernière.

condition préalable (ou a priori) exigence qui doit être satisfaite par l'appelant avant le début d'une fonction.

condition a posteriori exigence qui doit être satisfaite par la fonction avant qu'elle ne prenne fin.

4-12 - Exercices

4-12-1 - Exercice 4-8

À moins d'utiliser des scripts, inscrivez le code de ce chapitre dans un carnet Jupyter ou dans Pluto.

1. Dessinez un diagramme de pile qui montre l'état du programme pendant l'exécution de circle(🐢, radius). Vous pouvez faire l'arithmétique à la main ou ajouter des commentaires au code.

2. La version d'arc dans la section 4.7 n'est pas très précise, car l'approximation linéaire du cercle se fait toujours en dehors du cercle vrai. Par conséquent, la tortue se retrouve à quelques pixels de sa destination correcte. Ci-dessous, une solution qui illustre un moyen de réduire l'effet de cette erreur. Lisez le code et voyez si cela vous semble logique. Si vous dessinez un diagramme de pile, vous appréhenderez très probablement le fonctionnement.

1. """

2. Trace un arc d'un rayon et d'un angle donnés:

3. t: turtle

4. r: rayon

5. angle: angle sous-tendu par l'arc, en degrés

6. """

7. function arc(t, r, angle)

8. arc_len = 2 * π * r * abs(angle) / 360

9. n = trunc(arc_len / 4) + 3

10. step_len = arc_len / n

11. step_angle = angle / n

13. # faire un léger virage à gauche avant de démarrer

14. # réduit l'erreur causée par l'approximation linéaire de l'arc

15. turn(t, -step_angle/2)

16. polyline(t, n, step_len, step_angle)

17. turn(t, step_angle/2)

18. end

4-12-2 - Exercice 4-9

Rédigez un ensemble de fonctions générales appropriées pour dessiner des fleurs telles que celles-ci :

4-12-3 - Exercice 4-10

Rédigez un ensemble de fonctions générales appropriées pour dessiner des formes :

4-12-4 - Exercice 4-11

Les lettres de l'alphabet peuvent être construites à partir d'un nombre restreint d'éléments de base, comme des lignes verticales et horizontales ainsi que quelques courbes. Concevez un alphabet qui peut être dessiné avec un nombre minimal d'éléments de base et écrivez ensuite des fonctions qui dessinent les lettres.

Vous devez écrire une fonction pour chaque lettre, avec les noms draw_a, draw_b, etc., et mettre vos fonctions dans un fichier nommé letters.jl.

4-12-5 - Exercice 4-12

Consultez cette page Wikipédia afin d'apprendre des informations sur les spirales. Ensuite, écrivez un programme qui dessine une spirale d'Archimède telle que ceci :

5 - Conditions et récursion

Le sujet principal de ce chapitre est la déclaration if, qui exécute un code différent selon l'état du programme. Auparavant, considérons deux nouveaux opérateurs : la division euclidienne ÷ (\div TAB)(7) et le modulo.

5-1 - Division euclidienne (entière) et modulo

L'opérateur de division euclidienne ou entière ÷ divise deux nombres et arrondit au quotient, c'est-à-dire au nombre entier inférieur. Par exemple, supposons que la durée d'un film soit de 105 minutes. Combien de temps en heures cela représente-t-il ? La division conventionnelle / donne un nombre à virgule flottante :

1. julia> minutes = 105

2. 105

3. julia> minutes / 60

4. 1.75

Or, nous n'écrivons normalement pas les heures avec des points décimaux. La division entière ÷ retourne le quotient :

1. julia> heures = minutes ÷ 60

2. 1

Pour obtenir le reste, nous pourrions pratiquer ainsi :

1. julia> reste = minutes - heures * 60

2. 45

Une autre solution consiste à utiliser l'opérateur modulo, %, qui divise deux nombres et retourne le reste.

1. julia> reste = minutes % 60

2. 45

L'opérateur modulo est plus utile qu'il n'y paraît, par exemple, dans le cas où il faut déterminer si un nombre est divisible par un autre. Si x % y vaut zéro, alors x est divisible par y. L'opérateur modulo est commode pour extraire le ou les chiffres les plus à droite d'un nombre. Par exemple, en base 10, x % 10 donne le chiffre le plus à droite d'un nombre entier x. De même, x % 100 donne les deux derniers chiffres.

5-2 - Expressions booléennes

Une expression booléenne est une expression qui est soit vraie, soit fausse. Les exemples suivants utilisent l'opérateur ==, qui compare deux opérandes et retourne true s'ils sont égaux et false sinon :

1. julia> 5 == 5

2. true

3. julia> 5 == 6

4. false

true et false sont des valeurs spéciales qui appartiennent au type Bool. Il ne s'agit en rien de chaînes de caractères :

1. julia> typeof(true)

2. Bool

3. julia> typeof(false)

4. Bool

L'opérateur == fait partie des opérateurs relationnels (voir tableau 5.1).

Bien que ces opérations soient probablement familières, les symboles de Julia sont différents des symboles mathématiques. Une erreur courante consiste à utiliser un signe égal simple (=) au lieu d'un signe égal double (==). Il faut garder à l'esprit que = est un opérateur d'affectation et que == est un opérateur relationnel. Il n'existe pas de =< ou de =>.

TABLE 5.1 - Liste des opérateurs relationnels.

Opérateurs

Signification

x == y

# x est égal à y

x != y

# x n'est pas égal à y

x ≠ y

# (\ne TAB)

x > y

# x est plus grand que y

x < y

# x est plus petit que y

x >= y

# x est plus grand ou égal à y

x ≥ y

# (\ge TAB)

x <= y

# x est plus petit ou égal à y

x ≤ y

# (\le TAB)

5-3 - Opérateurs logiques

Il y a trois opérateurs logiques : && (et), || (ou) et ! (non). La signification de ces opérateurs est la même que dans le langage courant. Par exemple, x > 0 && x < 10 n'est vrai que si x appartient au segment fermé [1,9].

L'expression n % 2 == 0 || n % 3 == 0 est vraie si l'une ou l'autre des conditions (ou les deux) est vraie, c'est-à-dire si le nombre est divisible par 2 ou par 3.

Les deux && et || s'associent à droite, mais && a une priorité plus élevée que ||.

Enfin, l'opérateur ! annule une expression booléenne, donc !(x > y) est vrai si x > y est faux, c'est-à-dire si x est inférieur ou égal à y.

5-4 - Exécution conditionnelle

Afin d'écrire des programmes utiles, il est presque toujours nécessaire de pouvoir vérifier les conditions et modifier le comportement du programme en conséquence. Les déclarations conditionnelles nous offrent cette capacité. La forme la plus simple est la déclaration if :

1. if x > 0

2. println("x est positif")

3. end

L'expression booléenne après if est appelée une condition. Si elle est vraie, les instructions du bloc indenté sont exécutées. Sinon, rien ne se passe.

Les instructions if ont la même structure que les définitions de fonctions : un en-tête suivi d'un corps terminé par le mot-clé end. Les instructions de ce type sont dites composées.

Il n'y a pas de limite au nombre d'instructions formant le corps. Parfois, il est utile d'avoir un corps sans déclaration (généralement pour réserver la place pour un code non encore écrit).

1. if x < 0

2. # À FAIRE gérer les valeurs négatives!

3. end

5-5 - Exécution alternative

Une deuxième forme de la déclaration if consiste en une « exécution alternative » avec deux possibilités. La condition détermine quelle branche de l'alternative doit être exécutée. La syntaxe est :

1. if x % 2 == 0

2. println("x est pair")

3. else

4. println("x est impair")

5. end

Lorsque x est divisible par 2, le programme affiche x est pair. Si la condition est fausse, la deuxième série d'instructions est exécutée. Comme la condition doit être vraie ou fausse, l'une des branches de l'alternative est toujours exécutée.

5-6 - Enchaînement de conditions

Il arrive souvent qu'un programme requière plus de deux branches. Une manière de procéder consiste à exploiter les enchaînements de conditions avec le mot-clé elseif :

1. if x < y

2. println("x est inférieur à y")

3. elseif x > y

4. println("x est supérieur à y")

5. else

6. println("x est égal à y")

7. end

À nouveau, seule une branche fonctionnera. Il n'y a pas de limite au nombre de déclarations elseif. S'il y a une clause else, elle doit se trouver à la fin, mais n'est pas nécessaire.

1. if choice == "a"

2. draw_a

3. elseif choice == "b"

4. draw_b

5. elseif choice == "c"

6. draw_c

7. end

Chaque condition est examinée dans l'ordre. Si la première est fausse, la suivante est vérifiée et ainsi de suite. Si l'une d'entre elles est vraie, la branche correspondante est parcourue et la déclaration se termine. Même si plus d'une condition est vraie, seule la première branche vraie est exécutée.

5-7 - Imbrication de conditions

Une condition peut être imbriquée dans une autre. L'exemple de la section précédente peut s'écrire ainsi :

1. if x == y

2. println("x est égal à y")

3. else

4. if x < y

5. println("x est inférieur à y")

6. else

7. println("x est égal à y")

8. end

9. end

La condition « extérieure » contient deux branches. La première branche contient une seule instruction. La deuxième branche renferme un autre bloc if, qui lui-même comprend deux branches. Ces deux branches contiennent des instructions simples, bien qu'elles puissent également être des instructions conditionnelles à leur tour.

Même lorsque l'indentation est scrupuleusement observée, les conditions imbriquées deviennent très rapidement difficiles à lire. Il est bon de les éviter quand c'est possible.

Les opérateurs logiques permettent souvent de simplifier les déclarations conditionnelles imbriquées. Par exemple, le code suivant peut être reformulé en utilisant une seule condition :

1. if 0 < x

2. if x < 10

3. println("x est un nombre positif")

4. end

5. end

L'instruction d'affichage ne fonctionne que si les deux conditions sont passées. Le même résultat peut être obtenu avec l'opérateur && :

1. if 0 < x && x < 10

2. println("x est un nombre positif")

3. end

5-8 - Récursion

Il est permis qu'une fonction en appelle une autre et aussi qu'elle s'appelle elle-même. C'est un des aspects les plus « magiques » de la programmation. Considérons, par exemple, la fonction suivante :

1. function countdown(n)

2. if n ≤ 0

3. println("mise à feu !")

4. else

5. print(n, " ")

6. countdown(n-1)

7. end

8. end

Si n vaut 0 ou est négatif, le programme affiche "mise à feu !". Sinon, il affiche n suivi d'un espace. Ensuite, le programme appelle une fonction appelée countdown (compte à rebours) se passant à elle même n-1 comme argument.

Que se passe-t-il si nous appelons cette fonction comme ceci ?

1. julia> countdown(3)

2. 3 2 1 "mise à feu!"

L'exécution de countdown commence avec n = 3 et, comme n est supérieur à 0, le programme passe à l'intérieur du else et affiche la valeur 3 suivie d'un espace. L'exécution de la fonction countdown s'appelle elle-même et se poursuit avec n = 2. Puisque n est supérieur à 0, le programme passe à nouveau dans la partie else ; il affiche la valeur 2 suivie d'un espace. Le programme continue avec un autoappel de countdown avec n = 1. Puisque n est supérieur à 0, le programme passe à nouveau dans la partie else ; il affiche la valeur 1 suivie d'un espace. Le cycle se prolonge avec un autoappel de countdown qui se poursuit avec n = 0. Cette fois, comme n n'est pas supérieur à 0, le programme affiche l'expression "mise à feu !".

Une fonction qui s'appelle elle-même est récursive ; le processus qui y est associé se nomme une récursion.

Voici un autre exemple d'une fonction qui affiche n fois une chaîne de caractères à l'écran :

1. function printn(s, n)

2. if n ≤ 0

3. return

4. end

5. println(s)

6. printn(s, n-1)

7. end

Si n ≤ 0, la déclaration return permet de quitter la fonction. Le flux d'exécution revient immédiatement à l'instruction appelante. Donc, le programme s'arrête et les autres lignes de la fonction ne sont pas exécutées. Par exemple, imaginons la fonction appelante suivante :

1. printn("bonjour", 2)

2. bonjour

3. bonjour

Du fait que n = 2, le programme affiche une première fois bonjour. Puis, il appelle printn avec n = 1. Cet autoappel correspond à printn("bonjour", 1). Le programme affiche une fois encore bonjour. Puis, il appelle printn avec n = 0. En rentrant une troisième fois dans la fonction, le programme constate que la condition n ≤ 0 est remplie : il effectue un return.

Pour des exemples simples comme celui-ci, il est probablement plus facile d'utiliser une boucle for. Cependant, nous verrons plus tard des exemples difficiles à écrire avec une boucle for et beaucoup plus faciles à rédiger en invoquant une récursion.

5-9 - Diagrammes de pile pour les fonctions récursives

Dans la section 3.9, nous avons utilisé un diagramme de pile pour représenter l'état d'un programme lors d'un appel de fonction. Le même type de diagramme peut aider à interpréter une fonction récursive.

Chaque fois qu'une fonction est appelée, Julia crée un cadre pour contenir les variables et paramètres locaux de la fonction. Pour une fonction récursive, il peut y avoir plus d'un cadre sur la pile en même temps. La figure 5.9.1 montre un diagramme de pile pour la fonction countdown (compte à rebours) appelée avec n = 3.

FIGURE 5.9.1 – Diagramme de pile pour countdown appelé avec n = 3.

Comme d'habitude, le haut de la pile est le cadre de Main. Il est vide parce qu'aucune variable n'a été instanciée dans Main et qu'aucun argument ne lui a été passé. Les quatre cadres de countdown ont des valeurs différentes pour le paramètre n. Le bas de la pile, où n = 0, est appelé le cas de base. Aucun appel récursif n'a lieu pour n = 0, il n'y a donc plus de cadres en dessous.

5-9-1 - Exercice 5-1

En guise d'exercice, dessinez un diagramme de pile pour une fonction printn appelée avec s = "bonjour" et n = 2. Ensuite, écrivez une fonction appelée do_n qui prend un objet de fonction et un nombre n comme argument, et qui appelle la fonction n fois.

5-10 - Récursion infinie

Si une récursion n'atteint jamais le cas de base, elle continue à faire des appels récursifs indéfiniment : le programme ne se termine jamais. Il s'agit là d'une récursion infinie, ce qui constitue un mauvais cas de figure. Voici un programme minimal avec une récursion infinie :

1. function recurse()

2. recurse()

3. end

Dans la plupart des environnements de programmation, un programme à récursion infinie ne fonctionne pas indéfiniment. Julia envoie un message d'erreur lorsque la profondeur de récursion maximale est atteinte :

1. julia> recurse()

2. ERROR: StackOverflowError:

3. Stacktrace:

4. [1] recurse() at ./REPL[1]:2 (repeats 80000 times)

La trace d'appels (appelée encore trace de pile ou stacktrace) est un peu plus grande que celle que nous avons vue dans le chapitre 3. Lorsque l'erreur se produit, il y a 80 000 images récurrentes dans la pile.

Lorsqu'une récursion infinie est rencontrée accidentellement, la fonction incriminée doit être analysée pour confirmer qu'un cas de base ne fait pas d'appel récursif. Si un cas de base existe, il est nécessaire de vérifier que son accès est garanti.

5-11 - Saisie au clavier

Les programmes que nous avons écrits jusqu'ici n'acceptent aucune entrée de la part de l'utilisateur. Leur exécution produit toujours le même résultat.

Julia fournit une fonction interne appelée readline qui arrête le programme et attend que l'utilisateur saisisse une information. Lorsque l'utilisateur presse la touche ENTER, le programme reprend et readline transmet l'information que l'utilisateur a saisie sous forme de chaîne de caractères.

1. julia> texte = readline()

2. Qu'attendez-vous?

3. "Qu'attendez-vous?"

Avant d'obtenir la contribution de l'utilisateur, il est conseillé d'afficher un message lui indiquant ce qu'il doit saisir :

1. julia> print("Quel est votre prénom? "); readline()

2. Quel est votre prénom? Thierry

3. "Thierry"

Un point-virgule ; permet de placer plusieurs déclarations sur la même ligne. Dans le REPL, seul le dernier énoncé retourne sa valeur.

S'il est attendu de l'utilisateur qu'il saisisse un entier, il convient de convertir la valeur transmise en Int64 :

1. julia> print("Quelle est la célérité du son dans l'air au niveau de la mer (m/s)?"); c = readline()

2. Quelle est la célérité du son dans l'air au niveau de la mer (m/s)? 340

3. "340"

4. julia> parse(Int64, c)

5. 340

Toutefois, si l'utilisateur saisit autre chose qu'une suite de chiffres, une erreur apparaît à la conversion :

1. julia> print("Quelle est la célérité du son dans l'air au niveau de la mer (m/s)?"); c = readline()

2. Quelle est la célérité du son dans l'air au niveau de la mer (m/s)? Que signifie célérité?

3. "Que signifie célérité?"

4. julia> parse(Int64, c)

5. ERROR: ArgumentError invalid base 10 digit 'C' in "Célérité?"

6. […]

Ultérieurement, nous verrons comment gérer ce genre d'erreur.

5-12 - Débogage

Lorsqu'une erreur de syntaxe ou d'exécution se produit, le message d'erreur contient beaucoup d'informations, mais il peut être surchargé. Les parties les plus utiles sont généralement :

- quel type d'erreur s'est-il produit ?

- à quel endroit du programme l'erreur s'est-elle produite ?

Les erreurs de syntaxe sont généralement faciles à trouver, mais quelques astuces existent. En général, les messages d'erreur indiquent l'endroit où le problème a été découvert, mais l'erreur réelle peut se situer en amont dans le code, parfois sur la ligne précédente.

Il en va de même pour les erreurs d'exécution. Supposons que nous tentions de calculer un rapport signal/bruit en décibels (Signal-to-Noise Ratio, SNR). La formule est la suivante :

où représente l'amplitude. En Julia, il est possible d'écrire ce code :

1. signal_power = 9

2. noise_power = 10

3. ratio = signal_power ÷ noise_power

4. decibels = 10 * log10(ratio)

5. print(decibels)

Cependant, Julia retourne :

1. -Inf

Ce n'est pas le résultat attendu. Pour trouver l'erreur, il pourrait être utile d'afficher la valeur de ratio, qui s'avère valoir 0. Le problème se situe à la ligne 3, qui utilise la division euclidienne ÷ au lieu de la division décimale (/).

Il est nécessaire de prendre le temps de lire attentivement les messages d'erreur, bien qu'ils ne soient pas forcément explicites.

5-13 - Glossaire

division euclidienne opération, notée ÷, qui divise deux nombres et les arrondit au nombre entier inférieur (à la différence de la division décimale, qui retourne un quotient à virgule flottante).

opérateur modulo opérateur, désigné par le signe pourcentage (%), qui fonctionne sur des nombres entiers et retourne le reste lorsqu'un nombre est divisé par un autre.

expression booléenne expression dont la valeur est soit vraie (true), soit fausse (false).

opérateur relationnel un des opérateurs qui comparent deux opérandes : ==, ≠ (!=), >, <, ≥ (>=) et ≤ (<=).

opérateur logique un des opérateurs qui combinent les expressions booléennes : && (et), || (ou) et ! (non).

déclaration conditionnelle déclaration qui contrôle le flux d'exécution en fonction de certaines conditions.

condition expression booléenne dans une déclaration conditionnelle qui détermine quelle branche emprunter.

instruction composée instruction complexe qui se compose d'un en-tête et d'un corps. Le corps est terminé par le mot-clé end.

branche une des options d'instructions dans une déclaration conditionnelle.

chaîne conditionnelle déclaration conditionnelle avec une série de branches alternatives.

imbrication conditionnelle déclaration conditionnelle qui apparaît dans une des branches d'une autre déclaration conditionnelle.

instruction de retour instruction qui fait qu'une fonction s'arrête immédiatement et revient à l'appelant.

récursion processus d'autoappel d'une fonction en cours d'exécution.

cas de base branche conditionnelle dans une fonction récursive qui n'effectue pas d'appel récursif.

récursion infinie récursion qui n'a pas de cas de base ou qui ne l'atteint jamais. Finalement, une récursion infinie provoque une erreur d'exécution.

5-14 - Exercices

5-14-1 - Exercice 5-2

La fonction time retourne le temps actualisé, au méridien de Greenwich en secondes depuis une date de référence arbitraire qui, sur les systèmes , est le 1er janvier 1970.

1. julia> time()

2. 1.602969109299345e9

Écrivez un script qui lit l'heure actuelle puis la convertit en jours, heures, minutes et secondes depuis la date de référence.

5-14-2 - Exercice 5-3

Selon le dernier théorème de Fermat, il n'existe pas d'entiers positifs , et tel que pour toute valeur de > 2 :

1. Écrivez une fonction appelée checkfermat qui prend quatre paramètres (, , et ) et qui vérifie si le théorème de Fermat est valide. Si n est supérieur à 2 et que a^n + b^n == c^n, le programme doit afficher « Diantre, Fermat avait tort ! ». Sinon, le programme doit afficher « Non, ça ne fonctionne pas… ».

2. Écrivez une fonction qui invite l'utilisateur à entrer des valeurs pour a, b, c et n, les convertit en nombres entiers et utilise checkfermat pour vérifier s'ils violent le théorème de Fermat.

5-14-3 - Exercice 5-4

Avec trois bâtonnets, vous pouvez éventuellement construire un triangle. Par exemple, si l'un des bâtonnets mesure 12 cm de long et les deux autres 1 cm de long, vous ne pourrez pas faire se rencontrer les bâtonnets courts. Pour trois longueurs quelconques, il existe un test simple permettant de déterminer s'il est possible de former un triangle :

Si une des trois longueurs est supérieure à la somme des deux autres, un triangle sera impossible à former. Par ailleurs, si la somme de deux longueurs est égale à la troisième, nous obtenons un triangle dégénéré.

1. Écrivez une fonction appelée istriangle qui prend trois entiers comme arguments et qui imprime soit « Oui » soit « Non », selon que vous pouvez ou non former un triangle à partir de bâtonnets ayant les longueurs entrées.

2. Écrivez une fonction qui invite l'utilisateur à entrer trois longueurs de bâtonnets, les convertit en nombres entiers et utilise istriangle pour vérifier si les bâtonnets ayant les longueurs données peuvent former un triangle.

5-14-4 - Exercice 5-5

Quel est le résultat du programme suivant ? Dessinez un diagramme de pile qui montre l'état du programme lorsqu'il affiche le résultat.

1. function recurse(n, s)

2. if n == 0

3. println(s)

4. else

5. recurse(n-1, n+s)

6. end

7. end

9. recurse(3, 0)

Que se passerait-il si l'appel de fonction devenait : recurse(-1, 0) ?

Écrivez une chaîne de caractères qui explique tout ce que quelqu'un doit savoir pour utiliser cette fonction (et rien d'autre).

Les exercices suivants utilisent le module ThinkJulia, décrit dans le chapitre 4.

5-14-5 - Exercice 5-6

Lisez la fonction suivante. Pouvez-vous déterminer ce qu'elle effectue (voir les exemples dans le chapitre 4) ? Ensuite, exécutez-la pour voir si vous avez bien compris.

1. function draw(t, length, n)

2. if n == 0

3. return

4. end

5. angle = 50

6. forward(t, length*n)

7. turn(t, -angle)

8. draw(t, length, n-1)

9. turn(t, 2*angle)

10. draw(t, length, n-1)

11. turn(t, -angle)

12. forward(t, -length*n)

13. end

6 - Fonctions avec retour

De nombreuses fonctions de Julia, telles que les fonctions mathématiques (dont nous avons vu un échantillon), produisent des valeurs de retour. Cependant, toutes les fonctions que nous avons écrites sont vides (ou nulles) : elles ont un effet comme l'affichage d'une valeur ou le déplacement d'une tortue, mais elles ne retournent rien. Ce chapitre aborde les fonctions avec retour.

6-1 - Valeurs retournées

Dans ce cas, l'appel d'une fonction retourne une valeur que nous attribuons généralement à une variable ou que nous utilisons dans le cadre d'une expression. Par exemple :

1. e = exp(1.0)

2. height = radius * sin(radians)

Le premier exemple d'une fonction avec retour est area, qui retourne l'aire d'un cercle de rayon donné :

1. function area(radius)

2. a = π * radius^2

3. return a

4. end

Nous avons déjà vu la déclaration return. Cependant, dans une fonction avec retour, l'instruction return comprend une expression. Cette instruction signifie littéralement : « Quitter immédiatement la fonction et utiliser l'expression qui suit return comme valeur de retour ». L'expression peut être plus ou moins compliquée. Ainsi, la fonction area aurait pu être plus concise :

1. function area(radius)

2. π * radius^2

3. end

En effet, la valeur retournée par une fonction est la valeur de la dernière expression évaluée qui, par défaut, est la dernière expression figurant dans le corps de la définition de la fonction.

Cependant, des variables temporaires comme a (deuxième cadre de cette section) et des déclarations de retour explicites facilitent grandement le débogage. Il est parfois même utile d'avoir plusieurs déclarations de retour, une dans chaque branche conditionnelle :

1. function absvalue(x)

2. if x < 0

3. return -x

4. else

5. return x

6. end

7. end

Comme ces instructions return sont dans une branche conditionnelle alternative, une seule des options est empruntée. Dès qu'une déclaration return est exécutée, la fonction se termine sans exécuter d'instructions ultérieures. Le code qui apparaît après une instruction return ou après toute autre partie que le flux d'exécution ne peut jamais atteindre est appelé un code mort.

Dans une fonction avec retour, il est bon de s'assurer que tous les chemins possibles du programme aboutissent à une instruction return. Par exemple :

1. function absvalue(x)

2. if x < 0

3. return -x

4. end

5. if x > 0

6. return x

7. end

8. end

Cette fonction est incorrecte, car, si x vaut 0, aucune des deux conditions n'est avérée, si bien que la fonction se termine sans atteindre une instruction return. Si le flux d'exécution arrive ainsi à la fin d'une fonction, la valeur retournée est nothing, ce qui ne correspond pas à la valeur absolue de 0. Ceci est vérifiable :

1. julia> show(absvalue(0))

2. nothing

Julia fournit une fonction interne abs qui calcule les valeurs absolues.

6-1-1 - Exercice 6-1

Écrivez une fonction compare qui prend deux valeurs, x et y, et retourne 1 si x > y, 0 si x == y, et -1 si x < y.

6-2 - Développement progressif

À mesure que les fonctions deviennent de plus en plus volumineuses, le temps de débogage s'allonge. Pour faire face à des programmes de plus en plus complexes, il est astucieux de tirer parti d'un procédé appelé développement progressif ou incrémental. L'objectif est d'éviter de longues sessions de débogage en ajoutant et en testant individuellement un code de petite taille.

Par exemple, supposons que vous vouliez trouver la distance entre deux points, donnée par les coordonnées et . Selon le théorème de Pythagore, la distance est donnée par :

La première étape consiste à examiner à quoi devrait ressembler une fonction distance en Julia. En d'autres termes, quelles sont les entrées (paramètres) et quelle est la sortie (valeur de retour) ?

Dans ce cas, les entrées sont deux points, que vous pouvez représenter à l'aide de quatre nombres. La valeur de retour est la distance représentée par une valeur en virgule flottante.

Dans l'immédiat, écrivons un aperçu de la fonction :

1. function distance(x₁, x₂, y₁, y₂)

2. 0.0

3. end

Évidemment, cette version ne calcule pas les distances ; elle retourne toujours zéro. Cependant, elle est syntaxiquement correcte. Elle fonctionne, ce qui signifie que vous pouvez la tester avant de la complexifier. Les numéros d'indice sont disponibles dans le codage des caractères Unicode (\_1 TAB, \_2 TAB, etc.).

Pour tester la nouvelle fonction, appelons-la avec des exemples d'arguments :

1. distance(1, 2, 4, 6)

Ces valeurs ont été choisies pour que la distance horizontale soit 3 et la distance verticale 4. De cette façon, le résultat est 5 : c'est-à-dire l'hypoténuse d'un triangle 3-4-5. Lorsqu'on teste une fonction, il est utile de connaître la bonne réponse.

À ce stade, il est acquis que la fonction est syntaxiquement correcte. Dès lors, du code peut être ajouté dans le corps. La prochaine étape raisonnable consiste à trouver les différences et . La version suivante enregistre ces valeurs dans des variables temporaires et les affiche avec la macro @show.

1. function distance(x₁, x₂, y₁, y₂)

2. dx = x₂ - x₁

3. dy = y₂ - y₁

4. @show dx  dy

5. 0.0

6. end

Pour un appel distance(1, 2, 4, 6), la fonction doit afficher dx = 3 et dy = 4. Si c'est le cas, cela signifie que la fonction reçoit les bons arguments et effectue correctement le premier calcul (sinon, il n'y a que quelques lignes à vérifier).

Ensuite, la somme des carrés de dx et dy peut être incorporée :

1. function distance(x₁, y₁, x₂, y₂)

2. dx = x₂ - x₁

3. dy = y₂ - y₁

4. d² = dx^2 + dy^2

5. @show d²

6. 0.0

7. end

Là encore, il convient de lancer le programme à ce stade et de vérifier le résultat (d² = 25). Les puissances sont également disponibles (\^2 TAB). Enfin, la fonction sqrt est introduite afin de calculer et de retourner le résultat :

1. function distance(x₁, y₁, x₂, y₂)

2. dx = x₂ - x₁

3. dy = y₂ - y₁

4. d² = dx^2 + dy^2

5. sqrt(d²)

6. end

Si tout fonctionne correctement, nous en avons terminé. Sinon, il sera nécessaire d'afficher la valeur de sqrt(d²) avant end à l'aide de @show.

La version finale de la fonction n'affiche rien lorsqu'elle est exécutée. Elle ne fait que retourner une valeur. Les instructions d'affichage que nous avons écrites sont utiles pour le débogage, mais, une fois la fonction opérationnelle, il convient de les supprimer. Un tel code est appelé « canevas » (ou scaffolding) parce qu'il est utile pour construire le programme tout en n'apparaissant pas dans le produit final.

Au début, il est judicieux de n'ajouter qu'une ou deux lignes de code à la fois. Avec une expérience s'affinant progressivement, il devient possible d'écrire et de déboguer des parties un peu plus longues. Quoi qu'il en soit, le développement incrémental permet de gagner beaucoup de temps de débogage.

Les principaux aspects du processus sont les suivants :

1. Commencer par un programme opérationnel et apporter de petites modifications progressives. À tout moment, s'il y a une erreur, il est essentiel d'avoir une bonne idée de l'endroit où elle se trouve ;

2. Utiliser des variables pour maintenir des valeurs intermédiaires afin de pouvoir les afficher et les vérifier ;

3. Une fois que le programme complété fonctionne, supprimer une partie du canevas ou consolider plusieurs instructions en expressions composées (voir la section 6.3), mais à la condition que cela ne rende pas le programme difficile à lire.

6-2-1 - Exercice 6-2

Utilisez le développement incrémental pour écrire une fonction appelée hypotenuse qui retourne la longueur de l'hypoténuse d'un triangle rectangle en fonction de la longueur des deux autres côtés comme arguments. Enregistrez chaque étape du processus de développement au fur et à mesure.

6-3 - Composition

Bien entendu, une fonction peut être appelée depuis une autre. Par exemple, nous allons écrire une fonction qui prend deux points, le centre d'un cercle et un point sur la circonférence. Ceci nous permet de calculer le rayon et, partant, la surface de ce cercle.

Supposons que les coordonnées du centre soient enregistrées dans les variables et et que les coordonnées du point de la circonférence le soient dans et . La première étape consiste à trouver le rayon du cercle. Dans la section 6.2, nous avons écrit une fonction distance. En transposant pour le rayon :

1. radius = distance(xc, yc, xp, yp)

L'étape suivante consiste à trouver l'aire d'un cercle de ce rayon (ce que nous avons aussi écrit dans la section 6.1), ce qui fait que nous pouvons appeler cette fonction :

1. result = area(radius)

Par encapsulation, nous obtenons :

1. function circlearea(xc, yc, xp, yp)

2. radius = distance(xc, yc, xp, yp)

3. result = area(radius)

4. return result

5. end

Les variables temporaires radius et result sont utiles pour le développement et le débogage. Lorsqu'il est opérationnel, le programme peut être rédigé de manière plus concise en composant les appels de fonction :

1. function circlearea(xc, yc, xp, yp)

2. area(distance(xc, yc, xp, yp))

3. end

6-4 - Fonctions booléennes

Les fonctions peuvent renvoyer des booléens, ce qui est souvent pratique pour cacher des tests complexes à l'intérieur de celles-ci. Par exemple :

1. function isdivisible(x, y)

2. if x % y == 0

3. return true

4. else

5. return false

6. end

7. end

Il est courant de donner aux fonctions booléennes des noms qui ressemblent à des questions dichotomiques(8). La fonction isdivisible retourne soit true, soit false pour indiquer si x est divisible, ou non, par y.

Voici deux exemples :

1. julia> isdivisible(6, 3)

2. true

3. julia> isdivisible(6, 4)

4. false

Le résultat de l'opérateur == étant un booléen, la fonction peut être reformulée de manière plus concise :

1. function isdivisible(x, y)

2. x % y == 0

3. end

Les fonctions booléennes sont souvent utilisées dans les déclarations conditionnelles :

1. if isdivisible(x, y)

2. println("x est divisible par y")

3. end

Il pourrait être tentant d'écrire quelque chose comme ceci :

1. if isdivisible(x, y) == true

2. println("x est divisible par y")

3. end

Cependant, la comparaison supplémentaire avec true est inutile.

6-4-1 - Exercice 6-3

Écrivez une fonction isbetween(x, y, z) qui retourne true si x ≤ y ≤ z ou false dans le cas contraire.

6-5 - Davantage de récursion

Nous n'avons couvert qu'un petit sous-ensemble de Julia. Cependant, Julia est un langage de programmation complet, ce qui signifie que tout ce qui est calculable peut être exprimé dans ce langage. Tout programme écrit à ce jour pourrait être retranscrit en utilisant uniquement les caractéristiques de Julia telles que présentées jusqu'à présent. En principe, il n'y a plus que quelques commandes à apprendre pour contrôler des périphériques comme la souris, les disques, etc.

Prouver cette affirmation est un exercice complexe réalisé pour la première fois par Alan Turing, un des pionniers de l'informatique(9) et mathématicien à l'origine. C'est pourquoi elle est connue sous le nom de thèse de Turing(10).

Pour se persuader de ce qui est réalisable avec les outils étudiés jusqu'à présent, nous allons évaluer quelques fonctions mathématiques définies de manière récursive. Une définition récursive est analogue à une définition circulaire, dans le sens où la définition contient une référence à ce qui y est défini (ou à son exact contraire). Une définition vraiment circulaire n'est pas très utile(11). Dans le dictionnaire anglais, voyons la définition de :

1. vorpal an adjective used to describe something that is vorpal(12).

Nous pourrions imaginer en français :

1. gauche inverse de la droite ;

2. droite inverse de la gauche.

De telles définitions rendent perplexe. En revanche, si on cherche la définition de la fonction factorielle, désignée par le symbole !, on obtiendra probablement ceci :

Cette définition établit que la factorielle de 0 est 1 et que la factorielle de toute autre valeur n s'exprime comme n multiplié par la factorielle de n-1. Donc 3! est 3 fois 2!, avec 2! qui est 2 fois 1!, avec 1! qui est 1 fois 0!. En combinant le tout, 3! est égal à 3 * 2 * 1 * 1, ce qui fait 6.

Si un programmeur peut rédiger une définition récursive, il peut développer un programme Julia pour l'évaluer. La première étape consiste à décider des paramètres. Dans ce cas, il doit être clair que la fonction factorielle prend un entier :

1. function fact(n)

2. end

S'il advenait que l'argument vaille 0, la fonction devrait retourner 1 :

1. function fact(n)

2. if n == 0

3. return 1

4. end

5. end

Sinon — et c'est la partie intéressante —, nous devons faire un appel récursif pour trouver la factorielle de n-1 et ensuite la multiplier par n :

1. function fact(n)

2. if n == 0

3. return 1

4. else

5. recurse = fact(n-1)

6. result = n * recurse

7. return result

8. end

9. end

Le flux d'exécution de ce programme est similaire au flux de countdown vu dans la section 5.8. Si nous appelons fact avec n = 3 :

- Vu que 3 n'est pas égal à 0, nous prenons la deuxième branche et calculons la factorielle de n-1…

- Puisque 2 n'est pas égal à 0, nous prenons la deuxième branche et calculons la factorielle de n-1…

- Puisque 1 n'est pas égal à 0, nous prenons la deuxième branche et calculons la factorielle de n-1…

- Puisque nous arrivons au cas 0, nous prenons la première branche et nous retournons 1 sans faire d'appels récursifs supplémentaires.

- La valeur de retour, 1, est multipliée par n, qui est 1, et le résultat est retourné.

- La valeur de retour, 1, est multipliée par n, qui vaut 2, et le résultat est retourné.

- La valeur de retour 2 est multipliée par n (qui vaut 3) et le résultat, c'est-à-dire 6, devient la valeur de retour de l'appel de fonction qui a lancé l'ensemble du processus.

Le diagramme de pile (figure 6.5.1) illustre cette séquence d'appels de fonction. Les valeurs de retour sont indiquées en remontant dans la pile. Dans chaque cadre, la valeur de retour est celle de result, qui est le produit de n et de recurse. Dans le dernier cadre, les variables locales recurse et result n'existent pas, car la branche qui les crée n'est pas exécutée.

FIGURE 6.5.1 – Diagramme de pile associé à la fonction fact(n).

Julia fournit une fonction interne factorial qui calcule la factorielle d'un entier.

6-6 - Un acte de confiance

Suivre le déroulement de l'exécution est une manière de lire les programmes, bien que cela puisse s'avérer ardu. Souvent, nous agissons en confiance. Ainsi, lorsque nous arrivons à un appel de fonction, au lieu de suivre le flux d'exécution, nous supposons que la fonction se comporte correctement et retourne le bon résultat.

En fait, ceci est déjà vrai, par exemple, lorsque des fonctions internes sont utilisées. Lorsque cos() ou exp() sont appelées, leur utilisateur n'examine pas le corps de ces fonctions. Il suppose assez naturellement qu'elles sont opérationnelles, parce que les personnes qui les ont écrites sont de bons programmeurs.

Il en va de même lorsque nous appelons une de nos propres fonctions. Par exemple, dans les fonctions booléennes (section 6.4), nous avons écrit isdivisible qui détermine si un nombre est divisible par un autre. Une fois que nous nous sommes convaincus que cette fonction est correcte — en examinant le code et en faisant des tests, nous pouvons utiliser la fonction sans devoir analyser à nouveau le corps.

Il en va de même pour les programmes récursifs. Arrivé à un appel récursif, au lieu de suivre le déroulement de l'exécution, il est supposé que l'appel récursif fonctionne (c'est-à-dire qu'il retourne le résultat correct). Ensuite, survient la question : « En supposant que je puisse trouver la factorielle de , puis-je calculer la factorielle de  ? ». Il est clair que cela peut se faire en multipliant par n.

Il est un peu étrange d'accepter que la fonction se comporte correctement alors même qu'on n'a pas fini de l'écrire. C'est pour cela que l'expression « acte de confiance » peut être invoquée.

6-7 - Un exemple supplémentaire

Après la factorielle, l'exemple le plus courant d'une fonction mathématique définie de manière récursive est la suite de Fibonacci.

Voici un programme Julia représentant cette suite :

1. function fib(n)

2. if n == 0

3. return 0

4. elseif n == 1

5. return 1

6. else

7. return fib(n-1) + fib(n-2)

8. end

9. end

Suivre le déroulement de l'exécution, même pour des valeurs assez faibles de n, conduit rapidement à la saturation. Néanmoins, selon le principe de l'acte de confiance, il suffit de constater que deux appels récursifs fonctionnent correctement, pour admettre que le bon résultat est obtenu par addition.

6-8 - Types et vérification

Que se passe-t-il si fact est appelée avec 1.5 comme argument ?

1. julia> fact(1.5)

2. ERROR: UndefVarError: fact not defined

3. Stacktrace:

4. [1] fact(::Float64) at ./REPL[3]:2

Cela ressemble à une récursion infinie. Au premier abord, c'est surprenant, car la fonction possède un cas de base lorsque n == 0. Cependant, si n n'est pas un entier, le cas de base peut ne pas être atteint. En conséquence, la récursion opère à l'infini.

Dans le premier appel récursif, la valeur de n est 0.5. Dans le suivant, elle est -0.5. À partir de là, cette valeur devient inférieure à 0 sans jamais passer par 0.

Pour résorber ce problème, nous avons deux options : (i) soit essayer de généraliser la fonction factorielle pour travailler avec des nombres à virgule flottante, (ii) soit faire vérifier le type de son argument à la fonction fact. La première option recourt à la fonction gamma, mais cela dépasse le cadre de ce livre. La seconde option est à notre portée.

L'opérateur interne isa permet de vérifier le type de l'argument. Par ailleurs, pourquoi ne pas s'assurer que l'argument est positif ?

1. function fact(n)

2. if !(n isa Int64)

3. error("La factorielle est seulement définie pour les entiers.")

4. elseif n < 0

5. error("La factorielle n'est pas définie pour les entiers négatifs.")

6. elseif n == 0

7. return 1

8. else

9. return n * fact(n-1)

10. end

11. end

Le premier cas de base traite des non-entiers, le second des entiers négatifs. Dans les deux cas, le programme affiche un message d'erreur et ne retourne rien pour indiquer que quelque chose s'est mal passé :

1. julia> fact("Knuth")

2. ERROR: La factorielle est seulement définie pour les entiers.

3. julia> fact(-2)

4. ERROR: La factorielle n'est pas définie pour les entiers négatifs.

Si les deux contrôles sont correctement franchis, cela signifie que n est positif ou nul. En conséquence, la preuve est faite que la récursion prend fin.

Ce programme démontre un schéma parfois appelé « sentinelle ». Les deux premières conditions agissent comme des sentinelles, protégeant le code qui suit des valeurs qui pourraient causer une erreur. Les sentinelles permettent de prouver l'exactitude du code.

Dans la section 14.5, nous verrons une option plus souple à l'affichage d'un message d'erreur : la levée d'une exception.

6-9 - Débogage

La subdivision d'un programme de grande taille en petites fonctions crée des points de contrôle naturels pour le débogage. Si une fonction n'opère pas correctement, trois possibilités sont à envisager :

1. Un problème avec les arguments que la fonction reçoit : une condition a priori est violée ;

2. Un problème avec la fonction elle-même : une condition a posteriori est violée ;

3. Un problème avec la valeur de retour ou la façon dont elle est utilisée.

Pour écarter la première possibilité, il convient d'ajouter une instruction d'affichage au début de la fonction et d'y indiquer les valeurs des paramètres (et peut-être leur type). Une option consiste à écrire un code qui vérifie explicitement les conditions préalables.

Si les paramètres semblent bons, il convient d'ajouter une instruction d'affichage avant chaque instruction de retour et afficher la valeur de retour. Lorsque c'est possible, il est conseillé de vérifier le résultat à la main. Exécuter des appels de la fonction avec des valeurs qui facilitent la vérification du résultat aide beaucoup au débogage (voir la section 6.2 qui traite du développement incrémental).

Si la fonction semble opérationnelle, l'appel de la fonction doit être examiné pour s'assurer que la valeur de retour est utilisée correctement.

L'ajout d'instructions d'affichage au début et à la fin d'une fonction peut contribuer à rendre le flux d'exécution plus compréhensible. Par exemple, voici une version de fact avec des instructions d'affichage :

1. function fact(n)

2. space = " " ^ (4 * n)

3. println(space, "factorial ", n)

4. if n == 0

5. println(space, "returning 1")

6. return 1

7. else

8. recurse = fact(n-1)

9. result = n * recurse

10. println(space, "returning ", result)

11. return result

12. end

13. end

space est une chaîne de caractères d'espacement qui contrôle l'indentation de la sortie :

1. julia> fact(4)

2. factorial 4

3. factorial 3

4. factorial 2

5. factorial 1

6. factorial 0

7. returning 1

8. returning 1

9. returning 2

10. returning 6

11. returning 24

12. 24

Ce type de résultat peut être utile quand l'exécution d'un programme se passe de manière déroutante. Mettre au point un canevas efficace prend du temps, mais cette technique en épargnera beaucoup lors du débogage.

6-10 - Glossaire

variable temporaire variable utilisée pour stocker une valeur intermédiaire dans un calcul complexe.

code mort partie d'un programme qui ne peut jamais fonctionner, souvent parce qu'il apparaît après une instruction de retour.

développement progressif (ou incrémental) plan de développement de programmes destiné à limiter le débogage en ajoutant et en testant seulement de petits blocs d'instructions, un à un.

canevas (scaffolding) code utilisé pendant le développement du programme, mais qui ne fait pas partie de la version finale.

sentinelle modèle de programmation qui utilise une déclaration conditionnelle pour vérifier et gérer les circonstances susceptible de causer une erreur.

6-11 - Exercices

6-11-1 - Exercice 6-4

Dessinez un diagramme de pile pour le programme suivant. Qu'affiche le programme ?

1. function b(z)

2. prod = a(z, z)

3. println(z, " ", prod)

4. prod

5. end

7. function a(x, y)

8. x = x + 1

9. x * y

10. end

12. function c(x, y, z)

13. total = x + y + z

14. square = b(total)^2

15. square

16. end

18. x = 1

19. y = x + 1

20. println(c(x, y+3, x+y))

6-11-2 - Exercice 6-5

La fonction d'Ackermann, , est définie comme :

Rédigez une fonction appelée ack qui évalue la fonction d'Ackermann. Utilisez votre fonction pour évaluer ack(3, 4). Le résultat devrait être 125. Que se passe-t-il pour les valeurs plus grandes de m et n ?

6-11-3 - Exercice 6-6

Un palindrome est un mot qui s'écrit de la même façon à l'envers et à l'endroit, comme « ada »(13), « kayak » ou « ressasser ». Récursivement, un mot est un palindrome si la première et la dernière lettre sont identiques et si le milieu est un palindrome.

Voici des fonctions qui prennent un argument de chaîne de caractères et retournent la première, la dernière et le milieu des lettres :

1. function first(word)

2. first = firstindex(word)

3. word[first]

4. end

6. function last(word)

7. last = lastindex(word)

8. word[last]

9. end

11. function middle(word)

12. first = firstindex(word)

13. last = lastindex(word)

14. word[nextind(word, first) : prevind(word, last)]

15. end

Nous verrons comment cela fonctionne dans le chapitre 8.

1. Testez ces fonctions. Que se passe-t-il si vous appelez le milieu avec une chaîne de deux lettres ? Une lettre ? Qu'en est-il de la chaîne vide, qui s'écrit " " et ne contient pas de lettre ?

2. Écrivez une fonction appelée ispalindrome qui prend un argument sous la forme d'une chaîne de caractères et soit retourne true si c'est un palindrome, soit false dans le cas contraire. N'oubliez pas que vous pouvez utiliser la fonction intégrée length pour vérifier la longueur d'une chaîne de caractères.

6-11-4 - Exercice 6-7

Un nombre a est une puissance de b si, d'une part, il est divisible par b et si, de l'autre, a / b est une puissance de b. Écrivez une fonction appelée ispuissance qui prend les paramètres a et b et qui retourne true si a est une puissance de b.

Rappelez-vous les caractéristiques d'un cas de base.

6-11-5 - Exercice 6-8

Le plus grand commun diviseur (PGCD) de deux entiers a et b est le plus grand nombre qui divise les deux entiers avec un reste nul.

Il existe une manière simple de trouver le PGCD de deux nombres : si r est le reste de la division a / b, alors PGCD(a, b) = PGCD(b, r). Comme cas de base, nous pouvons utiliser PGCD(a, 0) = a.

Écrivez une fonction appelée pgcd qui prend les paramètres a et b et retourne leur plus grand diviseur commun.

7 - Itération

Ce chapitre traite de l'itération, c'est-à-dire de la capacité à exécuter un bloc d'instructions de manière répétée. Une sorte d'itération a déjà été rencontrée lors de l'utilisation de la récursion (chapitre 5). Une autre l'a été avec les boucles, dans la section 4.2. Dans le présent chapitre, nous utilisons l'instruction while qui constitue un autre type d'itération.

Auparavant, il est utile de revenir sur l'affectation des variables.

7-1 - Réaffectation

Comme le lecteur l'a certainement observé, il est permis de faire plus d'une affectation à la même variable. Une nouvelle affectation fait en sorte qu'une variable existante se réfère à une nouvelle valeur et, en conséquence, cesse de se référer à l'ancienne valeur.

1. julia> x = 5

2. 5

3. julia> x = 7

4. 7

La première fois que nous affichons x, sa valeur est de 5 ; la deuxième fois, sa valeur est de 7. La réaffectation peut se représenter dans un diagramme d'état comme suit :

À ce stade, il est temps d'aborder une source commune de confusion. Du fait que Julia utilise le signe égal = pour l'affectation, il est tentant d'interpréter une déclaration telle que a = b comme une proposition mathématique d'égalité, c'est-à-dire l'affirmation que a et b sont égaux. Cette interprétation est erronée.

Premièrement, l'égalité est une relation symétrique. L'affectation ne l'est pas. Par exemple, en mathématiques, si a = 7, il est permis d'écrire 7 = a. Dans Julia, l'énoncé a = 7 est licite ; 7 = a ne l'est pas.

En outre, en mathématiques, une proposition d'égalité est soit vraie soit fausse ad vitam. Si a = b maintenant, alors a sera toujours égal à b. Dans Julia, une déclaration d'affectation peut rendre deux variables égales, mais elles ne doivent pas nécessairement le rester éternellement.

1. julia> a = 5

2. 5

3. julia> b = a        # a et b pointent vers la même valeur 5

4. 5

5. julia> a = 3        # a ne pointe plus vers 5, mais vers 3

6. 3

7. julia> b            # b pointe toujours vers 5

8. 5

La troisième ligne modifie la valeur de a, mais ne change pas la valeur de b, de sorte qu'elles ne sont plus égales.

La réaffectation des variables est souvent utile, mais il faut l'utiliser avec prudence. Si les valeurs des variables changent fréquemment, cela peut rendre le code difficile à lire et à déboguer. Il n'est pas permis de définir une fonction qui porte le même nom qu'une variable définie précédemment.

7-2 - Mise à jour des variables

Il est courant de réaffecter une variable via la mise à jour de son ancienne valeur :

1. julia> x = x + 1

2. 8

Cela signifie « obtenir la valeur actuelle de x (à savoir 7), y ajouter 1 et ensuite affecter 8 à x ».

Lors d'une tentative de mise à jour d'une variable non déclarée, Julia retourne une erreur :

1. julia> y = y + 1

2. ERROR: UndefVarError: y not defined

Avant de pouvoir mettre à jour une variable, il est impératif de l'initialiser, généralement grâce à une simple affectation :

1. julia> y = 0

2. 0

3. julia> y = y +1

4. 1

7-3 - while

Les ordinateurs sont souvent utilisés pour automatiser les tâches répétitives. Répéter des tâches identiques ou similaires sans faire d'erreur constitue un point fort des ordinateurs, contrairement aux humains. Dans un programme informatique, la répétition est également appelée itération.

Nous avons déjà vu deux fonctions, countdown et printn, qui utilisent la récursion (section 5.8). Comme l'itération est très courante, Julia propose des fonctions pour la rendre plus facile d'utilisation. L'une d'entre elles est la déclaration for que nous avons vue dans la section 4.2. Nous y reviendrons ultérieurement.

while constitue une autre déclaration. Voici une version de countdown exploitant la déclaration while :

1. function countdown(n)

2. while n > 0

3. print(n, " ")

4. n = n - 1

5. end

6. println("mise à feu !")

7. end

Cette déclaration while peut quasiment être lue comme si elle était formulée en langage naturel : « Si n est supérieur à 0, affichez la valeur de n, puis décrémentez n. Lorsque vous arrivez à 0, affichez l'expression "mise à feu !" ».

De manière un peu plus formelle, voici le déroulement de l'instruction while :

1. Déterminer si la condition est vraie ou fausse ;

2. Si elle est fausse, quitter la déclaration while et continuer l'exécution à l'instruction suivante ;

3. Si la condition est vraie, exécuter le corps et revenir ensuite à l'étape 1.

Ce type de flux est appelé une boucle, car la troisième étape revient en boucle vers le haut. Il est fondamental que le corps de la boucle modifie la valeur d'une ou plusieurs variables, de sorte que la condition devienne fausse à terme et que la boucle se termine. Sinon, celle-ci se répète indéfiniment (boucle infinie).(14)

Dans le cas de countdown, on peut prouver que la boucle se termine : si n est nul ou négatif, la boucle ne se répète jamais. Sinon, n est décrémenté à chaque passage dans la boucle, de sorte que n finit par valoir 0. Pour certaines autres boucles, ce n'est pas si facile à identifier. Par exemple :

1. function seq(n)

2. while n != 1

3. println(n)

4. if n % 2 == 0      # n est pair

5. n = n /2

6. else               # n est impair

7. n = n*3 + 1

8. end

9. end

10. end

La condition pour cette boucle est n != 1, donc la boucle continue jusqu'à ce que n vaille 1 (ce qui rend la condition fausse). À chaque fois que la boucle se poursuit, le programme fournit la valeur de n et vérifie ensuite si elle est paire ou impaire. S'il est pair, n est divisé par 2 tandis que, s'il est impair, la valeur de n est remplacée par n*3 + 1. Par exemple, si l'argument passé à la séquence vaut 3, les valeurs résultantes de n sont 3, 10, 5, 16, 8, 4, 2, 1.

Comme n augmente parfois et qu'il diminue parfois, il n'y a pas de preuve évidente que n atteindra 1 ou que le programme se termine. Pour certaines valeurs particulières de n, nous pouvons prouver la fin du programme. Par exemple, si la valeur de départ est une puissance de deux, n sera pair à chaque passage dans la boucle jusqu'à ce qu'il atteigne 1. L'exemple précédent se termine par une telle séquence, en commençant par 16.

La question véritablement difficile est de savoir s'il peut être prouvé que ce programme se termine pour toutes les valeurs positives de n. Jusqu'à présent, personne n'a été capable ni de prouver ni de réfuter cet énoncé. Il s'agit de la conjecture de Collatz.

7-3-1 - Exercice 7-1

Réécrivez la fonction printn (voir la section 5.8) en tirant parti de l'itération plutôt que de la récursion.

7-4 - break

Parfois, on ne sait pas qu'il est temps de terminer une boucle avant d'avoir parcouru la moitié du corps. Dans ce cas, l'instruction break peut être utilisée pour sortir de la boucle. Supposons, par exemple, qu'il faille capter les données d'un utilisateur jusqu'à ce que celui-ci saisisse « done ». Voici un exemple :

1. while true

2. print("> ")

3. line = readline()

4. if line == "done"

5. break

6. end

7. println(line)

8. end

9. println("Done!")

La boucle conditionnelle est toujours vraie. Donc, la boucle se déroule jusqu'à ce qu'elle atteigne l'instruction break.

À chaque passage, la boucle présente une invite > à l'utilisateur. Si l'utilisateur saisit le terme done, l'instruction break force la sortie de la boucle. Sinon, le programme retourne la saisie de l'utilisateur et, ensuite, il revient au début de la boucle. Voici un exemple d'exécution :

1. > not done

2. not done

3. > done

4. Done!

Cette manière d'écrire des boucles while est courante, car on peut vérifier la condition n'importe où dans la boucle (pas seulement en haut) et la condition d'arrêt peut être exprimée affirmativement (« arrêtez quand cela se produit ») plutôt que négativement (« continuez tant que cela ne se produit pas »).

7-5 - continue

L'instruction break force la sortie d'une boucle. Lorsque l'instruction continue est rencontrée à l'intérieur d'une boucle, l'exécution saute au début de la boucle afin d'entamer l'itération suivante. Donc, l'exécution des instructions à l'intérieur du corps de la boucle est contournée. Par exemple :

1. for i in 1:10

2. if i % 3 == 0

3. continue

4. end

5. print(i, " ")

6. end

Julia retourne :

1. 1 2 4 5 7 8 10

Si i est divisible par 3, l'instruction continue arrête l'itération en cours et l'itération suivante commence. Seuls les nombres compris entre 1 et 10 non divisibles par 3 sont affichés.

7-6 - Racines carrées

Les boucles sont souvent utilisées dans les programmes qui traitent des résultats numériques en commençant par une réponse approximative et en l'améliorant de manière itérative pour converger vers la solution.

Par exemple, la méthode de Newton permet de calculer les racines carrées. Supposons que nous souhaitons connaître la racine carrée d'un nombre . En commençant avec presque n'importe quelle estimation x, il est possible calculer une meilleure estimation avec la formule suivante :

Si = 4 et x = 3 ( s'obtient par \alpha TAB et ainsi de suite pour tout l'alphabet grec) :

1. julia> α = 4

2. 4

3. julia> x =3

4. 3

5. julia> y = (x + α/x) / 2

6. 2.1666666666666665

Par rapport à la valeur initiale introduite (x = 3), le résultat s'approche davantage de . Après une répétition, le calcul converge vers la solution :

1. julia> x = y

2. 2.1666666666666665

3. julia> y = (x + α/x) / 2

4. 2.0064102564102564

Après quelques itérations manuelles, l'estimation devient presque exacte :

1. julia> x = y

2. 2.0064102564102564

3. julia> y = (x + α/x) / 2

4. 2.0000102400262145

5. julia> x = y

6. 2.0000102400262145

7. julia> y = (x + α/x) / 2

8. 2.0000000000262146

En général, le nombre d'itérations nécessaires pour obtenir la bonne réponse est a priori inconnu. En revanche, lorsque la valeur de l'estimation ne change plus, le calcul peut être arrêté :

1. julia> x = y

2. 2.0000000000262146

3. julia> y = (x + α/x) / 2

4. 2.0

5. julia> x = y

6. 2.0

7. julia> y = (x + α/x) / 2

8. 2.0

C'est le cas quand y == x. Voici une boucle qui commence avec une estimation initiale, x, et qui améliore celle-ci jusqu'à ce que le résultat cesse de changer :

1. while true

2. println(x)

3. y = (x + α/x) / 2

4. if y == x

5. break

6. end

7. x = y

8. end

Pour la plupart des valeurs, ce code fonctionne bien. Cependant, en général, il est dangereux de tester l'égalité de nombres à virgules flottantes. Les valeurs à virgule flottante ne sont qu'approximativement correctes : la plupart des nombres rationnels, comme , et les nombres irrationnels, comme , ne peuvent pas être représentés exactement par un Float64.

Plutôt que de vérifier si x et y sont rigoureusement égaux, il est plus sûr d'utiliser la fonction interne abs pour calculer la valeur absolue de leur différence :

1. if abs(x - y) < ε

2. break

3. end

Dans ce code, ε (\varepsilon TAB) a une valeur telle que 0,000 000 1 qui détermine la proximité au résultat réel.

7-7 - Algorithmes

La méthode de Newton est un exemple d'algorithme. Il s'agit d'un processus mécanique pour résoudre une catégorie de problèmes (dans ce cas, le calcul de racines carrées).

Pour comprendre ce qu'est un algorithme, il peut être utile de commencer par un procédé qui ne relève pas d'un algorithme. Pour apprendre la multiplication des nombres à un chiffre, il est nécessaire de mémoriser les tables de multiplication avec 100 solutions spécifiques. Ce type de connaissance n'est pas algorithmique.

Toutefois, un individu « paresseux » aurait peut-être appris quelques astuces. Par exemple, pour trouver le produit de n et 9, il est possible d'écrire comme premier chiffre et comme deuxième chiffre. Cette astuce est une solution générale pour multiplier tout nombre à un chiffre par 9 : il s'agit d'un algorithme.

De même, les techniques apprises à l'école primaire pour l'addition avec report, la soustraction par emprunt (ou celle par compensation) et la division longue (méthode de la potence) sont toutes de nature algorithmique. Les algorithmes ont pour caractéristique commune de ne nécessiter aucune intelligence pour être exécutés. Ce sont des processus mécaniques où chaque étape suit la précédente selon un ensemble de règles simples.

Exécuter des algorithmes est ennuyeux, mais les concevoir est intéressant, intellectuellement stimulant et constitue une part centrale de l'informatique.

Certains actes naturellement exécutés par les humains, sans difficulté ni pensée consciente, sont les plus difficiles à exprimer par des algorithmes. La compréhension du langage naturel en est un bon exemple. Nous agissons tous ainsi, mais, jusqu'à présent, personne n'a pu expliquer comment nous pratiquons, du moins pas sous la forme d'un algorithme.

7-8 - Débogage

Plus la taille des programmes augmente, plus de temps de débogage s'allonge. Davantage de code signifie davantage de risques d'erreur et plus d'endroits où les bogues peuvent se camoufler.

Le « débogage par bissection » est une des méthodes permettant de réduire le temps de débogage. Par exemple, s'il y a 100 lignes dans un programme et qu'elles sont vérifiées une à une, il en résultera 100 étapes. Il est plus astucieux de diviser le problème en deux en regardant vers le milieu du programme pour capter une valeur intermédiaire vérifiable. Il suffit d'ajouter une instruction d'affichage (ou quelque technique vérifiable) et lancer le programme. Si la vérification intermédiaire est incorrecte, le problème se situe dans la première moitié du programme. Si la vérification est correcte, le problème se situe dans la seconde moitié.

Chaque fois qu'un contrôle de ce type est utilisé, le nombre de lignes à analyser est divisé par deux. Pour 100 lignes de codes et après seulement six étapes, il n'y a plus qu'une ou deux lignes de code à vérifier, du moins en théorie.

Dans la pratique, il n'est pas toujours aisé de déterminer le « milieu du programme » et une vérification n'est pas toujours réalisable. Il n'est pas pertinent de compter les lignes et de trouver l'exact milieu. Mieux vaut repérer les endroits du programme où des erreurs pourraient apparaître et où il est facile de pratiquer une vérification. Ensuite, il est judicieux de choisir un endroit où les chances de débusquer le bogue avant ou après la vérification sont approximativement les mêmes.

7-9 - Glossaire

réaffectation attribution d'une nouvelle valeur à une variable qui existe déjà.

mise à jour affectation où la nouvelle valeur de la variable dépend de l'ancienne.

initialisation affectation qui donne une valeur initiale à une variable qui sera mise à jour ultérieurement.

incrémentation mise à jour qui augmente la valeur d'une variable (typiquement de 1).

décrémentation mise à jour qui diminue la valeur d'une variable (typiquement de 1).

itération exécution répétée d'un ensemble d'instructions en utilisant soit un appel de fonction récursif, soit une boucle.

while instruction qui permet des itérations contrôlées par une condition.

break instruction permettant de sortir d'une boucle.

continue instruction à l'intérieur d'une boucle qui provoque un saut au début de la boucle pour l'itération suivante.

boucle infinie boucle dans laquelle la condition de fin n'est jamais remplie.

algorithme processus général pour résoudre une catégorie de problèmes.

7-10 - Exercices

7-10-1 - Exercice 7-2

Copiez la boucle de la section 7.6 et encapsulez-la dans une fonction appelée mysqrt qui prend a comme paramètre, choisit une valeur raisonnable de x et retourne une estimation de la racine carrée de a.

Pour tester mysqrt, écrivez une fonction appelée testsquareroot qui affiche un tableau tel que représenté en figure 7.10.1.

FIGURE 7.10.1 – Tableau produit par la fonction testsquareroot.

7-10-2 - Exercice 7-3

La fonction interne Meta.parse prend une chaîne de caractères et la transforme en une expression. Cette expression peut être évaluée dans Julia avec la fonction Core.eval. Par exemple :

1. julia> expr = Meta.parse("1+2*3")

2. :(1 + 2 * 3)

3. julia> eval(expr)

4. 7

5. julia> expr = Meta.parse("sqrt(π)")

6. :(sqrt(π))

7. julia> eval(expr)

8. 1.7724538509055159

Écrivez une fonction appelée evalloop qui invite l'utilisateur de manière itérative à saisir une expression, prend les données résultantes et les évalue à l'aide de la fonction eval, puis affiche le résultat. eval doit continuer jusqu'à ce que l'utilisateur saisisse done, puis retourner la valeur de la dernière expression évaluée.

7-10-3 - Exercice 7-4

Le mathématicien Srinivasa Ramanujan a trouvé une série infinie qui peut être utilisée pour générer une approximation numérique de  :

Écrivez une fonction appelée estimatepi qui utilise cette formule pour calculer et retourner une estimation de . Elle doit utiliser une boucle while pour calculer les termes de la somme jusqu'à ce que le dernier terme soit inférieur à 1e-15 (ce qui est la notation Julia pour ). Vous pouvez vérifier le résultat en le comparant à la valeur de π fournie par Julia.

8 - Chaînes

Les chaînes sont des entités différentes des entiers, des flottants et des booléens. Elles font partie des structures de données (comme les tableaux [chapitre 10], les dictionnaires [chapitre 11], les tuples [chapitre 12] et les structures composites [chapitre 16 et suivants]). Une chaîne est une séquence, ce qui signifie qu'elle forme un ensemble ordonné de valeurs. Dans ce chapitre, nous voyons comment accéder aux caractères qui composent une chaîne et nous découvrirons certaines fonctions d'aide fournies par Julia afin de manipuler des chaînes.

8-1 - Caractères

Les locuteurs de langue anglaise sont familiarisés avec les caractères tels que les lettres de l'alphabet (A, B, C…), les chiffres et la ponctuation courante. Ces caractères sont normalisés et mis en correspondance avec des valeurs entières comprises entre 0 et 127 par la norme ASCII (American Standard Code for Information Interchange).

Il existe bien sûr de nombreux autres caractères utilisés dans d'autres langues que l'anglais, y compris des variantes des caractères ASCII avec des accents et d'autres modifications, des écritures connexes telles que le cyrillique et le grec, et des écritures sans aucun rapport avec l'ASCII et l'anglais, notamment l'arabe, le chinois, l'hébreu, l'hindi, le japonais et le coréen.

La norme Unicode traite les difficultés associées à la définition exacte d'un caractère et elle est généralement acceptée comme la norme définitive pour résoudre ces problèmes. Elle fournit un numéro unique pour chaque caractère à l'échelle mondiale.

Une valeur Char représente un seul caractère et elle est entourée de guillemets droits simples :

1. julia> 'x'

2. 'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

3. julia> '🍌'

4. '🍌': Unicode U+1F34C (category So: Symbol, other)

5. julia> typeof('x')

6. Char

Même les emojis(15) font partie de la norme Unicode (par exemple, \:banana: TAB)

8-2 - Une chaîne est une séquence

Une chaîne est une séquence de caractères. Il nous est loisible d'accéder aux caractères un à un avec l'opérateur crochets appairés [ ] :

1. julia> fruits = "banane"

2. "banane"

3. julia> letter = fruit[1]

4. 'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)

La deuxième déclaration sélectionne le caractère numéro 1 de la variable fruit et l'attribue à la variable letter.

L'expression entre parenthèses contient un indice qui désigne le caractère traité de la séquence.

En Julia, toute séquence indicée commence à 1 : le premier élément de tout objet indicé sur la base de nombres entiers se trouve à l'indice 1 et le dernier élément correspond à end (voir la figure 8.2.1).

FIGURE 8.2.1 – Exemple d'une table d'indices pour les séquences de caractères.

Par exemple,

1. julia> fruit[end]

2. 'e': ASCII/Unicode U+0065 (category Ll: Letter, lowercase)

Comme pour tout indice, une expression qui contient des variables et des opérateurs peut être employée :

1. julia> i = 1

2. 1

3. julia> fruit[i+1]

4. 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

5. julia> fruit[end-1]

6. 'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)

Naturellement, la valeur d'un indice doit être un entier, à défaut de quoi Julia retourne une erreur :

1. julia> fruit[1.5]

2. ERROR: MethodError: no method matching getindex(::String, ::Float64)

8-3 - length

length est une fonction interne qui retourne le nombre de caractères d'une chaîne :

1. julia> fruits = "🍌 🍎 🍐"

2. "🍌 🍎 🍐"

3. julia> len = length(fruits)

4. 5

Pour obtenir la dernière lettre d'une chaîne, il est tentant d'essayer ceci :

1. julia> last = fruits[len]

2. '': ASCII/Unicode U+0020 (category Zs: Separator, space)

Le résultat est quelque peu inattendu. Les chaînes sont encodées en utilisant le codage UTF-8. Or, l'UTF-8 est un codage à largeur variable, ce qui signifie que tous les caractères ne sont pas codés avec le même nombre d'octets.

La fonction sizeof retourne le nombre d'octets d'une chaîne :

1. julia> sizeof("🍌")

2. 4

Étant donné qu'un pictogramme est codé sur 4 octets et que l'indexation des chaînes est basée sur les octets, le 5e élément de fruits est un ESPACE. Cela signifie également que l'indice de chaque octet dans une chaîne UTF-8 n'est pas nécessairement l'indice valide pour un caractère. Si un tel indice d'octet non valide était utilisé dans une chaîne, Julia afficherait une erreur :

1. julia> fruits[2]

2. ERROR: StringIndexError("🍌 🍎 🍐", 2)

Dans le cas de fruits, le pictogramme 🍌 s'étend sur quatre octets. Les indices 2, 3 et 4 ne sont donc pas valables et l'indice du caractère suivant est 5. Cet indice valable suivant peut être calculé par nextind(fruits, 1). L'indice suivant peut être obtenu par nextind(fruits, 5) et ainsi de suite.

8-4 - Parcours d'une chaîne

De nombreux programmes requièrent le traitement en série d'une ou plusieurs chaînes de caractères. Souvent, ces traitements commencent au tout début. Ils sélectionnent chaque caractère à tour de rôle, effectuent une ou plusieurs modifications puis continuent jusqu'à la fin de la chaîne. Ce type de traitement correspond à une traversée ou un parcours de chaîne. Une façon d'effectuer un tel traitement consiste à recourir à une boucle while :

1. index = firstindex(fruits)

2. while index <= sizeof(fruits)

3. letter = fruits[index]

4. println(letter)

5. global index = nextind(fruits, index)

6. end

Cette boucle parcourt la chaîne et affiche chaque élément ligne après ligne. La condition de la boucle est index <= sizeof(fruits). Donc, quand l'indice est plus grand que le nombre d'octets dans la chaîne, la condition devient fausse et le corps de la boucle n'est plus exécuté.

La fonction firstindex retourne le premier indice d'octet valide. Le mot-clé global précédant index indique que nous voulons réaffecter l'indice de la variable définie dans Main (voir la section 11.7).

8-4-1 - Exercice 8-1

Écrivez une fonction qui prend une chaîne de caractères comme argument et affiche les lettres à l'envers, une par ligne.

La lecture intégrale d'une chaîne peut être menée à bien avec une boucle for :

1. for letter in fruits

2. println(letter)

3. end

À chaque passage dans la boucle, le caractère suivant de la chaîne est attribué à la variable letter. La boucle se poursuit jusqu'à ce qu'il n'y ait plus de caractère à lire.

L'exemple suivant montre l'utilisation de la concaténation (mise bout à bout de plusieurs chaînes de caractères) et d'une boucle for pour produire une série alphabétiquement ordonnée. Dans le célèbre livre américain pour enfants de Robert McCloskey intitulé Make Way for Ducklings (dont la traduction en français a pour titre Laissez passer les canards), les noms des canetons sont Jack, Kack, Lack, Mack, Nack, Ouack, Pack et Quack. La boucle for ci-dessous produit ces noms dans l'ordre :

1. prefixes = "JKLMNOPQ"

2. suffix = "ack"

3. for letter in prefixes

4. println(letter * suffix)

5. end

Le programme retourne :

1. Jack

2. Kack

3. Lack

4. Mack

5. Nack

6. Oack

7. Pack

8. Qack

Ce retour est toutefois incorrect, puisque Ouack et Quack ne sont pas correctement écrits.

8-4-2 - Exercice 8-2

Modifiez le programme précédent pour corriger cette erreur.

8-5 - Segments de chaînes

Un segment de chaîne est appelé slice en anglais. La sélection d'un segment est similaire à la sélection d'un caractère :

1. julia> str = "Jules César";

3. julia> str[1:5]

4. "Jules"

L'opérateur [n:m] retourne la partie de la chaîne du nième au mième octet. Il faut donc faire preuve de la même prudence que pour lors de la manipulation d'indices simples.

Le mot-clé end peut être utilisé pour indiquer le dernier octet de la chaîne :

1. julia> str[7:end]

2. "César"

Si le premier indice est supérieur au second, le résultat est une chaîne vide entourée de deux guillemets doubles :

1. julia> str[78:7]

2. " "

Une chaîne vide ne contient aucun caractère et a une longueur égale à 0. Mis à part cela, ses caractéristiques sont identiques à toute autre chaîne.

8-5-1 - Exercice 8-3

Pour poursuivre cet exemple, que pensez-vous que str[:] signifie ? Essayez.

8-6 - Les chaînes sont persistantes

Il est tentant d'utiliser l'opérateur [ ] sur le côté gauche d'une affectation, avec l'intention de changer un caractère dans une chaîne. Par exemple :

1. julia> salut = "Hello, world!"

2. "Hello, world!"

3. julia> salut[1] = 'J'

4. ERROR: MethodError: no method matching setindex!(::String, ::Char, ::Int64)

La raison de cette erreur provient de ce que les chaînes de caractères sont persistantes ou inchangeables (immutable en anglais). Cela signifie qu'une chaîne existante ne peut pas être modifiée. Le mieux qu'on puisse faire est de créer une nouvelle chaîne qui soit une variation de l'originale :

1. julia> salut = J * salut[2:end]

2. "Jello, world!"

Cet exemple concatène une nouvelle première lettre avec un segment de salut. Il n'a aucun effet sur la chaîne de caractères originale. Ceci est explicité dans la figure 8.6.1.

FIGURE 8.6.1 – Schématisation du remplacement de lettre dans une chaîne de caractères.

8-7 - Interpolation des chaînes

Construire des chaînes de caractères en utilisant la concaténation peut devenir rapidement ardu. Pour réduire la nécessité de ces appels verbeux à string ou à des multiplications de chaîne, Julia autorise l'interpolation de chaînes à l'aide de $ :

1. julia> salut1 = "Hello"

2. "Hello!"

3. julia> a_qui = "World"

4. "World"

5. julia> "$salut1, $(a_qui)!"

6. "Hello, World!"

Cette formulation est plus lisible et plus pratique que la concaténation de chaînes : salut1 * ", " * a_qui * "!".

L'expression complète la plus courte après le $ est prise comme l'expression dont la valeur doit être interpolée. Ainsi, toute expression dans une chaîne peut être interpolée en utilisant des parenthèses(16) :

1. julia> greet = "1 + 2 = $(1 + 2)"

2. "1 + 2 = 3"

8-8 - Recherche dans les chaînes

Que fait la fonction suivante ?

1. function find(word, letter)

2. index = firstindex(word)

3. while index <= sizeof(word)

4. if word[index] == letter

5. return index

6. end

7. index = nextind(word, index)

8. end

9. -1

10. end

Dans un sens, cette fonction find est l'inverse de l'opérateur [ ]. Au lieu de prendre un indice et d'extraire le caractère correspondant, elle prend un caractère et trouve l'indice où ce caractère apparaît. Si le caractère n'est pas trouvé, la fonction retourne -1.

C'est le premier exemple que nous voyons d'une déclaration de retour à l'intérieur d'une boucle. Si word[index] == letter, la fonction sort de la boucle et retourne immédiatement la position de la lettre.

Si le caractère n'apparaît pas dans la chaîne de caractères, le programme sort normalement de la boucle en retournant -1.

Ce schéma de calcul s'appelle une recherche.

8-8-1 - Exercice 8-4

Modifiez find pour qu'elle contienne un troisième paramètre, en l'occurrence l'indice dans word où la recherche doit commencer.

8-9 - Boucle et compteur

Le programme suivant compte le nombre d'occurrences de la lettre a dans une chaîne :

1. word = "banane"

2. counter = 0

3. for letter in word

4. if letter == 'a'

5. global counter = counter + 1

6. end

7. end

8. println(counter)

Ce programme illustre un mode de calcul avec emploi d'un compteur. Le compteur est une variable initialisée à 0 puis incrémentée (+1) à chaque fois que la lettre a est détectée. Lorsque la boucle se termine, le compteur contient la somme des occurrences de a.

8-9-1 - Exercice 8-5

Encapsulez le code précédent dans une fonction appelée count et généralisez-le pour qu'il accepte la chaîne et la lettre comme arguments.

Ensuite, réécrivez la fonction de sorte que, au lieu de parcourir la chaîne de caractères, count utilise la version à trois paramètres de find évoquée dans la section 8.8.

8-10 - Bibliothèque des chaînes

Julia fournit des fonctions qui permettent d'effectuer diverses opérations utiles sur les chaînes de caractères. Par exemple, la fonction uppercase prend une chaîne de caractères et retourne une nouvelle chaîne, toutes ses lettres étant converties en majuscules.

1. julia> uppercase("Hello, World!")

2. "HELLO, WORLD!"

Il s'avère qu'il existe une fonction appelée findfirst qui est remarquablement similaire à la fonction find que nous avons écrite :

1. julia> findfirst("a", "banane")

2. 2:2

En fait, la fonction findfirst est plus générale que notre fonction find. En plus des caractères, elle peut trouver des sous-chaînes :

1. julia> findfirst("ane", "banane")

2. 4:6

Par défaut, findfirst commence au début de la chaîne de caractères, mais la fonction findnext prend un troisième argument, l'indice où elle doit commencer :

1. julia> findnext("ne", "banane", 3)

2. 5:6

8-11 - L'opérateur ∈

L'opérateur ∈ (\in TAB) est un booléen(17) qui prend un caractère et une chaîne. Il retourne true si le caractère apparaît dans la chaîne :

1. julia> 'a' ∈ "banane"

2. true

Par exemple, la fonction suivante permet d'afficher toutes les lettres de word1 qui apparaissent également dans word2 :

1. function inboth(word1, word2)

2. for letter in word1

3. if letter ∈ word2

4. print(letter, " ")

5. end

6. end

7. end

Avec des noms de variables bien choisis, Julia se lit parfois comme du français. Ainsi, cette boucle peut être lue comme ceci : « pour (chaque) lettre du (premier) mot, si (la) lettre est un élément du (deuxième) mot, affichez (ladite) lettre ». Voici ce qui se passe quand on « compare » des pommes et des poires.

1. julia> inboth("pommes", "poires")

2. p o e s

8-12 - Comparaison de chaînes

Les opérateurs relationnels fonctionnent aussi avec les chaînes. Pour voir si deux chaînes sont égales :

1. word = "Prigogine"

2. if word == "thermodynamique"

3. println("Génial, la thermodynamique.")

4. end

D'autres opérations relationnelles sont utiles pour mettre les mots en ordre alphabétique :

1. word = "Prigogine"

2. if word < "thermodynamique"

3. println("Le mot $word vient avant thermodynamique.")

4. elseif word > "thermodynamique"

5. println("Le mot $word vient après thermodynamique.")

6. else

7. println("Génial, la thermodynamique.")

8. end

Julia ne gère pas les lettres majuscules et minuscules comme nous le faisons. Toutes les lettres majuscules passent avant les minuscules, le résultat est donc :

1. Le mot Prigogine vient avant thermodynamique.

Une manière courante de résoudre ce problème consiste à convertir les chaînes de caractères en un format standard, par exemple toutes les minuscules, avant d'effectuer la comparaison.

8-13 - Débogage

Lorsque des indices sont exploités pour parcourir les valeurs d'une chaîne séquentiellement, il est délicat d'obtenir le début et la fin exacts de la traversée de chaîne. Voici une fonction qui est censée comparer deux mots et retourner true si l'un des mots est l'inverse de l'autre. Cependant, elle contient deux erreurs :

1. function isreverse(word1, word2)

2. if length(word1) != length(word2)

3. return false

4. end

5. i = firstindex(word1)

6. j = lastindex(word2)

7. while j >= 0

8. j = prevind(word2, j)

9. if word1[i] != word2[j]

10. return false

11. end

12. i = nextind(word1, i)

13. end

14. true

15. end

La première déclaration vérifie si les mots sont de la même longueur. Si ce n'est pas le cas, nous pouvons immédiatement retourner false. Sinon, pour le reste de la fonction, nous supposons que les mots sont de la même longueur. C'est un exemple avec une sentinelle.

i et j sont des indices : i parcourt word1 vers l'avant tandis que j parcourt word2 à rebours. Si nous trouvons deux lettres qui ne correspondent pas, nous pouvons retourner false immédiatement. Si toute la boucle est parcourue et que toutes les lettres correspondent, le programme retourne true.

La fonction lastindex retourne le dernier indice d'octet valide de la chaîne de caractères et prevind l'indice valide du caractère précédent.

Si nous testons cette fonction avec les mots « pots » et « stop », nous nous attendons à ce que la valeur de retour soit true. Ce n'est pourtant pas le cas :

1. julia> isreverse("pots", "stop")

2. false

Pour déboguer ce genre d'erreur, la première démarche consiste à imprimer la valeur des indices dans la séquence :

1. while j >= 0

2. j = prevind(word2, j)

3. @show i j

4. if word1[i] != word2[j]

En relançant le programme, nous obtenons de précieuses informations :

1. julia> isreverse("pots", "stop")

2. i = 1

3. j = 3

4. false

Lors du premier passage dans la boucle, j vaut 3. Or, ce devrait être 4. Cette valeur peut être fixée en déplaçant la ligne j = prevind(word2, j) à la fin de la boucle while.

Cela accompli, le programme est relancé, ce qui conduit au résultat :

1. julia> isreverse("pots", "stop")

2. i = 1

3. j = 4

4. i = 2

5. j = 3

6. i = 3

7. j = 2

8. i = 4

9. j = 1

10. i = 5

11. j = 0

12. ERROR: BoundsError: attempt to access "pots" at index [5]

Cette fois, une BoundsError a été émise. La valeur de i est de 5, ce qui est en dehors de la plage pour la chaîne « pots ».

8-13-1 - Exercice 8-6

Exécutez le programme sur papier, en changeant les valeurs de i et j à chaque itération. Trouvez et corrigez la deuxième erreur dans cette fonction.

8-14 - Glossaire

séquence collection ordonnée de valeurs où chaque valeur est identifiée par un indice entier,

norme ASCII norme de codage des caractères pour la communication électronique spécifiant 128 caractères,

norme Unicode norme de l'industrie informatique pour le codage, la représentation et le traitement cohérents des textes exprimés dans la plupart des systèmes d'écriture du monde,

indice valeur entière utilisée pour sélectionner un élément dans une séquence, tel qu'un caractère dans une chaîne de caractères. En Julia, les indices commencent à 1,

encodage UTF-8 encodage de caractères à largeur variable capable de coder tous les 1112064 points de code valides en Unicode en utilisant un à quatre octets de 8 bits (abréviation de l'anglais Universal Character Set Transformation Format – 8 bits),

traversée lecture des éléments dans une séquence, en effectuant une opération similaire sur chacun d'eux,

segment (slice) partie d'une chaîne de caractères délimitée par une série d'indices,

chaîne vide chaîne sans caractères et de longueur 0, représentée par deux guillemets droits,

persistance (ou immutability) propriété d'une séquence dont les éléments ne peuvent pas être modifiés,

interpolation de chaîne processus d'évaluation d'une chaîne contenant un ou plusieurs caractères, donnant un résultat dans lequel les caractères sont remplacés par leurs valeurs correspondantes,

recherche modèle de traversée d'une séquence qui s'arrête lorsqu'il trouve ce qu'il cherche,

compteur variable généralement initialisée à zéro puis incrémentée d'une unité à chaque passage dans une boucle.

8-15 - Exercices

8-15-1 - Exercice 8-7

Le but de cet exercice est de consulter la documentation Julia associées aux chaînes. Vous voudrez peut-être essayer certaines fonctions pour vous assurer que vous comprenez leur fonctionnement. Les méthodes strip et replace sont particulièrement utiles.

La documentation utilise une syntaxe qui peut être déroutante. Par exemple, dans search(string::AbstractString, chars::Chars, [start::Integer]), les crochets indiquent des arguments optionnels. Ainsi, string et chars sont obligatoires, tandis que start est optionnel.

8-15-2 - Exercice 8-8

Il existe une fonction interne appelée count similaire à la fonction count de la section 8.9 (voir en particulier l'exercice 8.9.1 et la section 4.9). Lisez la documentation de count et utilisez-la pour compter le nombre de a dans « abracadabra ».

8-15-3 - Exercice 8-9

Un segment de chaîne peut prendre un troisième indice. Le premier indique le début, le troisième la fin et le deuxième la « taille du pas », c'est-à-dire le nombre d'espaces entre les caractères successifs. Un pas de 2 signifie un caractère sur deux, un pas de 3 signifie un caractère sur trois, etc.

1. julia> mot = "servovalve"

2. "servovalve"

3. julia> mot[1:2:6]

4. "sro"

Un pas de -1 traverse le mot à l'envers, de sorte que [end:-1:1] produit une chaîne inversée. Utilisez cette technique pour écrire une version d'une ligne de la fonction ispalindrome (exercice de la sous-section 6.11.3).

8-15-4 - Exercice 8-10

Les fonctions suivantes sont toutes destinées à vérifier si une chaîne de caractères contient des minuscules, mais certaines d'entre elles sont erronées. Pour chaque fonction, décrivez ce que la fonction fait réellement (en supposant que le paramètre est une chaîne de caractères).

1. function anylowercase1(s)

2. for c in s

3. if islowercase(c)

4. return true

5. else

6. return false

7. end

8. end

9. end

11. function anylowercase2(s)

12. for c in s

13. if islowercase('c')

14. return "true"

15. else

16. return "false"

17. end

18. end

19. end

21. function anylowercase3(s)

22. for c in s

23. flag = islowercase(c)

24. end

25. flag

26. end

28. function anylowercase4(s)

29. flag = false

30. for c in s

31. flag = flag || islowercase(c)

32. end

33. flag

34. end

36. function anylowercase5(s)

37. for c in s

38. if !islowercase(c)

39. return false

40. end

41. end

42. true

43. end

8-15-5 - Exercice 8-11

Un code César est une forme de chiffrement faible qui implique le décalage de chaque lettre par un nombre fixe (le cas échéant, une lettre peut revenir à sa place initiale). Par exemple, 'A' décalé de 3 devient 'D' et 'Z' décalé de 1 devient 'A'.

Pour faire pivoter un mot, il suffit de décaler chaque lettre de la même manière. Par exemple, « oui » décalé de 10 devient « yes » et « lit » décalé de 7 devient « spa », et décalé de -4 devient « hep ». Dans le film 2001 : l'Odyssée de l'espace, l'ordinateur du vaisseau s'appelle HAL, c'est-à-dire IBM décalé de -1.(18)

Écrivez une fonction appelée rotateword qui prend une chaîne de caractères ainsi qu'un entier comme paramètres et retourne une nouvelle chaîne de caractères contenant les lettres de la chaîne originale pivotée de la quantité donnée.

Vous pouvez utiliser la fonction intégrée Int, qui convertit un caractère en un code numérique, et Char, qui convertit les valeurs numériques en caractères. Les lettres de l'alphabet sont codées dans l'ordre alphabétique, par exemple :

1. julia> Int('c') - (Int'a')

2. 2

Cela parce que c est la troisième lettre de l'alphabet. Prudence : les codes numériques des lettres majuscules sont différents :

1. julia> Char(Int('A') + 32)

2. 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

9 - Étude de cas : jeux de mots

Ce chapitre présente la deuxième étude de cas, qui consiste à résoudre des énigmes lexicales en recherchant des mots présentant certaines propriétés. Par exemple, nous trouverons les palindromes les plus longs en anglais et nous chercherons des mots dont les lettres apparaissent dans l'ordre alphabétique. Un autre plan de développement de programmes sera présenté : la réduction à un problème précédemment résolu.

9-1 - Lecture de listes de mots

Pour les exercices de ce chapitre, nous utiliserons une liste de mots français. Celle qui convient le mieux à notre objectif est une liste de mots collectée et mise dans le domaine public par Christophe Pallier dans le cadre du projet de dictionnaire Français-GUTenberg de Christophe Pythoud [8]. Il s'agit d'une liste de 336 531 mots encodés en UTF-8. Un navigateur comme Firefox permet d'enregistrer directement ce fichier sous le nom mots_FR.txt (par exemple).

Ce fichier est en mode texte brut, tout éditeur de texte peut l'afficher. Cependant, ce fichier est lisible depuis Julia. La fonction intégrée open prend le nom du fichier comme paramètre et retourne un flux de fichier utilisable pour lire le fichier.

1. julia> fin = open("/home/chemin_vers_fichier/mots_FR.txt")

2. IOStream(<file /home/chemin_vers_fichier/mots_FR.txt>)

fin est un flux de fichier utilisé pour l'entrée et lorsqu'il n'est plus nécessaire, il doit être fermé avec close(fin).

Julia fournit plusieurs fonctions de lecture, dont readline qui lit les caractères du fichier jusqu'à arriver à une NEWLINE et retourne le résultat sous forme de chaîne :

1. julia> readline(fin)

2. "a"

Le premier mot de cette liste est a (conjugaison du verbe « avoir » à la troisième personne au singulier du présent de l'indicatif).

Le flux de fichiers garde une trace de l'endroit où il s'est arrêté, si bien qu'appeler à nouveau readline conduit à l'affichage du mot suivant :

1. julia> readline(fin)

2. "à"

Parce que le fichier peut être parcouru dans une boucle for, ce programme lit le fichier mots_FR.txt et imprime chaque mot, un par ligne :

1. for line in eachline("/chemin_vers_fichier/mots_FR.txt")

2. println(line)

3. end

Le dernier mot affiché est « zythums ».

9-2 - Exercices

9-2-1 - Exercice 9-1

Écrivez un programme qui lit mots_FR.txt et n'imprime que les mots de plus de 20 caractères (sans compter les espaces).

9-2-2 - Exercice 9-2

Le roman La Disparition est un lipogramme écrit par Georges Perec (membre de l'Oulipo) en 1968 et publié en 1969. Il tire son originalité du fait que ses 300 pages (nombre variable selon les éditions) ne comportent pas une seule fois la lettre e, pourtant généralement la plus utilisée dans la langue française.

Écrivez une fonction appelée hasno_e qui retourne true si le mot passé en argument ne contient pas la lettre e.

Modifiez votre programme par rapport à la section précédente pour imprimer seulement les mots qui ne contiennent pas de e et calculez-en le pourcentage sur la liste.

9-2-3 - Exercice 9-3

Écrivez une fonction nommée avoids qui prend un mot et une chaîne de lettres interdites en arguments, et qui retourne true si le mot n'en utilise aucune.

Modifiez votre programme pour demander à l'utilisateur d'entrer une chaîne de lettres interdites, puis imprimez le nombre de mots qui ne contiennent aucune de ces lettres. Pouvez-vous trouver une combinaison de 5 lettres interdites qui exclut le plus petit nombre de mots ?

9-2-4 - Exercice 9-4

Écrivez une fonction nommée usesonly qui prend un mot et une chaîne de lettres, et qui retourne true si le mot ne contient que des lettres de cette série. Pouvez-vous faire une phrase (autre que « Hoe alfalfa ») en utilisant uniquement les lettres acefhlo ?

9-2-5 - Exercice 9-5

Écrivez une fonction appelée usesall qui prend un mot et une chaîne de lettres obligatoires, et qui retourne true si le mot utilise toutes les lettres obligatoires au moins une fois. Combien y a-t-il de mots qui utilisent toutes les voyelles aeiou ? Que diriez-vous de aeiouy ?

9-2-6 - Exercice 9-6

Écrivez une fonction appelée isabecedarian qui retourne true si les lettres d'un mot apparaissent dans l'ordre alphabétique (les lettres doubles sont admises). Combien existe-t-il de mots abécédaires ?

9-3 - Recherche

Tous les exercices de la section 9.2 présentent un point commun. Ils peuvent être résolus avec le modèle de recherche dans les chaînes (voir la section 8.8). L'exemple le plus simple est le suivant :

1. function hasno_e(word)

2. for letter in word

3. if letter == 'e'

4. return false

5. end

6. end

7. true

8. end

La boucle for traverse les caractères du mot passé en argument. Si la lettre e est détectée, le programme retourne immédiatement false. Sinon, il passe à la lettre suivante. Si le programme sort de la boucle normalement, cela signifie que la lettre e n'a pas été décelée et, par conséquent, le programme retourne true.

Cette fonction pourrait être reformulée de manière plus concise en utilisant l'opérateur ∉ (\notin TAB), mais la version de base illustre bien la logique du schéma de recherche.

La fonction avoids est une version plus générale de hasno_e, bien qu'elle présente la même structure :

1. function avoids(word, forbidden)

2. for letter in word

3. if letter ∉ forbidden

4. return false

5. end

6. end

7. true

8. end

Le programme peut retourner false dès qu'il trouve une lettre interdite passée comme second argument. S'il arrive au bout de la boucle, le programme retourne true.

La fonction usesonly est analogue, si ce n'est que le sens de la condition est inversé :

1. function usesonly(word, available)

2. for letter in word

3. if letter ∉ available

4. return false

5. end

6. end

7. true

8. end

Au lieu d'une série de lettres interdites, nous avons une série de lettres permises. Si nous trouvons une lettre qui n'est pas présente dans un mot, le programme retourne false.

useall est similaire, sauf que nous inversons le rôle du mot et de la série de lettres :

1. function useall(word, required)

2. for letter in required

3. if letter ∉ word

4. return false

5. end

6. end

7. true

8. end

Au lieu de parcourir les lettres du mot, la boucle parcourt les lettres obligatoires. Si l'une des lettres requises n'apparaît pas dans le mot, le programme retourne false.

Si vous pensez vraiment comme un informaticien, vous avez identifié que usesall est un exemple de problème déjà résolu. Dans ce cas, il est vraisemblable que vous ayez écrit :

1. function useall(word, required)

2. usesonly(required, word)

3. end

Il s'agit d'un exemple de plan de développement de programmes appelé réduction à un problème préalablement résolu. Cela signifie que le programmeur identifie le problème sur lequel il travaille comme un cas de problème résolu et qu'il applique une solution existante.

9-4 - Boucles sur les indices

Les fonctions de la section 9.3 ont été écrites avec des boucles for parce que seuls divers caractères des chaînes étaient utiles. Il était donc possible de travailler sans les indices.

Pour isabecedarian, nous devons comparer des lettres adjacentes, ce qui est un peu délicat avec une boucle for :

1. function isabecedarian(word)

2. i = firstindex(word)

3. previous = word[i]

4. j = nextind(word, i)

5. for c in word[j:end]

6. if c < previous

7. return false

8. end

9. previous = c

10. end

11. true

12. end

Une autre possibilité consiste à faire appel à la récursion :

1. function isabecedarian(word)

2. if length(word) <= 1

3. return true

4. end

5. i = firstindex(word)

6. j = nextind(word, i)

7. if word[i] > word[j]

8. return false

9. end

10. isabecedarian(word[j:end])

11. end

Une option supplémentaire revient à exploiter une boucle while :

1. function isabecedarian(word)

2. i = firstindex(word)

3. j = nextind(word, 1)

4. while j <= sizeof(word)

5. if word[j] < word[i]

6. return false

7. end

8. i = j

9. j = nextind(word, i)

10. end

11. true

12. end

La boucle commence à i = 1 et j = nextind(word, 1). Elle se termine lorsque j > sizeof(word). À chaque passage, la boucle compare le iième caractère (qui peut être considéré comme le caractère courant) au jième caractère (qui est le suivant).

Si le caractère suivant est inférieur en termes d'ordre alphabétique au caractère courant, alors il y a rupture dans le schéma abécédaire. En conséquence, le programme retourne false.

Si la fin de la boucle est atteinte sans trouver de rupture, alors le mot passe le test. Pour se convaincre que la boucle se termine correctement, nous pouvons prendre pour exemple « choux ».

À présent, voici une version d'ispalindrome qui utilise deux indices. L'un commence au début et croît, l'autre commence à la fin et décroît.

1. function ispalindrome(word)

2. i = firstindex(word)

3. j = lastindex(word)

4. while i < j

5. if word[i] != word[j]

6. return false

7. end

8. i = nextind(word, i)

9. j = prevind(word, j)

10. end

11. true

12. end

Ce cas peut être réduit à un problème préalablement résolu :

1. function ispalindrome(word)

2. isreverse(word, word)

3. end

La fonction isreverse a été étudiée à la section 8.13.

9-5 - Débogage

Tester des programmes est une tâche complexe. Les fonctions présentées dans ce chapitre sont relativement faciles à tester, car les résultats sont vérifiables manuellement. Nonobstant cela, il est difficile — voire impossible — de choisir un ensemble de mots qui permettent de tester toutes les erreurs possibles.

Par exemple avec hasno_e, deux cas sont évidents à vérifier : les mots qui ont un e devraient amener le programme à retourner false, et les mots qui n'en ont pas devrait forcer le programme à retourner true. Trouver chacun de ces cas est assez facile.

Très souvent, existent des sous-cas moins évidents. Parmi les mots qui contiennent un « e », il convient de tester les mots qui ont un « e » au début, à la fin et quelque part au milieu. Cela implique de tester les mots longs, les mots courts et les mots très courts, comme la chaîne vide. Or, la chaîne vide est un exemple de cas particulier non trivial où des erreurs se camouflent souvent.

En plus des situations de test à traiter manuellement, tester le programme avec une liste de mots comme mots_FR.txt est nécessaire. En analysant la sortie, il est peut-être possible de détecter des erreurs. Attention, toutefois : on court toujours le risque de détecter un type d'erreur (des mots qui ne devraient pas être inclus, mais qui le sont) et inversement (des mots qui devraient être inclus et qui… ne le sont pas).

En général, les tests contribuent à trouver des bogues. En tout état de cause, il n'est guère facile de produire un bon ensemble de tests. Même dans ce cas, personne ne peut être sûr à 100 % que le programme est correct. Selon le célèbre informaticien Edsger W. Dijkstra :

Program testing can be used to show the presence of bugs, but never to show their absence !(19)

9-6 - Glossaire

flux de fichiers valeur qui représente un fichier ouvert.

réduction à un problème préalablement résolu procédé destiné à résoudre un problème en l'exprimant comme un cas de figure déjà résolu.

cas particulier cas de test atypique ou non trivial (et susceptible de ne pas être traité correctement).

9-7 - Exercices 9-7

9-7-1 - Exercice

Cet exercice est basé sur un casse-tête qui fut diffusé dans l'émission de radio Car Talk :

« Donnez-moi un mot avec trois paires de lettres consécutives.
Je vous donne deux mots qui se qualifient presque. Par exemple, le mot « committee », c-o-m-m-i-t-t-e-e. Ce serait génial, sauf pour le i qui se faufile au milieu. Ou « Mississippi » : M-i-s-s-i-s-s-i-p-p-i. Si vous pouviez enlever ces i, ce serait parfait. Pourtant, il existe un mot qui a trois paires de lettres consécutives et, à ma connaissance, c'est peut-être le seul mot. Bien sûr, il y en a probablement 500 autres, mais je n'en connais qu'un seul. Quel est ce mot ? »

Écrivez un programme qui résout ce problème.

9-7-2 - Exercice 9-8

Voici un autre casse-tête de l'émission de radio Car Talk :

« L'autre jour, je conduisais sur l'autoroute et, par hasard, j'ai jeté un coup d'œil à mon compteur kilométrique. Comme la plupart des odomètres, il affiche six chiffres, en kilomètres entiers seulement. Ainsi, si ma voiture avait par exemple fait 300 000 miles, je verrais 3-0-0-0-0-0.

Ce que j'ai vu ce jour-là était très intéressant. J'ai remarqué que les quatre derniers chiffres étaient palindromiques, c'est-à-dire qu'ils lisaient de la même façon en avant et en arrière. Par exemple, 5-4-4-5 est un palindrome. Donc, mon odomètre aurait pu afficher 3-1-5-4-4-5.

Un kilomètre plus tard, les 5 derniers chiffres étaient palindromiques. Par exemple, il aurait pu lire 3-6-5-4-5-6. Un kilomètre plus loin, les 4 chiffres du milieu sur 6 étaient palindromiques. Vous êtes prêt ? Un kilomètre plus tard, les 6 chiffres formaient un palindrome !

La question est de savoir ce qui était inscrit sur l'odomètre quand je l'ai regardé la première fois. »

Rédigez un programme Julia qui teste tous les nombres à six chiffres et qui affiche tous les nombres répondant aux exigences du casse-tête.

9-7-3 - Exercice 9-9

Voici un dernier casse-tête de Car Talk :

« Récemment, j'ai eu la visite de ma mère. Nous avons réalisé que les deux chiffres qui composent mon âge, lorsqu'ils sont inversés, donnent son âge. Par exemple, si elle est âgée de 73 ans, j'ai 37 ans. Nous nous sommes demandées combien de fois cela s'était produit au fil des ans, mais nous nous sommes laissées distraire par d'autres sujets et nous n'avons jamais trouvé de réponse.

En rentrant chez moi, je me suis rendu compte que les chiffres de notre âge ont été réversibles six fois jusqu'à présent. J'ai aussi compris que si nous avons de la chance, cela se reproduirait dans quelques années, et si nous avons vraiment de la chance, cela se reproduirait encore une fois après. En d'autres termes, cela se produirait 8 fois en tout. La question est donc de savoir quel âge j'ai maintenant ? »

Écrivez un programme Julia qui cherche des solutions à cette énigme.

La fonction lpad pourrait vous être utile.

10 - Tableaux

Ce chapitre présente un des types intégrés les plus utiles de Julia : les tableaux. Nous allons apprendre également ce qu'est un objet et ce qui se produit éventuellement lorsque plusieurs noms désignent le même objet.

10-1 - Un tableau est une séquence

À l'instar d'une chaîne de caractères, un tableau est une séquence de valeurs. Dans une chaîne, les valeurs sont des caractères. Dans un tableau, elles peuvent être de n'importe quel type. Les valeurs d'un tableau sont appelées des éléments (ou parfois des items).

Il existe plusieurs façons de créer un nouveau tableau. La plus simple consiste à mettre les éléments entre crochets [ ] :

1. julia> [10, 20, 30, 40]

2. 4-element Array{Int64,1}:

3. 10

4. 20

5. 30

6. 40

1. julia> ["ici", "là", "encore", "déjà"]

2. julia> ["spam", 2.0, 5, [10, 20]]

La deuxième ligne du dernier encadré illustre un cas de tableau imbriqué.

Un tableau qui ne contient aucun élément est dit « vide ». Il est possible d'en créer un avec des crochets sans contenu [ ].

Comme on peut s'y attendre, il est possible d'affecter des tableaux à des variables :

1. julia> fromages = ["Maroilles", "Beaufort", "Ossau-iraty"];

3. julia> nombres = [42, 123];

5. julia> vide = [];

7. julia> print(fromages, " ", nombres, " ", vide)

8. ["Maroilles", "Beaufort", "Ossau-iraty"] [42, 123] Any[]

La fonction typeof permet de connaître le type d'un tableau :

1. julia> typeof(fromages)

2. Array{String,1}

3. julia> typeof(nombres)

4. Array{Int64,1}

5. julia> typeof(vide)

6. Array{Any,1}

Entre les accolades, se trouvent le type et un nombre. Ce nombre indique les dimensions du tableau (un tableau simple a une seule dimension ; un tableau bidimensionnel ou matrice, deux, etc.). Le tableau vide contient des valeurs de type Any, c'est-à-dire qu'il peut contenir des valeurs de tout type.

10-2 - Les tableaux sont non persistants

La syntaxe pour accéder aux éléments d'un tableau est la même que celle permettant d'accéder aux caractères d'une chaîne : il convient d'utiliser l'opérateur [ ]. L'expression à l'intérieur des parenthèses spécifie l'indice. Pour rappel, en Julia, les indices commencent à 1 comme indiqué dans la figure 10.2.1.

FIGURE 10.2.1 – Tableau nommé alpha et ses indices.

Par exemple :

1. julia> fromages[1]

2. "Maroilles"

1. julia> alpha = ["spam", 2.0, 5, [10, 20]]

2. julia> alpha[4][2]

3. 20

Contrairement aux chaînes, les tableaux sont non persistants (c'est-à-dire qu'ils sont modifiables). Lorsque l'opérateur [ ] apparaît à gauche d'une affectation, il identifie l'élément du tableau concerné par cette dernière :

1. julia> nombres[2] = 5

2. 5

3. julia> print(nombres)

4. [42, 5]

Alors qu'il était auparavant égal à 123, le second élément de nombres vaut désormais 5.

La figure 10.2.2 montre les diagrammes d'état pour fromages, nombres et vide.

FIGURE 10.2.2 – Diagrammes d'état pour trois tableaux élémentaires.

Les tableaux sont représentés par des cases contenant les éléments et les indices. fromages fait référence à un tableau avec trois éléments indicés 1, 2 et 3. Le tableau nombres contient deux éléments. Le diagramme montre que la valeur du deuxième élément a été réaffectée de 123 à 5. Quant à lui, vide fait référence à un tableau sans élément.

Les indices de tableau fonctionnent de la même manière que ceux des chaînes (mais sans les mises en garde UTF-8) :

- toute expression entière peut être utilisée comme un indice. Par exemple, alpha[1+1] retourne 2.0 ;

- si vous essayez de lire ou d'écrire un élément qui n'existe pas, vous obtenez une BoundsError ;

- le mot-clé end pointe vers le dernier indice d'un tableau.

L'opérateur ∈ fonctionne également sur les tableaux :

1. julia> "Ossau-iraty" ∈ fromages

2. true

3. julia> "Brie" in fromages

4. false

10-3 - Parcourir un tableau

Avant de parcourir un tableau, il est utile de voir comment déterminer le nombre d'éléments qui le constituent en utilisant la fonction length :

1. julia> length(nombres)

2. 2

3. julia> length(alpha)

4. 4

Bien qu'un tableau puisse en contenir un autre (comme c'est le cas pour alpha), le tableau imbriqué compte toujours comme un élément unique.

La manière la plus courante de parcourir les éléments d'un tableau consiste à utiliser une boucle for. La syntaxe est la même que pour les chaînes de caractères :

1. for fromage in fromages

2. println(fromage)

3. end

Cela ne fonctionne bien que quand il suffit de lire les éléments du tableau. S'il est nécessaire d'en ajouter, d'en retirer ou d'en mettre à jour, il faut manipuler les indices. Une façon courante de procéder repose sur l'usage de la fonction interne eachindex :

1. for i in eachindex(nombres)

2. nombres[i] = nombres[i] * 2

3. end

Cette boucle for parcourt le tableau et met à jour chaque élément. Chaque fois que la boucle est parcourue, nous obtenons l'indice de l'élément suivant. L'instruction d'affectation dans le corps utilise i pour lire l'ancienne valeur de l'élément et pour affecter la nouvelle valeur.

Une boucle for sur un tableau vide ne fait jamais tourner le corps de cette boucle :

1. for x in []

2. println("Ceci n'arrivera pas!")

3. end

10-4 - Segments de tableau

L'opérateur de segmentation fonctionne également avec les tableaux :

1. julia> t = ['a', 'b', 'c', 'd', 'e', 'f'];

3. julia> print(t[1:3])

4. ['a', 'b', 'c']

5. julia> print(t[3:end])

6. ['c', 'd', 'e', 'f']

L'opérateur de segmentation [:] effectue une copie de l'ensemble du tableau :

1. julia> print(t[:])

2. ['a', 'b', 'c', 'd', 'e', 'f']

Les tableaux étant non persistants, il est souvent utile d'en faire une copie avant d'effectuer des opérations qui les modifient.

Un opérateur de segmentation sur le côté gauche d'une affectation peut mettre à jour plusieurs éléments d'un coup :

1. julia> t[2:3] = ['x', 'y'];

3. julia> print(t)

4. ['a', 'x', 'y', 'd', 'e', 'f']

10-5 - Bibliothèque de fonctions associées aux tableaux

Plusieurs fonctions internes de Julia sont conçues pour agir sur les tableaux. Par exemple, push! ajoute un nouvel élément à la fin d'un tableau :

1. julia> t = ['a', 'b', 'c'];

3. julia> push!(t, 'd');

5. julia> print(t)

6. ['a', 'b', 'c', 'd']

La fonction append! ajoute des éléments d'un second tableau à la fin d'un premier :

1. julia> t1= ['a', 'b', 'c'];

3. julia> t2= ['d', 'e'];

5. julia> append!(t1, t2);

7. julia> print(t1)

8. ['a', 'b', 'c', 'd', 'e']

Il doit être noté que t2 n'est pas modifié au cours de cette opération.

La fonction sort! organise les éléments du tableau dans l'ordre croissant (ou alphabétique) :

1. julia> t = ['d', 'c', 'e', 'b', 'a'];

3. julia> sort!(t);

5. julia> print(t)

6. ['a', 'b', 'c', 'd', 'e']

La fonction sort retourne une copie des éléments du tableau dans l'ordre :

1. julia> t1 = ['d', 'c', 'e', 'b', 'a'];

3. julia> t2 = sort(t1);

5. julia> print(t1)

6. ['d', 'c', 'e', 'b', 'a']

7. julia> print(t2)

8. ['a', 'b', 'c', 'd', 'e']

Par convention syntaxique, dans Julia, ! est associé aux noms des fonctions qui modifient leurs arguments.

10-6 - Mise en correspondance (mapping), filtre et réduction

Pour additionner tous les nombres d'un tableau, une boucle comme celle-ci est utilisable :

1. function addall(t)

2. total = 0

3. for x in t

4. total += x

5. end

6. total

7. end

La variable locale total est initialisée à 0. À chaque passage dans la boucle for, += extrait un élément du tableau et l'ajoute à total. L'opérateur += constitue un raccourci pour mettre à jour une variable. Cette déclaration d'affectation avec incrémentation,

1. total += x

équivaut à :

1. total = total + x

À chaque passage dans la boucle, total accumule la somme des éléments. Une variable utilisée de cette façon est appelée un accumulateur.

L'addition des éléments d'un tableau est une opération si courante que Julia fournit la fonction interne sum :

1. julia> t= [1, 2, 3, 4]

2. sum(t)

3. 10

Une opération de ce type qui combine une séquence d'éléments en une seule valeur s'appelle une opération de réduction.

Souvent, il faut parcourir un tableau tout en en construisant un autre. Par exemple, la fonction suivante prend un tableau de chaînes de caractères et retourne un nouveau tableau qui contient les chaînes en majuscules :

1. function capitalizeall(t)

2. res = []

3. for s in t

4. push!(res, uppercase(s))

5. end

6. res

7. end

La variable res est initialisé avec un tableau vide. À chaque passage par la boucle, l'élément suivant est ajouté à res qui, en conséquence, constitue un autre type d'accumulateur.

L'opération qu'effectue capitalizeall est parfois appelée mapping parce que cette fonction effectue une « mise en correspondance » sur chacun des éléments d'une séquence (en l'occurrence avec une conversion en majuscules).

Une autre opération courante consiste à sélectionner certains des éléments d'un tableau et à retourner un sous-tableau. Par exemple, la fonction suivante prend un tableau de chaînes de caractères et retourne un tableau qui ne contient que les chaînes en majuscules :

1. function onlyupper(t)

2. res = []

3. for s in t

4. if s == uppercase(s)

5. push!(res, s)

6. end

7. end

8. res

9. end

Une fonction comme onlyupper agit tel un filtre puisqu'elle opère sélectivement sur certains éléments.

La plupart des opérations courantes sur les tableaux peuvent être exprimées comme une combinaison de « mise en correspondance » (mapping), de filtrage et de réduction.

10-7 - Syntaxe avec points

Pour chaque opérateur binaire tel que ^, il y a un opérateur pointé correspondant .^ automatiquement défini pour effectuer distributivement ^ élément par élément sur les tableaux. Par exemple, [1, 2, 3] ^ 3 n'est pas défini, mais [1, 2, 3] .^ 3 est défini comme le calcul du résultat élément par élément [1^3, 2^3, 3^3] :

1. julia> print([1, 2, 3] .^ 3)

2. [1, 8, 27]

Dans Julia, toute fonction f peut être appliquée de manière distribuée à tout tableau. Par exemple, pour mettre en majuscules un tableau de chaînes de caractères, nous n'avons pas besoin d'une boucle explicite :

1. julia> t = uppercase.(["abc", "def", "ghi"]);

3. julia> print(t)

4. ["ABC", "DEF", "GHI"]

C'est une façon élégante de créer un mapping. La fonction capitalizeall peut être mise en œuvre en une seule ligne :

1. function capitalizeall(t)

2. uppercase.(t)

3. end

10-8 - Suppression et insertion d'éléments

Il existe plusieurs façons de supprimer des éléments appartenant à un tableau. Si l'indice de l'élément à supprimer est connu, la fonction splice! est utilisable :

1. julia> t = ['a', 'b', 'c'];

3. julia> splice!(t, 2)

4. 'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)

5. julia> print(t)

6. ['a', 'c']

splice! modifie le tableau et retourne l'élément supprimé.

pop! supprime le dernier élément d'un tableau et le retourne :

1. julia> t = ['a', 'b', 'c'];

3. julia> pop!(t)

4. 'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

5. julia> print(t)

6. ['a', 'b']

popfirst! supprime le premier élément et le retourne :

1. julia> t = ['a', 'b', 'c'];

3. julia> popfirst!(t)

4. 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

5. julia> print(t)

6. ['b', 'c']

Les fonctions pushfirst! et push! insèrent un élément au début et à la fin d'un tableau, respectivement.

Si vous n'avez pas besoin de la valeur supprimée, vous pouvez tirer profit de la fonction deleteat! :

1. julia> t = ['a', 'b', 'c'];

3. julia> print(deleteat!(t, 2))

4. ['a', 'c']

La fonction insert! insère un élément à un indice donné :

1. julia> t = ['a', 'b', 'c'];

3. julia> print(insert!(t, 2, 'x'))

4. ['a', 'x', 'b', 'c']

10-9 - Tableaux et chaînes

Une chaîne est une séquence de caractères alors qu'un tableau est une séquence de valeurs. Ceci dit, un tableau de caractères n'est pas une chaîne. Pour convertir une chaîne de caractères en un tableau de caractères, il convient d'employer la fonction collect :

1. julia> t = collect("spam");

3. julia> print(t)

4. ['s', 'p', 'a', 'm']

La fonction collect permet de décomposer une chaîne ou une autre séquence en éléments individuels.

Si vous souhaitez décomposer une chaîne de caractères en mots, vous pouvez utiliser la fonction split :(20)

1. julia> t = split("Au calme clair de lune triste et beau");

3. julia> print(t)

4. SubString{String}["Au", "calme", "clair", "de", "lune", "triste", "et", "beau"]

Un délimiteur (qui constitue un argument optionnel) précise le(s) caractère(s) à utiliser pour détacher les mots les uns des autres. L'exemple suivant utilise un trait d'union comme délimiteur :

1. julia> t = split("spam-spam-spam", '-');

3. julia> print(t)

4. SubString{String}["spam", "spam", "spam"]

La fonction join effectue l'opération inverse de split. Elle prend un ensemble de chaînes de caractères et concatène les éléments :

1. julia> t = ["Au", "calme", "clair", "de", "lune", "triste", "et", "beau"];

3. julia> s = join(t, ' ')

4. "Au calme clair de lune triste et beau"

Dans ce cas, le délimiteur est un caractère d'espacement. Pour concaténer des chaînes de caractères sans espace(s), il convient de ne pas spécifier de délimiteur.

10-10 - Objets et valeurs

Un objet est une structure de données à laquelle une variable peut se référer. Jusqu'à présent, vous pouviez utiliser indifféremment les termes « objet » et « valeur », qui constituent des séquences informatiques.

Si les déclarations d'affectation sont effectuées ainsi :

1. a = "banane"

2. b = "banane"

il est clair que a et b font tous deux référence à une chaîne de caractères. Cependant, a et b font-elles référence à la même chaîne ? Il existe deux états possibles illustrés dans la figure 10.10.1.

(a) a et b pointent vers deux objets différents.

(b) a et b pointent vers le même objet.

FIGURE 10.10.1 – Diagramme d'état et notion d'objets

Pour vérifier si deux variables désignent le même objet, il faut utiliser l'opérateur (\equiv TAB) ou ===.

1. julia> a = "banane"

2. "banane"

3. julia> b = "banane"

4. "banane"

5. julia> a ≡ b

6. true

Dans cet exemple, Julia n'a créé qu'un seul objet « chaîne de caractères ». Nous avons donc affaire au cas représenté à la sous-figure 10.10.1b.

A contrario, lorsque deux tableaux sont instanciés, nous sommes confrontés à deux objets :

1. julia> a = [1, 2, 3];

3. julia> b = [1, 2, 3];

5. julia> a ≡ b

6. false

Dans ce cas, les deux tableaux sont équivalents parce qu'ils contiennent les mêmes éléments. Toutefois, ils ne sont pas identiques parce qu'ils ne représentent pas le même objet. Ce cas est illustré dans le diagramme d'état de la figure 10.10.2.

FIGURE 10.10.2 – Diagramme d'état pour deux tableaux équivalents, mais non identiques.

Si deux objets sont identiques, ils sont forcément équivalents, mais, s'ils sont équivalents, ils ne sont pas nécessairement identiques.

Pour être précis, un objet a une valeur. Si nous évaluons [1, 2, 3], nous obtenons un tableau d'objets dont la valeur est une séquence d'entiers. Si un autre tableau a les mêmes éléments, on dit qu'il a la même valeur, mais il ne s'agit pas du même objet.

10-11 - Aliasing

(21) Si a se rapporte à un objet et que nous effectuons l'affectation b = a, alors les deux variables se rapportent au même objet :

1. julia> a = [1, 2, 3];

3. julia> b = a;

5. julia> a ≡ b

6. true

Le diagramme d'état lié à cette situation est donné à la figure 10.11.1.

FIGURE 10.11.1 – Diagramme d'état pour deux alias.

L'association d'une variable à un objet est un référencement. Dans cet exemple, il y a deux références au même objet, symbolisées par les deux flèches.

Un objet ayant plus d'une référence possède plus d'un nom, on dit donc que l'objet est aliasé.

Si l'objet aliasé est non persistant, les modifications apportées à un alias affectent l'autre :

1. julia> b[1] = 42

2. 42

3. julia> print(a)

4. [42, 2, 3]

Bien que ce comportement puisse être utile, il est sujet à des erreurs. En général, il est plus sûr d'éviter les alias lorsque sont traités des objets non persistants.

En revanche, pour les objets persistants comme les chaînes de caractères, l'aliasing ne constitue pas un véritable problème. Dans cet exemple (qui se réfère à la sous-figure 10.10.1a) :

1. a = "banane"

2. b = "banane"

travailler sur a n'affecte pas b.

10-12 - Tableaux en tant qu'argument

Lorsqu'un tableau est passé en argument à une fonction, la fonction acquiert une référence au tableau, puisqu'elle y accède. Si la fonction modifie le tableau, l'appelant perçoit le changement. Par exemple, deletehead! supprime le premier élément d'un tableau :

1. function deletehead!(t)

2. popfirst!(t)

3. end

Voici un exemple :

1. julia> letters = ['a', 'b', 'c'];

3. julia> deletehead!(letters);

5. julia> print(letters)

6. ['b', 'c']

Le paramètre t et la variable letters sont des alias pour le même objet. Le diagramme de pile est donné à la figure 10.12.1.

FIGURE 10.12.1 – Diagramme de pile dans le cas d'un tableau passé en argument à une fonction.

Lorsque letters est passé à t, les variables variables (l'une globale, letters, et l'autre locale, t) se réfèrent au même tableau.

Il est important de distinguer les opérations qui modifient les tableaux et celles qui en créent de nouveaux. Par exemple, push! modifie un tableau alors que vcat en crée un nouveau.

Voici un exemple d'utilisation de push! :

1. julia> t1 = [1, 2];

3. julia> t2 = push!(t1,3);

5. julia> print(t1)

6. ['1', '2', '3']

En l'espèce, t2 est un alias de t1.

À présent, voici un exemple d'utilisation de la fonction vcat :

1. julia> t3 = vcat(t1, [4]);

3. julia> print(t1)

4. ['1', '2', '3']

5. julia> print(t3)

6. ['1', '2', '3', '4']

Le résultat de vcat est un nouveau tableau. Le tableau original est inchangé.

Cette différence est importante lorsque sont rédigées des fonctions censées modifier des tableaux.

Par exemple, cette fonction baddeletehead ne supprime pas le premier élément d'un tableau :

1. function baddeletehead(t)

2. t = t[2:end]     # erroné

3. end

L'opérateur de segmentation crée un nouveau tableau et l'affectation y fait référence, mais cela n'altère pas l'argument de la fonction baddeletehead en lui-même.

1. julia> t4 = baddeletehead(t3);

3. julia> print(t3)

4. ['1', '2', '3', '4']

5. julia> print(t4)

6. ['2', '3', '4']

Au début de baddeletehead, t et t3 font référence au même tableau. À la fin, t fait référence à un nouveau tableau, mais t3 pointe toujours vers le tableau original, non modifié.

Une autre manière de procéder consiste à écrire une fonction qui crée et retourne un nouveau tableau. Par exemple, tail retourne tout sauf le premier élément d'un tableau :

1. function tail(t)

2. t[2:end]

3. end

Cette fonction ne modifie pas le tableau original. Voici comment elle est utilisée :

1. julia> letters = ['a', 'b', 'c'];

3. julia> rest = tail(letters);

5. julia> print(rest)

6. ['b', 'c']

10-13 - Débogage

Une utilisation imprudente des tableaux (et des autres objets non persistants) peut entraîner de longues heures de débogage. Voici quelques pièges courants et les moyens de les éviter :

- la plupart des fonctions agissant sur les tableaux modifient ces derniers. Cette caractéristique les oppose aux fonctions traitant les chaînes de caractères, qui retournent une nouvelle chaîne de caractères et laissent l'original intact.
Si vous avez l'habitude d'écrire du code de type chaîne de caractères de cette façon :

1. new_word = strip(word)

Il est tentant d'écrire un code de tableau comme celui-ci :

1. t2 = sort!(t1)

Parce que sort! retourne le tableau original modifié t1, t2 est un alias de t1.

Avant d'utiliser les fonctions et les opérateurs agissant sur tableaux, il est vivement recommandé de lire attentivement la documentation et de tester ces fonctions en mode interactif.

- il est recommandé de choisir une manière de faire et de s'y tenir.
Une partie du problème avec les tableaux provient du grand nombre de techniques utilisables. Par exemple, pour supprimer un élément d'un tableau, il est concevable utiliser pop!, popfirst!, delete_at ou même l'affectation d'un segment extrait du tableau. Pour ajouter un élément, il est possible de tirer parti de push!, pushfirst!, insert! ou vcat. En supposant que t soit un tableau et x un élément de ce tableau, ces méthodes sont correctes :

1. insert!(t, 4, x)

2. push!(t, x)

3. append!(t, [x])

Alors que celles-ci sont erronées :

1. insert!(t, 4, [x])    # erroné

2. push!(t, [x])         # erronée

3. vcat(t, [x])          # erroné

- il est fortement préconisé d'effectuer des copies pour éviter les alias.
Lors de l'utilisation d'une fonction comme sort! qui modifie l'argument, alors qu'il s'avère nécessaire de conserver également le tableau original, une copie devrait être créée :

1. julia> t = [3, 1, 2];

3. julia> t2 = t[:];     # t2 = copy(t)!

5. julia> sort!(t2);

7. julia> print(t)

8. [3, 1, 2]

9. julia> print(t2)

10. [1, 2, 3]

Dans cet exemple, il est judicieux de tirer avantage de la fonction intégrée sort, qui retourne un nouveau tableau trié tout en laissant intact l'original :

1. julia> sort!(t2);

3. julia> println(t)

4. [3, 1, 2]

5. julia> println(t2)

6. [1, 2, 3]

10-14 - Glossaire

tableau séquence de valeurs.

élément (ou item) une des valeurs d'un tableau (ou de toute autre séquence, comme une matrice).

tableau imbriqué tableau constituant un élément d'un autre tableau.

accumulateur variable utilisée dans une boucle pour additionner (c'est-à-dire accumuler) un résultat.

déclaration d'affectation incrémentée déclaration qui met à jour la valeur d'une variable en utilisant un opérateur comme = (par exemple, +=).

opérateur point (dot) opérateur binaire qui est appliqué élément par élément à des tableaux.

syntaxe avec dot syntaxe utilisée pour appliquer une fonction par élément à un tableau quelconque.

opération de réduction modèle de traitement qui parcourt un tableau et accumule les éléments en un seul résultat.

mapping (ou mise en correspondance) modèle de traitement qui parcourt un tableau et effectue une opération sur chaque élément.

filtre modèle de traitement qui parcourt un tableau et sélectionne les éléments qui satisfont à un critère donné.

objet composant auquel une variable peut se référer. Tout objet a un type et une valeur.

équivalence propriété qu'ont deux séquences (par exemple des tableaux) à posséder les mêmes éléments de deux objets différents.

identité propriété qu'ont deux séquences (par exemple des tableaux) à désigner le même objet (ce qui implique a minima une équivalence).

référence association entre une variable et sa valeur.

aliasing cas de pseudonymie où deux ou plusieurs variables se réfèrent au même objet.

arguments facultatifs arguments figurant dans une fonction, mais dont l'usage n'est pas toujours nécessaire.

délimiteur caractère ou chaîne de caractères utilisés pour indiquer l'endroit où une chaîne doit être démariée du reste de la chaîne.

10-15 - Exercices

10-15-1 - Exercice 10-1

Écrivez une fonction appelée nestedsum qui prend un tableau contenant des tableaux d'entiers et additionne les éléments de tous les tableaux imbriqués. Par exemple :

1. julia> t = [[1, 2], [3], [4, 5, 6]];

3. julia> nestedsum(t)

4. 21

10-15-2 - Exercice 10-2

Écrivez une fonction appelée cumulsum qui prend un tableau de nombres et retourne la somme cumulée ; c'est-à-dire un nouveau tableau où le iième élément est la somme des i premiers éléments du tableau original. Par exemple :

1. julia> t = [1, 2, 3];

3. julia> print(cumulsum(t))

4. 2Any[1, 3, 6]

10-15-3 - Exercice 10-3

Écrivez une fonction appelée interior qui prend un tableau et retourne un nouveau tableau qui contient tous les éléments sauf le premier et le dernier. Par exemple :

1. julia> t = [1, 2, 3, 4];

3. julia> print(interior(t))

4. [2, 3]

10-15-4 - Exercice 10-4

Écrivez une fonction appelée interior! qui prend un tableau, le modifie en supprimant le premier et le dernier élément, puis retourne nothing. Par exemple :

1. julia> t = [1, 2, 3, 4];

3. julia> interior!(t)

4. julia> print(t)

5. [2, 3]

10-15-5 - Exercice 10-5

Écrivez une fonction appelée issort qui prend un tableau comme paramètre et retourne true si le tableau est trié par ordre croissant et false dans le cas contraire. Par exemple :

1. julia> issort([1, 2, 2])

2. true

3. julia> issort(['b', 'a'])

4. false

10-15-6 - Exercice 10-6

Deux mots sont des anagrammes si vous pouvez réarranger les lettres de l'un pour écrire l'autre. Écrivez une fonction appelée isanagram qui prend deux chaînes de caractères et qui retourne true si ces mots sont des anagrammes.

10-15-7 - Exercice 10-7

Écrivez une fonction appelée hasduplicates qui prend un tableau et retourne true s'il y a un élément qui apparaît plus d'une fois. Elle ne doit pas modifier le tableau original.

10-15-8 - Exercice 10-8

Cet exercice se rapporte au « paradoxe des anniversaires » (voir ce lien web).

Dans une classe de 23 élèves, quelles sont les chances que deux d'entre eux fêtent leur anniversaire le même jour ? Vous pouvez estimer cette probabilité en générant des échantillons aléatoires de 23 anniversaires et en vérifiant s'il y a des correspondances.

Vous pouvez générer des dates aléatoires avec rand(1:365).

10-15-9 - Exercice 10-9

Écrivez une fonction qui lit le fichier mots_FR.txt (voir la section 9.1) et construit un tableau avec un élément par mot. Écrivez deux versions de cette fonction, l'une utilisant push! et l'autre utilisant l'expression t = [t..., x]. Laquelle prend le plus de temps à exécuter ? Pourquoi ?

10-15-10 - Exercice 10-10

Pour vérifier si un mot se trouve dans le tableau de mots, vous pourriez utiliser l'opérateur ∈, mais ce serait lent, car ce dernier recherche les mots dans l'ordre.

Comme les mots sont dans l'ordre alphabétique, nous pouvons accélérer la recherche grâce à la bissection (également appelée recherche binaire, voir la section 7.8), qui est similaire à ce que vous faites lorsque vous cherchez un mot dans le dictionnaire. Vous commencez au milieu et vous vérifiez si le mot que vous recherchez vient avant le mot du milieu du tableau. Si c'est le cas, vous recherchez dans la première moitié du tableau de la même manière. Sinon, vous cherchez dans la deuxième moitié.

Dans les deux cas, vous réduisez de moitié l'espace de recherche restant. Si le tableau contient 336 531 mots, il faudra environ 19 étapes pour trouver le mot ou conclure qu'il n'y est pas.

Écrivez une fonction appelée inbisect qui prend un tableau trié et une valeur cible et retourne true si le mot est dans le tableau et false s'il n'y est pas.

10-15-11 - Exercice 10-11

Deux mots sont une « paire inversée » si chacun est l'inverse de l'autre. Écrivez un programme reversepairs qui trouve toutes les paires inversées dans le tableau de mots.

10-15-12 - Exercice 10-12

Deux mots « s'emboîtent » si on prend des lettres alternées de chacun d'eux pour former un nouveau mot. Par exemple, « cor » et « ria » s'emboîtent pour former « croira ».

1. Écrivez un programme qui trouve toutes les paires de mots qui s'emboîtent.
Remarque. Cet exercice est inspiré d'un exemple décrit sur ce lien.

N'énumérez pas toutes les paires.

2. Pouvez-vous trouver des mots qui s'emboîtent les uns dans les autres, c'est-à-dire qu'une lettre sur trois forme un mot, en commençant par la première, la deuxième ou la troisième ?

11 - Dictionnaires

Ce chapitre présente un autre type intégré, les dictionnaires.

11-1 - Dictionnaire et « mise en correspondance » (mapping)

Un dictionnaire constitue la généralisation d'un tableau. Dans un tableau, les indices doivent être des nombres entiers. Dans un dictionnaire, ils peuvent être de (presque) n'importe quel type.

Un dictionnaire contient une collection d'indices, appelés clés, et une collection de valeurs. Chaque clé est associée à une valeur unique. L'association d'une clé et d'une valeur est appelée une paire clé-valeur ou parfois un élément.

En langage informatique, un dictionnaire représente une mise en correspondance (ou mapping) ou association entre des clés et des valeurs, de sorte qu'on peut également dire que chaque clé « correspond » à une valeur (voir la figure 11.1.1). À titre d'exemple, nous allons construire un dictionnaire qui établit une correspondance entre des mots anglais et français, de sorte que les clés et les valeurs sont toutes des chaînes de caractères.

La fonction Dict crée un nouveau dictionnaire ne contenant aucun élément. Comme Dict est le nom d'une fonction intégrée, il faut éviter de l'utiliser comme nom de variable.

1. julia> eng2fr = Dict()

2. Dict{Any,Any} with 0 entries

Le type de dictionnaire est entouré d'accolades : les clés sont de type Any et les valeurs également.

Le dictionnaire est vide. Pour y ajouter des éléments, utilisons des crochets :

1. julia> eng2fr["one"] = "un";

Cette ligne crée un élément qui relie la clé "one" à la valeur "un". À nouveau affiché, le dictionnaire montre une paire clé-valeur. La clé est connectée à la valeur par une flèche => :

1. julia> eng2fr

2. Dict{Any,Any} with 1 entry:

3. "one" => "un"

Ce format de sortie est également un format d'entrée. Par exemple, nous pouvons créer un nouveau dictionnaire comportant trois éléments :

1. julia> eng2fr = Dict("one" => "un", "two" => "deux", "three" => "trois")

2. Dict{String,String} with 3 entries:

3. "one" => "un"

4. "two" => "deux"

5. "three" => "trois"

La figure 11.1.1 résume les attributs du dictionnaire eng2fr.

FIGURE 11.1.1 – Représentation du dictionnaire eng2fr.

Toutes les clés et valeurs initiales sont des chaînes de caractères, de sorte qu'un Dict{String,String} est créé.

L'ordre des paires « clé-valeur » n'est peut-être pas le même si vous essayez ces instructions sur votre ordinateur. En général, l'ordre des éléments (c'est-à-dire des couples « clé-valeur ») dans un dictionnaire est imprévisible.

Néanmoins, cela n'a guère d'importance du fait que les éléments d'un dictionnaire ne sont jamais indicés avec des entiers. Ce sont les clés qui sont utilisées pour rechercher les valeurs correspondantes :

1. julia> eng2fr["two"]

2. "deux"

La clé "two" correspond toujours à la valeur "deux", l'ordre des éléments n'a donc pas d'importance.

Si la clé n'est pas dans le dictionnaire, Julia retourne une exception :

1. julia> eng2fr["four"]

2. ERROR: KeyError: key "four" not found

La fonction length opère également sur les dictionnaires. Elle retourne le nombre de paires clé-valeur :

1. julia> length(eng2fr)

2. 3

La fonction keys retourne la collection de clés du dictionnaire :

1. julia> ks = keys(eng2fr);

3. julia> print(ks)

4. ["two", "one", "three"]

À présent, l'opérateur ∈ est utilisable pour déterminer si un terme est une clé du dictionnaire :

1. julia> "one" ∈ ks

2. true

3. julia> "un" ∈ ks

4. false

Pour déterminer si une valeur appartient à un dictionnaire, la fonction values est exploitable. Elle retourne l'ensemble des valeurs, si bien qu'ensuite il est alors possible de tirer parti de l'opérateur ∈ :

1. julia> ks = values(eng2fr);

3. julia> "un" ∈ ks

4. true

L'opérateur ∈ utilise des algorithmes différents selon que sont traités des tableaux ou des dictionnaires. Pour les tableaux, il recherche les éléments dans l'ordre, comme dans la section 8.8. Le temps de recherche s'allonge proportionnellement à la taille des tableaux.

Pour les dictionnaires, Julia utilise un algorithme appelé table de hachage (ou hash table) qui a une propriété remarquable : l'opérateur ∈ prend à peu près le même temps quel que soit le nombre d'éléments du dictionnaire.

11-2 - Dictionnaires en tant que collections de compteurs

Soit une chaîne dont il faut compter l'occurrence(22) de chaque lettre. A priori, il y a trois possibilités :

1. Créer 26 variables, une pour chaque lettre de l'alphabet, parcourir la chaîne et pour chaque caractère, incrémenter le compteur correspondant, probablement en utilisant des conditions imbriquées ;

2. Créer un tableau de 26 éléments. Ensuite, convertir chaque caractère en un nombre (en utilisant la fonction interne Int), utiliser les nombres comme indices dans le tableau et incrémenter le compteur correspondant ;

3. Créer un dictionnaire où les clés sont des caractères et où les valeurs correspondantes sont des compteurs. La première fois qu'un caractère est rencontré, le programme ajoute un élément au dictionnaire. Ensuite, la valeur d'un élément existant est incrémentée. Par exemple, pour le terme « abracadabra », la valeur de la clé « a » est 5, la valeur de la clé « b » est 2, la valeur de la clé « r » est 2, la valeur de la clé « c » est 1 et la valeur de la clé « d » est 1.

Chacune de ces options effectue le même calcul, mais avec une mise en œuvre différente.

Une implémentation est définie comme la mise en œuvre d'un calcul. Certaines s'avèrent plus performantes que d'autres. Par exemple, un des avantages de l'implémentation par dictionnaire provient du fait qu'il n'est pas nécessaire de connaître préalablement les lettres constituant la chaîne. Nous n'avons qu'une obligation : faire de la place pour insérer dans le dictionnaire les lettres détectées.

Voici à quoi pourrait ressembler le code :

1. function histogram(s)

2. d = Dict()

3. for c in s

4. if c ∉ keys(d)

5. d[c] = 1

6. else

7. d[c] += 1

8. end

9. end

10. d

11. end

Le nom de la fonction est histogram, un terme associé au domaine des statistiques pour désigner une série de fréquences (c'est-à-dire d'occurrences). Comment fonctionne ce code ? Pour être concret, effectuons un appel sous la forme histogram("brontosaure") :

1. julia> h = histogram("brontosaure")

On passe donc la chaîne "brontosaure" à s. Ensuite, un dictionnaire vide d est créé.

Dans un premier temps, supposons que la boucle for ne contienne pas le code du test conditionnel if et qu'à sa place il n'y ait qu'une instruction println(c). Que retournerait le programme ? Il afficherait chaque lettre du mot « brontosaure » avec, chaque fois, un retour à la ligne. En effet, au premier passage, la variable c vaut 'b', au deuxième passage, elle vaut 'r' et ainsi de suite jusqu'à valoir 'e'. La boucle for parcourt donc entièrement la chaîne de caractères.

Dans un deuxième temps, on peut s'assurer de ce que contient keys(d). Pour ce faire, il suffit de remplacer l'instruction println(c) par @show keys(d). Nous devons nous attendre à 11 retours keys(d) = Any{}.

Dans un troisième temps, nous allons remplacer le test conditionnel if par ce bloc :

1. if c ∉ keys(d)

2. d[c] = 1

3. print(d[c])

4. else

5. print("ω")

6. end

De sorte que la fonction prenne transitoirement cette forme :

1. function histogram(s)

2. d = Dict()

3. for c in s

4. if c ∉ keys(d)

5. d[c] = 1

6. println(d[c])

7. else

8. println("ω")

9. end

10. end

11. end

Que retourne la fonction histogram("brontosaure") ? Au premier passage dans la boucle for, la variable c contient 'b'. Le code teste si 'b' se trouve parmi les clés du dictionnaire. Le dictionnaire étant vide, 'b' ne s'y trouve pas. À la ligne suivante, d['b'] effectue l'entrée de 'b' comme clé du dictionnaire et l'entier 1 est affecté à la valeur d['b'] correspondante. Le code remonte à la boucle for avec c qui, cette fois, vaut 'r' et ainsi de suite jusqu'à la cinquième lettre, 't'. La première partie de l'affichage sera donc 11111. Au passage suivant, c vaut 'o'. Cependant, il existe déjà un 'o' dans le dictionnaire et, par conséquent, le code passe au else et l'affichage devient 11111ω. Il est maintenant clair qu'à la fin nous devons obtenir un affichage tel que 11111ω111ω1.

Revenons à la fonction histogram initiale. Comme précédemment, au premier passage dans la boucle for, c vaut 'b'. Le code teste si 'b' se trouve parmi les clés du dictionnaire. Puisque ce dernier est vide, 'b' ne s'y trouve pas ; d['b'] effectue l'entrée de 'b' comme clé du dictionnaire et l'entier 1 est affecté à la valeur d['b'].

En conséquence, l'état du dictionnaire est :

1. keys(d) = Any['b']

2. d = Dict{Any,Any}('b'=> 1)

Au deuxième tour, l'état du dictionnaire passe à :

1. keys(d) = Any['r', 'b']

2. d = Dict{Any,Any}('r'=> 1,'b'=> 1)

Au cinquième tour, le dictionnaire prend cette configuration (à l'ordre des clés près) :

1. keys(d) = Any['t', 'n', 'o', 'r', 'b']

2. d = Dict{Any,Any}('t'=> 1,'n'=> 1,'o'=> 1,'r'=> 1,'b'=> 1)

Au passage suivant, le programme entre dans la partie else puisque 'o' se trouve déjà dans le dictionnaire. Sa valeur correspondante est incrémentée et devient 2. L'état du dictionnaire devient :

1. keys(d) = Any['t', 'n', 'o', 'r', 'b']

2. d = Dict{Any,Any}('t'=> 1,'n'=> 1,'o'=> 2,'r'=> 1,'b'=> 1)

In fine, l'état du dictionnaire est :

1. keys(d) = Any['e','r','u','a','s','o','t', 'n', 'o', 'r', 'b']

2. d = Dict{Any,Any}('e'=> 1,'u'=> 1,'a'=> 1,'s'=> 1,'t'=> 1,'n'=> 1,'o'=> 2,'r'=> 2,'b'=> 1)

Si vous appliquez l'appel histogram("brontosaure"), le programme retourne la valeur de d sous cette forme :

1. Dict{Any,Any} with 9 entries

2. 'n'   =>   1

3. 's'   =>   1

4. 'a'   =>   1

5. 'r'   =>   2

6. 't'   =>   1

7. 'o'   =>   2

8. 'u'   =>   1

9. 'e'   =>   1

10. 'b'   =>   1

L'histogramme indique que les lettres a, b, e, n, s, t et u apparaissent une fois ; o et r apparaissent deux fois.

Les dictionnaires possèdent une fonction appelée get qui prend une clé et une valeur par défaut. Si la clé apparaît dans le dictionnaire, get retourne la valeur correspondante. Sinon, elle retourne la valeur par défaut. Par exemple :

1. julia> h = histogram("brontosaure");

3. julia> get(h, 'a', 0)

4. 1

5. julia> get(h, 'o', 0)

6. 2

7. julia> get(h, 'z', 0)

8. 0

11-2-1 - Exercice 11-1

Utilisez get pour réécrire histogram de manière plus concise. Vous devriez pouvoir éliminer la déclaration if.

11-3 - Boucles et dictionnaires

Les clés d'un dictionnaire peuvent être parcourues grâce à une boucle for. Par exemple, printhist affiche chaque clé et la valeur correspondante :

1. function printhist(h)

2. for c in keys(h)

3. println(c, " ", h[c])

4. end

5. end

Voici à quoi ressemble le résultat :

1. julia> histogram("perroquet");

3. julia> printhist(h)

4. u  1

5. e  2

6. p  1

7. r  2

8. o  1

9. q  1

10. t  1

Là encore, les clés ne sont pas dans un ordre particulier. Pour parcourir les clés dans l'ordre dans l'ordre alphabétique, la combinaison des fonctions sort et collect se révèle pratique :

1. julia> for c in sort(collect(keys(h)))

2. println(c, " ", h[c])

3. end

4. e  2

5. o  1

6. p  1

7. q  1

8. r  2

9. u  1

10. t  1

11-4 - Recherche inverse

Avec un dictionnaire d et une clé k, il est facile de trouver la valeur correspondante v = d[k]. Cette opération est appelée une recherche directe (ou lookup).

Ceci dit, comment procéder si nous disposons d'une valeur v et que nous souhaitons trouver sa clé k ? Ici, nous sommes confrontés à deux problèmes :

1. Premièrement, il peut y avoir plus d'une clé qui correspond à la valeur v (par exemple, dans le cas du tableau d associé à « brontosaure », à la valeur « 2 » correspondaient les clés 'o' et 'r') ;

2. Deuxièmement, il n'y a pas d'astuce simple pour effectuer une recherche inverse (ou reverse lookup).

Voici une fonction qui prend une valeur et retourne la première clé qui correspond à cette valeur :

1. function reverselookup(d, v)

2. for k in keys(d)

3. if d[k] == v

4. return k

5. end

6. end

7. error("LookupError")

8. end

Cette fonction est un exemple de schéma de recherche inverse, mais elle utilise une fonction error non encore abordée jusqu'ici. La fonction error est exploitée pour produire une ErrorException qui interrompt le flux de contrôle normal. Dans ce cas, elle affiche le message "LookupError", indiquant qu'une clé n'existe pas.

Si le programme arrive à la fin de la boucle, cela signifie que v n'est pas une valeur du dictionnaire. En conséquence, le programme émet une exception.

Voici un exemple de recherche inversée réussie :

1. julia> h = histogram("magnifique");

3. julia> key = reverselookup(h, 2)

4. 'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)

et voici un cas qui a échoué :

1. julia> key = reverselookup(h, 3)

2. ERROR: LookupError

Quand une exception se produit, l'effet est le même que lorsque Julia en émet une : une trace de pile (ou stacktrace) et un message d'erreur sont affichés.

Julia propose un moyen optimisé de faire une recherche inversée : findall(isequal(3), h).

Une recherche inverse est beaucoup plus lente qu'une recherche classique. S'il est nécessaire d'y recourir souvent, ou si le dictionnaire est de grande taille, la performance du programme en souffrira.

11-5 - Dictionnaires et tableaux

Les tableaux peuvent constituer des valeurs au sein d'un dictionnaire. Supposons un dictionnaire qui fait correspondre des lettres à des occurrences (voir section 11.2). On peut vouloir l'inverser, c'est-à-dire créer un dictionnaire qui fait correspondre des occurrences (en tant que clés) à des lettres (en tant que valeurs). Comme plusieurs lettres peuvent présenter la même occurrence, celles-ci peuvent être groupées, selon leur occurrence, sous forme de tableaux et ces tableaux peuvent devenir des valeurs dans le dictionnaire inversé.

La figure 11.5.1 permet de visualiser l'inversion d'un tableau (en l'espèce, l'histogramme du mot « perroquet »). Les zones grisées de la figure correspondent à deux tableaux constituant les deux valeurs du dictionnaire inversé.

FIGURE 11.5.1 – Inversion de dictionnaire où les valeurs du dictionnaire inversé sont des tableaux.

Voici une fonction permettant d'inverser un dictionnaire :

1. function invertdict(d)

2. inverse = Dict()

3. for key in keys(d)

4. val = d[key]

5. if val ∉ keys(inverse)

6. inverse[val] = [key]

7. else

8. push!(inverse[val], key)

9. end

10. end

11. inverse

12. end

À chaque fois, dans la boucle, key récupère une clé à partir de d pendant que val récupère la valeur correspondante. Si val n'est pas dans inverse, cela signifie qu'elle n'a pas été détectée auparavant. En conséquence, un nouvel élément est créé et initialisé avec un singleton (un tableau qui contient un seul élément). Si cette valeur a été repérée auparavant, la clé qui lui correspond est ajoutée au tableau à l'aide de la fonction push!.

Voici un exemple :

1. julia> hist = histogram("perroquet");

3. julia> inverse = invertdict(hist)

4. Dict{Any,Any} with 2 entries:

5. 2 => ['e', 'r']

6. 1 => ['u', 'p', 'o', 'q', 't']

La figure 11.5.2 reprend les diagrammes d'état associés à hist et inverse. Un dictionnaire est représenté sous la forme d'un cadre contenant des paires clé-valeur (que ces valeurs soient des entiers, des flottants ou des chaînes de caractères). Pour conserver la simplicité de lecture des diagrammes, les tableaux sont disposés dans des cadres à l'extérieur de ceux associés au dictionnaire.

FIGURE 11.5.2 – Diagrammes d'état pour les fonctions hist et inverse.

Dans la section 11.1, il a été mentionné qu'un dictionnaire est implémenté à l'aide d'une table de hachage et cela signifie que les clés doivent être hachables. Le hachage est une fonction qui prend une valeur (de n'importe quel type) et retourne un entier. Les dictionnaires utilisent ces entiers, appelés valeurs de hachage, pour enregistrer et rechercher des paires clé-valeur.

11-6 - Mémos

Si vous avez quelque peu manié la fonction fibonacci de la section 6.7, vous avez remarqué que plus le nombre passé en argument est grand, plus la fonction prend de temps à effectuer le calcul. De surcroît, la durée d'exécution augmente de plus en plus rapidement.

Pour comprendre pourquoi, considérons la figure 11.6.1 qui montre le graphe d'appel pour fibonacci avec n = 4 :

FIGURE 11.6.1 – Graphe d'appel associé à la fonction fibonacci.

Un graphe d'appel comporte un ensemble de cadres associés à une (ou des) fonction(s), avec des flèches reliant chaque cadre aux cadres des fonctions qu'il appelle. En haut du graphique, fibonacci avec n = 4 appelle fibonacci avec n = 3 et n = 2, et fibonacci avec n = 3 appelle fibonacci avec n = 2 et n = 1. Et ainsi de suite.

Il est facile de dénombrer les appels à fibonacci(0) et fibonacci(1) et de conclure à l'inefficacité de cette solution.

Un moyen de résoudre ce problème consiste à garder une trace des valeurs déjà calculées en les enregistrant dans un dictionnaire. Une valeur calculée précédemment et stockée pour une utilisation ultérieure s'appelle un mémo. Voici une version « mémo » de fibonacci :

1. known = Dict(0=>0, 1=>1)

3. function fibonacci(n)

4. if n ∈ keys(known)

5. return known[n]

6. end

7. res = fibonacci(n-1) + fibonacci(n-2)

8. known[n] = res

9. res

10. end

known est un dictionnaire qui garde la trace des nombres de la suite de Fibonacci déjà calculés. Il commence par deux éléments : la clé 0 est associée à la valeur 0 et la clé 1 à la valeur 1.

Chaque fois que la fonction fibonacci est appelée, elle vérifie known. Si le résultat est déjà dans le dictionnaire, il est rappelé immédiatement. Sinon, fibonacci calcule la nouvelle valeur, l'ajoute au dictionnaire et la retourne.

À l'exécution, cette version de fibonacci s'avère beaucoup plus rapide que l'originale.

11-7 - Variables globales

Dans l'exemple précédent, known est créé en dehors de la fonction. Ce dictionnaire appartient donc à la fonction Main. Les variables dans Main sont dites globales, car elles sont accessibles depuis n'importe quelle fonction. Contrairement aux variables locales qui disparaissent lorsque leur fonction se termine, les variables globales persistent d'un appel de fonction à l'autre.

Il est courant d'utiliser des variables globales pour les drapeaux (flags), c'est-à-dire des variables booléennes qui indiquent si une condition est vraie (ou fausse). Par exemple, certains programmes utilisent un drapeau verbose pour contrôler le niveau de détail de la sortie :

1. verbose = true

3. function example1()

4. if verbose

5. println("Running example1")

6. end

7. end

La réaffectation d'une variable globale n'entraîne pas la modification de cette dernière. L'exemple suivant est censé déterminer si la fonction (example2) a été appelée :

1. been_called = false

3. function example2()

4. been_called = true      # ERRONÉ

5. end

À l'exécution, nous constatons que la valeur de been_called ne change pas. Le problème vient de ce qu'example2 crée une nouvelle variable locale appelée been_called. La variable locale disparaît lorsque la fonction se termine. Par conséquent, cela n'affecte pas la variable globale.

Pour conférer le caractère global à une variable se trouvant à l'intérieur d'une fonction, il est nécessaire de placer le terme global immédiatement devant le nom de la variable :

1. been_called = false

3. function example2()

4. global been_called

5. been_called = true

6. end

Cette déclaration globale transmet à l'interpréteur une information signifiant : « Dans cette fonction, quand nous disons been_called, nous désignons la variable globale ; ne créez pas de variable locale ». Ainsi, quand, à l'intérieur de la fonction, true est affecté à been_called, cela affecte la variable globale.

Voici un exemple qui tente erronément de mettre à jour une variable globale :

1. count = 0

3. function example3()

4. count = count + 1     # ERRONÉ

5. end

À l'exécution, Julia retourne un message d'erreur :

1. julia> example3()

2. ERROR: UndefVarError: count not defined

Julia suppose que count est locale et que vous en prenez connaissance avant de modifier sa valeur. À nouveau, la solution est de déclarer count comme variable globale.

1. count = 0

3. function example3()

4. global count

5. count += 1

6. end

Si une variable globale fait référence à une séquence non persistante (comme un tableau ou un dictionnaire), il est possible de modifier les valeurs de cette séquence sans déclarer la variable comme étant globale :

1. known = Dict(0=>0, 1=>1)

3. function example4()

4. known[2] = 1

5. end

Par conséquent, il est admissible d'ajouter, supprimer et remplacer des éléments d'un tableau ou d'un dictionnaire global. Cependant, la réaffectation de la variable au sein d'une fonction requiert une déclaration en tant que variable globale :

1. known = Dict(0=>0, 1=>1)

3. function example5()

4. global known

5. known = Dict()

6. end

Pour des raisons de performance, il est judicieux d'attribuer le mot-clé const à une variable globale. De cette manière, il devient impossible de la réaffecter. Si une variable globale const se réfère à une séquence non persistante, il reste néanmoins possible d'en modifier la valeur.

1. const known = Dict(0=>0, 1=>1)

3. function example4()

4. known[2] = 1

5. end

Les variables globales peuvent s'avérer très utiles. Néanmoins, si un programme en contient un grand nombre et qu'elles sont modifiées fréquemment, elles peuvent rendre le programme difficile à déboguer et le conduire à mal fonctionner.

11-8 - Débogage

Lorsqu'on travaille avec des jeux de données volumineux, il peut devenir difficile de déboguer en affichant et en vérifiant le résultat manuellement. Voici quelques suggestions pour le débogage de grands jeux de données :

- réduire la taille de l'entrée :
si possible, réduire la taille du jeu de données. Par exemple, si un programme lit un fichier texte, on peut commencer par les 10 premières lignes, voire — si c'est possible avec le plus petit échantillon sur lequel des erreurs sont identifiables. Il est fortement conseillé de ne pas éditer les fichiers eux-mêmes, mais plutôt de modifier le programme afin qu'il ne lise que les n premières lignes.
S'il y a une erreur, il convient de réduire n à la plus petite valeur qui révèle l'erreur. Par la suite, la valeur de n peut être augmentée progressivement à mesure que sont détectées et corrigées les erreurs ;

- vérifiez les résumés et les types :
au lieu d'afficher et de vérifier l'ensemble des données, il faut penser à afficher les résumés de données : par exemple, le nombre d'éléments dans un dictionnaire ou le total d'un tableau de nombres.
Une cause fréquente d'erreurs d'exécution tient en une valeur dont type est erroné. Pour déboguer ce type d'erreur, il suffit souvent d'afficher le type d'une valeur ;

- rédigez des vérifications automatiques :
parfois, il est habile d'écrire un peu de code pour vérifier automatiquement la présence d'erreurs. Par exemple, si la moyenne d'un tableau de nombres est calculée, on peut aisément vérifier que le résultat n'est pas supérieur au plus grand élément du tableau ou inférieur au plus petit. Il s'agit là d'une « vérification de bon sens ».
Un autre type de contrôle consiste à comparer les résultats de deux calculs différents pour vérifier qu'ils sont cohérents. Ici, il est question d'une « vérification de cohérence » ;

- formater les messages émis :
le formatage des messages de débogage peut aider à repérer une erreur. Nous en avons vu un exemple dans la section 6.9. Là encore, le temps investi à construire un canevas peut considérablement réduire le temps de débogage.

11-9 - Glossaire

mapping (association ou mise en correspondance) relation dans laquelle chaque élément d'un ensemble correspond à un élément d'un autre ensemble.

dictionnaire séquence permettant une mise en correspondance de clés avec leur valeur.

paire clé-valeur représentation de la mise en correspondance (mapping) d'une clé à une valeur.

item dans un dictionnaire, synonyme d'une paire clé-valeur.

clé objet qui apparaît dans un dictionnaire comme la première partie d'une paire clé-valeur.

valeur objet qui apparaît dans un dictionnaire comme la deuxième partie d'une paire clé-valeur. Ce terme est plus spécifique que notre utilisation précédente du terme « valeur ».

implémentation mise en œuvre d'un calcul.

table de hachage (hash table) algorithme utilisé pour mettre en œuvre les dictionnaires en Julia.

fonction de hachage (hash function) fonction utilisée par une table de hachage pour calculer l'emplacement d'une clé.

recherche directe (lookup) opération du dictionnaire qui prend une clé et trouve la valeur correspondante.

recherche inversée (reverse lookup) opération sur un dictionnaire qui prend une valeur et trouve une ou plusieurs clés qui y correspondent.

singleton tableau (ou toute autre séquence) avec un seul élément.

graphe d'appel diagramme qui montre chaque cadre créé pendant l'exécution d'un programme, avec une flèche de chaque appelant à chaque appelé.

mémo valeur calculée et enregistrée pour éviter tout calcul futur redondant.

variable globale variable définie dans Main, en dehors de toute autre fonction. Les variables globales sont accessibles à partir de n'importe quelle fonction.

déclaration globale déclaration qui établit le nom d'une variable globale.

drapeau (flag ou fanion) variable booléenne utilisée pour indiquer si une condition est vraie.

déclaration énoncé tel que le mot global qui renseigne l'interpréteur sur une variable.

constante globale constante définie dans Main et ne pouvant être réaffectée.

11-10 - Exercices

11-10-1 - Exercice 11-2

Écrivez une fonction qui lit les mots dans la liste mots_FR.txt et les enregistre sous forme de clés dans un dictionnaire. Les valeurs n'ont pas d'importance. Vous pouvez alors utiliser l'opérateur ∈ comme moyen rapide de vérifier si une chaîne se trouve dans le dictionnaire.

Si vous avez résolu l'exercice 10.15.10, vous pouvez comparer la rapidité de cette implémentation avec l'opérateur ∈ par rapport à la recherche par bissection.

11-10-2 - Exercice 11-3

Lisez la documentation de la fonction get! agissant sur les dictionnaires et utilisez-la pour rédiger une version plus concise d'invertdict.

11-10-3 - Exercice 11-4

Appliquez la technique « mémo » à la fonction d'Ackermann (exercice 6.11.2) et notez si cette technique permet d'évaluer la fonction avec des nombres en argument plus grands que dans sa forme initiale.

11-10-4 - Exercice 11-5

Si vous avez résolu l'exercice 10.15.7, vous avez déjà une fonction appelée hasduplicates qui prend un tableau comme paramètre et retourne true si un objet apparaît plus d'une fois dans le tableau.

Utilisez un dictionnaire pour écrire une version plus rapide et plus simple de hasduplicates.

11-10-5 - Exercice 11-6

Deux mots sont des « paires de rotation » si vous pouvez faire tourner l'un d'eux et obtenir l'autre (voir l'exercice 8.15.5).

Écrivez un programme qui lit un tableau de mots et trouve toutes les paires de rotation.

11-10-6 - Exercice 11-7

Voici un autre casse-tête de Car Talk.

« Cette lettre a été envoyée par un certain Dan O'Leary. Il a récemment rencontré un mot commun d'une syllabe et de cinq lettres qui a la propriété unique suivante. Lorsque vous retirez la première lettre, les autres lettres forment un homophone(23). Remplacez la première lettre, c'est-à-dire remettez-la à sa place et supprimez la deuxième lettre et le résultat est encore un autre homophone du mot original. D'où la question : quel est le mot ?

Je vais maintenant vous donner un exemple qui ne fonctionne pas. Examinons le mot de cinq lettres « wrack ». W-R-A-C-K, comme dans « to wrack with pain ». Si j'enlève la première lettre, il me reste un mot de quatre lettres, « R-A-C-K ». Comme dans « Holy cow, did you see the rack on that buck! It must have been a nine-pointer! » C'est un homophone parfait. Si vous remettez le « w » et que vous enlevez le « r », vous obtenez le mot « wack », qui est un vrai mot, mais pas l'homophone des deux autres termes.

Mais il y a au moins un mot que Dan et nous connaissons, qui donnera deux homophones si vous enlevez l'une des deux premières lettres pour faire deux nouveaux mots de quatre lettres. La question est de savoir quel est ce mot. »

Vous pouvez utiliser le dictionnaire de l'exercice 11.10.1 pour vérifier si une chaîne se trouve dans le tableau de mots.

Pour vérifier si deux mots sont homophones, vous pouvez utiliser le Dictionnaire de prononciation anglaise de la CMU. Vous pouvez le télécharger sur le site The CMU Pronouncing Dictionary.

Écrivez un programme qui pourrait afficher tous les termes résolvant l'énigme.

12 - Tuples

Ce chapitre présente un autre type intégré, les tuples(24). Par la suite, il montre comment les tableaux, les dictionnaires et les tuples peuvent être couplés. Nous présentons également une fonction utile pour les tableaux à arguments multiples ainsi que pour les opérateurs d'agrégation et de dispersion.

Il n'y a pas de consensus sur prononciation de « tuple ». Vous pouvez écouter comment prononcer ce mot en anglais sur ce site. Il semble qu'il n'y ait pas de prononciation française recommandée.

12-1 - Les tuples sont persistants

Un tuple est une séquence de valeurs. Celles-ci peuvent être de type quelconque et elles sont indexées par des entiers, de sorte que, à cet égard, les tuples ressemblent beaucoup aux tableaux. La différence notoire est que les tuples sont persistants (immutable, en anglais). Chaque élément peut avoir son propre type (nombres, chaînes de caractères, tableaux, dictionnaires, tuples).

Syntaxiquement, un tuple est une liste de valeurs séparées par des virgules :

1. julia> t = 'a', 'b', 'c', 'd', 'e'

2. ('a', 'b', 'c', 'd', 'e')

Sans que cela soit nécessaire, il est courant de mettre les tuples entre parenthèses :

1. julia> t = ('a', 'b', 'c', 'd', 'e')

2. ('a', 'b', 'c', 'd', 'e')

Pour créer un tuple avec un seul élément, il est nécessaire de ponctuer avec une virgule :

1. julia> t = ('a',)

2. ('a',)

3. julia> typeof(t)

4. Tuple{Char}

Une valeur entre parenthèses sans virgule finale n'est pas un tuple :

1. julia> t2 = ('a')

2. 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

3. julia> typeof(t2)

4. Char

Une autre manière de créer un tuple consiste à employer la fonction interne tuple. Sans aucun argument, elle crée un tuple vide :

1. julia> tuple()

2. ()

Si plusieurs arguments sont fournis, le résultat est un tuple incorporant tous les arguments donnés :

1. julia> t3 = tuple(1, 'a', pi)

2. (1, 'a', 𝜋 = 3.1415926535897...)

Comme tuple est le nom d'une fonction interne, il faut éviter de l'utiliser comme nom de variable.

La plupart des opérateurs agissant sur les tableaux fonctionnent également sur des tuples. L'opérateur crochet [] indice tout élément d'un tuple :

1. julia> t[2:4]

2. ('b', 'c', 'd')

Cependant, comme les tuples sont persistants, vouloir en modifier un élément conduit à une erreur :

1. julia> t[1] = ('A')

2. ERROR: MethodError: no method matching setindex!(::NTuple{5,Char}, ::Char, ::Int64)

Les opérateurs relationnels agissent sur les tuples comme avec d'autres séquences. Julia commence par comparer le premier élément de chaque séquence. S'ils sont égaux, Julia passe aux éléments suivants et ainsi de suite jusqu'à trouver des éléments qui diffèrent. Les éléments ultérieurs ne sont pas pris en compte (même s'il s'agit, par exemple, de grands nombres).

1. julia> (0, 1, 2) < (0, 3, 4)

2. true

3. julia> (0, 1, 2000000) < (0, 3, 4)

4. true

12-2 - Affectation des tuples

Il est souvent utile d'échanger les valeurs de deux variables. Dans le cas des affectations classiques, il est nécessaire utiliser une variable temporaire. Par exemple, pour permuter a et b :

1. temp = a

2. a = b

3. b = temp

Cette solution est remplacée par l'affectation de tuples qui s'avère plus élégante :

1. a, b = b, a

Le membre de gauche de l'affectation est un tuple de variables, alors que le membre de droite est un tuple d'expressions. Chaque valeur est attribuée à sa variable respective. Toutes les expressions du membre de droite sont évaluées avant toute affectation.

Par ailleurs, le nombre de variables du membre de gauche doit être inférieur au nombre de valeurs du membre de droite :

1. julia> (a, b) = (1, 2, 3)

2. (1, 2, 3)

3. julia> a, b, c = 1, 2

4. ERROR: BoundsError: attempt to access (1, 2) at index [3]

Plus généralement, le membre de droite peut être tout type de séquence (chaîne, tableau ou tuple). Par exemple, pour séparer une adresse électronique en un nom d'utilisateur et un domaine, vous pouvez écrire(25) :

1. julia> addr = "jules.cesar@rome.spqr"

2. jules.cesar@rome

3. julia> uname, domain = split(addr, '@')

4. 2-element Array{SubString{String},1}:

5. "jules.cesar"

6. "rome.spqr"

La valeur de retour de split est un tableau à deux éléments : le premier élément est attribué à uname, le second à domain. Si le séparateur avait été '.', Julia aurait affiché trois chaînes.

12-3 - Les tuples en tant que valeurs retournées

À vrai dire, une fonction ne peut retourner qu'une seule valeur. Toutefois, si la valeur est un tuple, l'effet est le même que si elle retournait plusieurs valeurs. Par exemple, soit deux entiers dont on veut obtenir par division le quotient et le reste. Comme il est inefficace de calculer x ÷ y puis x % y, il est préférable de réaliser ces opérations simultanément.

La fonction interne divrem qui prend deux arguments retourne un tuple de deux valeurs, le quotient et le reste. Le résultat est enregistrable sous forme de tuple :

1. julia> t = divrem(7, 3)

2. (2, 1)

Il est aussi possible d'utiliser l'affectation des tuples pour enregistrer les éléments séparément :

1. julia> q, r = divrem(7, 3);

3. julia> @show q r;

4. q = 2

5. r = 1

Voici un exemple de fonction retournant un tuple :

1. function minmax(t)

2. minimum(t), maximum(t)

3. end

maximum et minimum sont des fonctions internes qui identifient les éléments les plus petits et les plus grands d'une séquence. minmax calcule les deux et retourne un tuple de deux valeurs. La fonction interne extrema est plus efficace.

12-4 - Tuples et arguments multiples

Les fonctions peuvent prendre un nombre variable d'arguments. Un nom de paramètre qui se termine par trois points consécutifs (...) agrège les arguments dans un tuple. Par exemple, printall prend un nombre quelconque d'arguments et les affiche :

1. function printall(args...)

2. println(args)

3. end

Le nom du paramètre d'agrégation est libre, quoique le terme args soit conventionnel. Voici comment procède la fonction :

1. julia> printall(1, 2.0, '3')

2. (1, 2.0, '3')

À l'agrégation s'oppose la dispersion(26). Soit une séquence de valeurs qu'on souhaite passer à une fonction sous forme d'arguments multiples, il est alors possible d'utiliser l'opérateur ... . Par exemple, divrem prend exactement deux arguments, mais cette fonction ne peut pas manipuler directement de tuples :

1. julia> t = (7, 3);

2. julia> divrem(t)

3. ERROR: MethodError: no method matching divrem(::Tuple{Int64,Int64})

En revanche, il est envisageable de disperser le tuple :

1. julia> divrem(t...)

2. (2, 1)

De nombreuses fonctions internes utilisent des tuples à arguments en nombre variable. C'est le cas de max et min :

1. julia> max(1, 2, 3)

2. 3

En revanche la fonction sum ne traite pas les tuples :

1. julia> sum(1, 2, 3)

2. ERROR: MethodError: no method matching sum(::Int64, ::Int64, ::Int64)

12-4-1 - Exercice 12-1

Écrivez une fonction appelée sumall qui prend un nombre quelconque d'arguments et retourne leur somme.

12-5 - Tableaux et tuples

zip est une fonction interne qui prend deux ou plusieurs séquences et retourne une collection de tuples où chacun contient un élément de chaque séquence. Le nom de la fonction fait référence à une fermeture éclair.

Cet exemple permet de « zipper » une chaîne et un tableau :

1. julia> s = "abc";

2. julia> t = [1, 2, 3];

4. julia> zip(s, t)

5. zip("abc", [1, 2, 3])

Le résultat est un objet zip capable d'itérer(27) à travers les paires(28). L'utilisation la plus courante de zip se rencontre dans une boucle :

1. julia> for pair in zip(s, t)

2. println(pair)

3. end

4. ('a', 1)

5. ('b', 2)

6. ('c', 3)

Un objet zip est une espèce d'itérateur agissant sur une séquence. D'une certaine manière, les itérateurs sont similaires aux tableaux, mais, contrairement à ces derniers, il n'est pas possible d'utiliser les indices pour sélectionner un élément donné de l'itérateur.

Afin d'utiliser des opérateurs et des fonctions de tableau, il est astucieux d'utiliser un objet zip pour créer un tableau :

1. julia> collect(zip(s, t))

2. 3-element Array{Tuple{Char,Int64},1}:

3. ('a', 1)

4. ('b', 2)

5. ('c', 3)

Le résultat obtenu est un tableau de tuples. Dans cet exemple, chaque tuple contient un caractère de la chaîne et l'indice correspondant du tableau.

Si les séquences ne sont pas de même longueur, la plus courte détermine la taille du résultat.

1. julia> collect(zip("Yann", "Le Cun"))

2. 4-element Array{Tuple{Char,Char},1}:

3. ('Y', 'L')

4. ('a', 'e')

5. ('n', ' ')

6. ('n', 'C')

L'affectation des tuples est exploitable dans une boucle for pour parcourir un ensemble des tuples :

1. julia> t = [('a', 1), ('b', 2), ('c', 3)];

3. julia> for (letter, number) in t

4. println(number, " ", letter)

5. end

6. 1 a

7. 2 b

8. 3 c

À chaque fois, Julia sélectionne le tuple suivant dans le tableau et attribue les éléments à letter et number. Les parenthèses autour de (letter, number) sont obligatoires.

En combinant zip, for et l'affectation de tuples, on obtient une expression utile pour parcourir deux (voire plusieurs) séquences simultanément. Par exemple, hasmatch prend deux séquences, t1 et t2, et retourne true s'il y a un indice i tel que t1[i] == t2[i] :

1. function hasmatch(t1, t2)

2. for (x, y) in zip(t1, t2)

3. if x == y

4. return true

5. end

6. end

7. false

8. end

S'il faut parcourir les éléments d'une séquence et leurs indices, la fonction interne enumerate s'avère utile :

1. julia> for (index, element) in enumerate("abc")

2. println(index, " ", element)

3. end

4. 1 a

5. 2 b

6. 3 c

Le résultat de la fonction enumerate est un objet recensé qui parcourt une séquence de paires. Chaque paire contient un indice (à partir de 1) et un élément de la séquence traitée.

12-6 - Dictionnaires et tuples

Les dictionnaires peuvent être utilisés comme des itérateurs qui permettent de parcourir les paires clé-valeur. Nous pouvons y recourir dans une boucle for de ce type :

1. julia> d = Dict('a'=>1, 'b'=>2, 'c'=>3);

3. julia> for (key, value) in d

4. println(key, " ", value)

5. end

6. a 1

7. c 3

8. b 2

Comme on peut s'y attendre, les paires clé-valeur ne sont pas classées de manière ordonnée.

Inversement, un tableau de tuples est utilisable pour initialiser un nouveau dictionnaire :

1. julia> t = [('a', 1), ('c', 3), ('b', 2)];

3. julia> d = Dict(t)

4. Dict{Char,Int64} with 3 entries:

5. 'a'=> 1

6. 'c'=> 3

7. 'b'=> 2

En combinant Dict et zip, on accède à un procédé concis de création d'un dictionnaire :

1. julia> d = Dict(zip("abc", 1:3))

2. Dict{Char,Int64} with 3 entries:

3. 'a' => 1

4. 'c' => 3

5. 'b' => 2

Il est courant d'utiliser les tuples comme clés dans les dictionnaires. Par exemple, un annuaire téléphonique peut établir une correspondance entre des paires « nom — prénom » d'une part et de l'autre des numéros de téléphone. En supposant que nous ayons défini last (pour le nom de famille), first (pour le prénom) et number, nous pourrions écrire :

1. directory[last, first] = number

L'expression entre crochets est un tuple. Parcourir ce dictionnaire peut se faire via l'affectation des tuples.

1. for ((last, first), number) in directory

2. println(first, " ", last, " ", number)

3. end

Cette boucle parcourt les paires clé-valeur dans directory, ces paires étant des tuples. Elle attribue les éléments de la clé dans chaque tuple à last et first ainsi que la valeur à number. Ensuite, elle affiche le nom et le numéro de téléphone correspondant.

Il existe deux façons de représenter les tuples dans un diagramme d'état. La version détaillée montre les indices et les éléments tels qu'ils apparaissent dans un tableau. Par exemple, le tuple ("Rabelais", "François") apparaîtra comme dans la figure 12.6.1.

FIGURE 12.6.1 – Diagramme d'état associé au tuple ("Rabelais", "François")

Cependant, dans un diagramme plus étendu, il est pratique d'omettre les détails. Par exemple, un diagramme de l'annuaire téléphonique peut apparaître comme dans la figure 12.6.2.

Ici, les tuples sont représentés en utilisant la syntaxe de Julia comme raccourci graphique(29).

FIGURE 12.6.2 – Diagramme d'état associé au répertoire téléphonique.

12-7 - Séquences de séquences

Notre attention s'est portée sur les tableaux de tuples. Néanmoins, presque tous les exemples de ce chapitre fonctionnent également avec des tableaux de tableaux, des tuples de tuples et des tuples de tableaux. Pour éviter d'énumérer les combinaisons possibles, il est parfois plus facile de parler de « séquences de séquences ».

Dans de nombreux contextes, les différents types de séquences (chaînes de caractères, tableaux et tuples) peuvent être utilisés de manière interchangeable. Alors, comment choisir l'une d'entre elles plutôt que les autres ?

Tout d'abord, les chaînes de caractères sont plus limitées que les autres séquences, car les éléments doivent absolument être des caractères. Elles sont également persistantes. S'il faut pouvoir modifier les caractères d'une chaîne (plutôt que d'en créer une nouvelle), un tableau de caractères est plus pratique.

Les tableaux sont plus courants que les tuples, principalement parce qu'ils sont non persistants. Malgré cela, il existe quelques cas où les tuples leur sont préférés :

- dans certains contextes, comme une instruction de retour, il est syntaxiquement plus simple de créer un tuple qu'un tableau ;

- quand une séquence est passée en argument d'une fonction, l'utilisation de tuples réduit le risque de comportements inattendus dû à l'aliasing ;

- pour des raisons de performance, le compilateur pouvant se focaliser sur le type.

Comme les tuples sont persistants, ils ne fournissent pas de fonctions telles que sort! et reverse! qui modifient les tableaux existants. Cependant, Julia fournit la fonction interne sort qui prend un tableau et retourne un nouveau tableau avec les mêmes éléments triés et ordonnés. Julia met aussi à disposition la fonction reverse qui prend une séquence quelconque et retourne une séquence du même type dans l'ordre inverse.

12-8 - Débogage

Les tableaux, les dictionnaires et les tuples sont des exemples de structures de données. Plus loin dans ce document, nous abordons les structures de données composites comme des tableaux de tuples ou des dictionnaires qui contiennent des tuples comme clés et des tableaux comme valeurs. Les structures de données composites sont utiles. Malheureusement, elles sont sujettes à des erreurs de forme, c'est-à-dire des erreurs causées lorsqu'une structure de données présente un mauvais type, une mauvaise taille ou une mauvaise structure. Par exemple, si vous vous attendez à un tableau avec un entier et que vous recevez un entier ordinaire (c'est-à-dire non incorporé à un tableau), la structure de données ne fonctionnera pas.

Julia permet d'attacher un type aux éléments d'une séquence. La manière de procéder est détaillée dans le chapitre 17. Spécifier le type contribue à éliminer beaucoup d'erreurs de forme.

12-9 - Glossaire

tuple séquence persistante d'éléments où chacun de ceux-ci peut avoir son propre type.

affectation de tuples affection avec une séquence sur le membre de droite et un tuple de variables sur le membre de gauche. Le membre de droite est d'abord évalué et ses éléments sont ensuite affectés aux variables du membre de gauche.

agrégation opération consistant à assembler un tuple d'argument de longueur variable.

dispersion opération consistant à traiter une séquence comme une liste d'arguments.

objet zip résultat de l'appel de la fonction interne zip. Objet qui itère à travers une séquence de tuples.

itérateur objet qui peut itérer à travers une séquence, mais qui ne fournit pas d'opérateurs ni de fonctions de tableau.

structure de données collection de valeurs connexes, souvent organisées en tableau, dictionnaires, tuples, etc.

erreur de forme erreur causée par une valeur ayant une forme incorrecte, c'est-à-dire un type ou une taille incorrecte.

12-10 - Exercices

12-10-1 - Exercice 12-2

Écrivez une fonction appelée mostfrequent qui prend une chaîne de caractères et affiche les lettres par ordre décroissant d'occurrence. Trouvez des échantillons de texte de plusieurs langues différentes et voyez comment la fréquence des lettres varie d'une langue à l'autre. Comparez vos résultats avec les tableaux répertoriés sur les sites anglais et français.

12-10-2 - Exercice 12-3

Encore quelques anagrammes…

1. Écrivez un programme qui lit une liste de mots à partir d'un fichier (voir la section 9.1) et imprime les ensembles de mots qui sont des anagrammes. Voici un échantillon de ce à quoi la sortie pourrait ressembler avec le fichier mots_FR.txt :

1. ["mari", "mira", "rami", "rima"]

2. ["marche", "charme"]

3. ["mienne", "ennemi"]

4. ["alimenter", "terminale"]

Si vous voulez construire un dictionnaire qui établit une correspondance entre une collection de lettres et une série de mots qui peuvent être orthographiés avec ces lettres, comment pouvez-vous représenter la collection de lettres de manière à ce qu'elle puisse servir de clé ?

2. Modifiez le programme précédent de manière à ce qu'il affiche les anagrammes par ordre décroissant de taille.

3. Au jeu de Scrabble, un « scrabble(30) » est un coup où les sept lettres de votre grille sont posées en les combinant à une lettre du tableau, pour former un mot de huit lettres. Quelle collection de 8 lettres forme le plus de « scrabble » possible ?

12-10-3 - Exercice 12-4

Deux mots forment une paire de métathèse(31) si vous pouvez transformer l'un en l'autre en échangeant deux lettres. Par exemple, « converser » et « conserver ». Écrivez un programme qui trouve toutes les paires de métathèse dans le dictionnaire.

Ne testez pas toutes les paires de mots et ne testez pas tous les échanges possibles.

Remarque. Cet exercice est inspiré d'un exemple se trouvant sur le site« Puzzlers.org ».

12-10-4 - Exercice 12-5

Voici un autre exemple tiré du site Car Talk.

« Quel est le mot anglais le plus long, qui reste un mot anglais valide, car vous enlevez ses lettres une à une ?

Maintenant, les lettres peuvent être enlevées à l'une ou l'autre extrémité, ou au milieu, mais vous ne pouvez pas réarranger les lettres. Chaque fois que vous laissez tomber une lettre, vous vous retrouvez avec un autre mot anglais. Si vous faites cela, vous finirez par avoir une lettre et ce sera aussi un mot anglais - un mot que l'on trouve dans le dictionnaire. Je veux savoir quel est le mot le plus long et combien de lettres il comporte.

Je vais vous donner un petit exemple modeste : Sprite. D'accord ? On commence par sprite, on enlève une lettre, une de l'intérieur du mot, on enlève le r, et on se retrouve avec le mot spite, puis on enlève le e à la fin, on se retrouve avec spit, on enlève le s, on se retrouve avec pit, it et I. »

Écrivez un programme pour trouver tous les mots qui peuvent être réduits de cette façon, puis trouvez le plus long.

Le commentaire est associé à la langue anglaise. Utilisez mots_FR.txt (voir la section 9.1). Cet exercice est un peu plus difficile que la plupart des autres, aussi voici quelques suggestions :

1. Vous pourriez écrire une fonction qui prend un mot et calcule un tableau de tous les mots qui peuvent être formés en enlevant une lettre. Ce sont les « mots-fils » ;

2. Récursivement, un mot est réductible si l'un de ses fils est réductible. Comme cas de base, vous pouvez considérer la chaîne vide comme réductible ;

3. La chaîne vide peut être additionnée au dictionnaire ;

4. Pour améliorer la performance de votre programme, vous pouvez utiliser un « mémo » pour les mots connus pour être réductibles.

13 - Étude de cas : structures de données — choix

À ce stade, nous avons appris à connaître les structures de données de base en Julia et nous avons vu certains des algorithmes qui les exploitent.

Ce chapitre présente une étude de cas avec des exercices qui permettent de réfléchir à la sélection des structures de données en fonction du problème à résoudre.

13-1 - Analyse de la fréquence des mots

Comme d'habitude, il convient au moins d'essayer de résoudre les exercices avant d'en lire les solutions.

13-1-1 - Exercice 13-1

Écrivez un programme qui lit un fichier texte, découpe chaque ligne en mots et supprime les espaces ainsi que la ponctuation. Le programme doit ensuite convertir ces mots en minuscules.

La fonction isletter teste si un caractère est alphabétique.

13-1-2 - Exercice 13-2

Rendez-vous sur le site du Projet Gutenberg et téléchargez votre livre préféré en format texte brut (pour les livres en français, suivez ce lien).

Modifiez votre programme par rapport à l'exercice précédent pour parcourir le livre que vous avez téléchargé. Passez sur les informations d'en-tête au début du fichier et traitez le reste des mots comme dans l'exercice 13.1.1.

Modifiez ensuite le programme pour compter le nombre total de mots dans le livre et la fréquence d'utilisation de chaque mot.

Affichez le nombre de mots différents utilisés dans le livre. Comparez différents livres écrits à des époques différentes par des auteurs différents. Quel auteur emploie-t-il le vocabulaire le plus étendu ?

13-1-3 - Exercice 13-3

Modifiez le programme de l'exercice 13.1.2 pour imprimer les 20 mots les plus fréquemment utilisés dans le livre que vous avez choisi.

13-1-4 - Exercice 13-4

Modifiez le programme précédent pour lire une liste de mots, puis affichez tous les mots du livre qui ne figurent pas dans la liste de mots. Combien d'entre eux résultent de fautes de frappe ? Combien d'entre eux sont des mots courants qui devraient se trouver dans la liste de mots ? Combien d'entre eux sont rares ?

13-2 - Nombres aléatoires

Avec les mêmes données en entrée, la plupart des programmes informatiques produisent les mêmes données en sortie à chaque exécution. Ces programmes sont dits déterministes. Le déterminisme est généralement souhaité, puisque nous souhaitons que le même calcul donne le même résultat. Cependant, dans certaines applications (comme la sécurité informatique, l'analyse mathématique, les jeux, etc.), nous tenons à ce que l'ordinateur produise des données aléatoires.

Rendre un programme véritablement non déterministe (c'est-à-dire stochastique) est une tâche difficile. Néanmoins, il existe des moyens de le rendre au moins quasi non déterministe. Un de ces moyens consiste à utiliser des algorithmes qui fournissent des nombres pseudoaléatoires. Ces nombres ne sont pas vraiment aléatoires, parce qu'ils sont produits par un calcul déterministe, mais, en première analyse, il est pratiquement impossible de les distinguer de nombres réellement sortis au hasard.

La fonction rand retourne un flottant aléatoire entre 0.0 et 1.0 (y compris 0.0, mais pas 1.0). Chaque fois que rand est appelée, un nombre extrait d'une longue série est produit. Par exemple, nous pouvons faire tourner cette boucle :

1. for i in 1:10

2. x = rand()

3. println(x)

4. end

La fonction rand est susceptible de prendre un itérateur ou un tableau comme argument et elle retourne un élément aléatoire :

1. for i in 1:10

2. x = rand(1:6)

3. print(x, " ")

4. end

13-2-1 - Exercice 13-5

Écrivez une fonction appelée choosefromhist qui prend un histogramme tel que défini dans la section 11.2 et qui retourne une valeur aléatoire de l'histogramme choisi avec une probabilité proportionnelle à la fréquence. Par exemple, pour cet histogramme :

1. julia> t = ['a', 'a', 'b'];

3. julia> histogram(t)

4. Dict{Any,Any} with 2 entries:

5. 'a' => 2

6. 'b' => 1

Votre fonction devrait retourner 'a' avec une probabilité de et 'b' avec une probabilité de .

13-3 - Histogramme des mots

Dans un premier temps, essayez les exercices précédents avant de poursuivre. Dans un second temps, rendez-vous sur cette page où vous pouvez télécharger le texte brut UTF-8 de l'œuvre de Victor Hugo intitulée « Notre-Dame de Paris ». Une fois le fichier enregistré sous notre_dame_de_paris.txt, supprimez l'en-tête jusqu'au titre « LIVRE PREMIER » non inclus.

À présent, voici un programme qui lit un fichier et construit un histogramme des mots inclus dans ce fichier :

1. function processfile(filename)

2. hist = Dict()

3. for line in eachline(filename)

4. processline(line, hist)

5. end

6. hist

7. end;

9. function processline(line, hist)

10. line = replace(line, '-' =>  ' ')

11. for word in split(line)

12. word = string(filter(isletter, [word...])...)

13. word = lowercase(word)

14. hist[word] = get!(hist, word, 0) + 1

15. end

16. end;

1. hist = processfile("/home/chemin_vers_fichier/notre_dame_de_paris.txt");

Ce programme lit le fichier notre_dame_de_paris.txt.

processfile parcourt en boucle les lignes du fichier, les passant une à une à processline. L'histogramme hist est utilisé comme un accumulateur.

processline utilise la fonction replace pour remplacer les traits d'union par des espaces avant d'utiliser split pour diviser la ligne traitée en un tableau de chaînes de caractères. La boucle for de processline traverse le tableau de mots et utilise filter, islettre et lowercase pour supprimer la ponctuation et convertir les caractères en minuscules.(32)

Enfin, processline met à jour l'histogramme soit en créant un nouvel élément en cas de première détection, soit en incrémentant un élément existant.

Pour compter le nombre total de mots dans le fichier, il faut additionner les fréquences dans l'histogramme :

1. function totalwords(hist)

2. sum(values(hist))

3. end

Le nombre de mots différents n'est que le nombre d'éléments dans le dictionnaire :

1. function differentwords(hist)

2. length(hist)

3. end

Voici le code permettant d'afficher les résultats :

1. julia> println("Nombre total de mots: ", totalwords(hist))

2. Nombre total de mots: 172053

4. julia> println("Nombre de mots différents: ", differentwords(hist))

5. Nombre de mots différents: 17510

13-4 - Mots les plus communs

Pour trouver les mots les plus courants, nous pouvons (i) créer un tableau de tuples où chacun d'eux contient un mot et sa fréquence d'apparition et (ii) le trier. La fonction suivante prend un histogramme et retourne un tableau de tuples d'occurrence des mots :

1. function mostcommon(hist)

2. t = []

3. for (key, value) in hist

4. push!(t, (value, key))

5. end

6. reverse(sort(t))

7. end

Dans chaque tuple, la fréquence apparaît en premier, de sorte que le tableau résultant est trié par fréquences. Voici une boucle qui affiche les 10 mots les plus courants :

1. t = mostcommon(hist)

2. println("Les mots les plus courants sont:")

3. for (freq, word) in t[1:10]

4. println(word, "\t", freq)

5. end

Plutôt qu'un espace, un caractère de tabulation (\t) est employé comme séparateur. De cette manière, la deuxième colonne est alignée. Voici les résultats pour Notre-Dame de Paris :

1. Les mots les plus courants sont:

2. de       8263

3. la       5427

4. et       4585

5. le       4124

6. à        3363

7. les      2456

8. il       2446

9. un       2042

10. que      2033

11. qui      1978

Ce code peut être simplifié en utilisant le mot-clé rev associé à la fonction sort. À ce sujet, il est judicieux de consulter la page de documentation de sort.

13-5 - Paramètres optionnels

Nous avons vu des fonctions internes à Julia qui prennent des arguments optionnels (voir l'exercice 8.15.1). Il est également possible d'écrire des fonctions définies par le programmeur avec des arguments optionnels. Par exemple, voici une fonction qui affiche les mots les plus courants dans un histogramme :

1. function printmostcommon(hist, num=10)

2. t = mostcommon(hist)

3. println("Les mots les plus courants sont: ")

4. for (freq, word) in t[1:num]

5. println(word, "\t", freq)

6. end

7. end

Le premier paramètre est obligatoire, le second facultatif. La valeur par défaut de num est 10.

Si un seul argument est fourni :

1. printmostcommon(hist)

num prend la valeur par défaut. Si vous fournissez deux arguments :

1. printmostcommon(hist, 20)

num prend la valeur de l'argument indiqué. En d'autres termes, l'argument optionnel remplace la valeur par défaut.

Si une fonction possède à la fois des paramètres obligatoires et facultatifs, tous les paramètres obligatoires apparaîtront les premiers.

13-6 - Soustraction de dictionnaires

Trouver les mots d'un livre, qui ne sont pas dans la liste de mots de mots_FR.txt revient à un problème de soustraction d'ensembles, c'est-à-dire trouver « les mots du livre moins les mots de la liste ».

subtract prend les dictionnaires d1 et d2 et, ensuite, retourne un nouveau dictionnaire qui contient toutes les clés de d1 qui ne se trouvent pas dans d2. Comme nous ne nous soucions pas vraiment des valeurs, nous pouvons toutes les fixer à nothing.

1. function subtract(d1, d2)

2. res = Dict()

3. for key in keys(d1)

4. if key ∉ keys(d2)

5. res[key] = nothing

6. end

7. end

8. res

9. end

En vue de trouver les mots de Notre-Dame de Paris n'appartenant pas au fichier mots_FR.txt, nous pouvons utiliser processfile pour construire un histogramme associé à mots_FR.txt, et ensuite subtract :

1. words = processfile("mots_FR.txt")

2. diff = subtract(hist, words)

3. println("Mots du roman n'appartenant pas à la liste mots_FR.txt:")

4. for word in keys(diff)

5. print(word, " ")

6. end

Voici un bref extrait des résultats obtenus dans le cas de Notre-Dame de Paris :

1. Mots du roman n'appartenant pas à la liste mots_FR.txt:

2. lambrissé pâtisserie polyèdre magnétisme apostrophé damoiselles caballero gisante bretonne…

Certains de ces mots sont des noms, des adverbes, etc. D'autres, comme « damoiselles », ne sont plus d'usage courant en français. Toutefois, certains sont des mots courants qui devraient absolument figurer dans la liste.

13-6-1 - Exercice 13-6

Julia fournit une structure de données appelée Set qui permet de nombreuses opérations usuelles. Vous pouvez les consulter à la section 20.2 du chapitre 20 et lire la documentation Set-Like Collections.

Écrivez un programme exploitant la soustraction associée à Set pour détecter des mots du roman Notre-Dame de Paris qui ne sont pas dans la liste mots_FR.txt.

13-7 - Mots aléatoires

Pour choisir un mot aléatoire dans l'histogramme, l'algorithme le plus simple consiste à construire un tableau contenant plusieurs copies de chaque mot selon la fréquence observée. Ensuite, il faut les extraire du tableau :

1. function randomword(h)

2. t = []

3. for (word, freq) in h

4. for i in 1:freq

5. push!(t, word)

6. end

7. end

8. rand(t)

9. end

Une autre possibilité consiste à :

1. Utiliser des clés pour obtenir un tableau des mots du livre ;

2. Construire un tableau qui contient la somme des occurrences d'un mot (voir l'exercice 10.15.2). Le dernier élément de ce tableau est le nombre total (n) de mots dans le livre ;

3. Choisir un nombre aléatoire compris entre 1 et n. Utiliser une recherche par bissection (voir exercice 10.15.10) pour trouver l'indice où le nombre aléatoire pourrait être inséré dans la somme ;

4. Utiliser l'indice pour trouver le mot correspondant dans le tableau de mots.

13-7-1 - Exercice 13-7

Écrivez un programme qui applique cet algorithme pour choisir un mot aléatoire dans le livre.

13-8 - Analyse de Markov

Si quelqu'un choisissait au hasard des mots du roman Notre-Dame de Paris, il pourrait se faire une idée du vocabulaire, mais il n'obtiendrait très probablement pas une phrase cohérente. Voici le résultat d'une extraction aléatoire :

1. supplie des jours société de elle me vie en je

Une série de mots pris au hasard présente rarement du sens, car il n'existe pas de relation entre les mots successifs. Par exemple, dans une phrase sémantiquement correcte, on attend qu'un article comme « le » soit très probablement suivi d'un adjectif ou d'un nom, mais pas d'un verbe et encore moins d'un adverbe.

L'analyse de Markov est une manière d'établir ce genre de relations. Pour une séquence de mots donnée, cette analyse caractérise la probabilité qu'un mot en suive un autre. Par exemple, voici un extrait du poème Liberté de Paul Éluard :

Sur mes cahiers d’écolier
Sur mon pupitre et les arbres
Sur le sable sur la neige
J’écris ton nom
[…]
Sur la lampe qui s’allume
Sur la lampe qui s’éteint
Sur mes maisons réunies
J’écris ton nom
[…]
Sur mes refuges détruits
Sur mes phares écroulés
Sur les murs de mon ennui
J’écris ton nom
[…]
Et par le pouvoir d’un mot
Je recommence ma vie
Je suis né pour te connaître
Pour te nommer

Liberté

En l'occurrence, l'expression « la lampe » est toujours suivie du pronom relatif « qui », alors que l'expression « lampe qui » peut être suivie des verbes pronominaux conjugués au présent de l'indicatif « s'allume » ou « s'éteint ».

L'analyse de Markov consiste en une mise en correspondance de chaque préfixe (comme « la lampe » ou « lampe qui ») avec tous les suffixes possibles (comme « s'allume » ou « s'éteint »).

Grâce à cette correspondance, il devient possible de concevoir un texte aléatoire en commençant par n'importe quel préfixe et en choisissant aléatoirement parmi les suffixes possibles. Ensuite, la particule finale du préfixe peut être combinée avec le nouveau suffixe pour former le préfixe suivant. Le procédé est alors répété.

Par exemple, si nous commençons par le préfixe « le sable », le mot suivant doit être « sur » (c'est la seule combinaison entre ces deux termes observable dans l'extrait). Le préfixe suivant est « sable sur », donc le suffixe suivant doit être « la ». Ensuite, le préfixe suivant est « sur la » qui a pour suffixes soit « neige », soit « lampe ».

Dans cet exemple, le nombre de mots formant un préfixe vaut toujours deux, mais il est tout à fait envisageable d'entreprendre une analyse de Markov avec des préfixes dont les mots apparaissent en nombre quelconque, voire variable.

13-8-1 - Exercice 13-8

Analyse de Markov :

1. Écrivez un programme destiné à lire un texte depuis un fichier et à effectuer une analyse de Markov. Le résultat doit être un dictionnaire qui établit une correspondance entre les préfixes et une collection de suffixes possibles. La collection peut être un tableau, un tuple ou un dictionnaire. Il est de votre ressort de faire un choix approprié. Vous pouvez tester votre programme avec un nombre de préfixes égal à deux, mais votre programme devrait permettre de tester facilement des nombres supérieurs à deux.

2. Ajoutez une fonction au programme précédent pour produire un texte aléatoire basé sur l'analyse de Markov. Dans un premier temps, utilisez un préfixe constitué de deux mots sur le texte de Notre-Dame de Paris.
Que se passe-t-il si vous augmentez la longueur du préfixe ? Le texte aléatoire a-t-il plus de sens ?

3. Une fois votre programme opérationnel, vous pouvez essayer une combinaison : si vous mélangez des textes provenant de deux ou plusieurs livres, le texte aléatoire résultant assemblera le vocabulaire et les phrases des diverses sources de manière intéressante.

Remarque. Ce problème est basé sur un exemple de Brian K. Kernighan et Rob Pike (voir la référence [9]).

Vous devriez essayer cet exercice avant de poursuivre.

13-9 - Structure de données

Utiliser l'analyse de Markov pour générer du texte aléatoire est amusant, cependant, cet exercice a aussi un intérêt : la sélection de la structure des données. Dans les exercices précédents, vous deviez exercer des choix pour :

- la représentation des préfixes ;

- la représentation de la collection de suffixes possibles ;

- la représentation de la correspondance entre chaque préfixe et la collection de suffixes possibles.

La solution pour ce dernier choix à poser est simple. Un dictionnaire constitue le choix évident pour établir une correspondance entre les clés et les valeurs correspondantes.

Pour les préfixes, les options les plus évidentes sont les chaînes de caractères, les tableaux de chaînes de caractères ou les tuples de chaînes de caractères. Pour les suffixes, un tableau est une option ; un histogramme de dictionnaire en est une autre.

Comment choisir ? La première étape consiste à réfléchir aux opérations qu'il est nécessaire d'implémenter pour chaque structure de données. Pour les préfixes, nous devons pouvoir supprimer des mots du début et en ajouter à la fin. Par exemple, si le préfixe est « le sable » et que le mot suivant est « sur », il faut pouvoir former le préfixe suivant, « sable sur ». Un tableau pourrait constituer un premier choix, car il est facile d'y ajouter et d'y supprimer des éléments. Pour la collection de suffixes, les opérations à effectuer comprennent l'ajout d'un nouveau suffixe (ou l'augmentation de la fréquence d'un suffixe existant) ainsi que le choix d'un suffixe aléatoire.

L'ajout d'un nouveau suffixe est tout aussi facile dans le cas d'un tableau que dans celui d'un histogramme. Choisir un élément aléatoire dans un tableau est commode ; en choisir un dans un histogramme s'avère plus difficile à réaliser efficacement (voir l'exercice 13.7.1).

Jusqu'à présent, la facilité de mise en œuvre a été privilégiée. Cependant, d'autres facteurs interviennent dans le choix de la structure de données. L'un d'eux est le temps d'exécution. Parfois, une raison théorique permet d'espérer qu'une structure de données se montre plus rapide qu'une autre. Par exemple, nous avons vu que l'opérateur ∈ est plus rapide pour les dictionnaires que pour les tableaux, du moins lorsque le nombre d'éléments est important.

Cependant, très souvent, nul ne sait a priori quelle implémentation sera la plus rapide. Une option consiste à développer deux implémentations et à observer la meilleure. Cette approche est appelée analyse comparative (ou benchmarking). Une autre option pratique consiste à choisir la structure de données la plus facile à mettre en œuvre, puis à analyser si elle est suffisamment rapide pour l'application prévue. Si c'est le cas, il n'est pas nécessaire de chercher plus loin. Sinon, des outils comme le module Profile (voir la documentation Julia et Profileview) permettent d'identifier les blocs les plus lents d'un programme. Citons également BenchmarkTools (voir BenchmarkTools.jl) qui fournit diverses macros permettant d'automatiser les mesures de temps d'exécution sur plusieurs jeux de paramètres (aléatoires, par exemple) (33).

L'espace de stockage constitue un autre facteur à prendre en compte. Par exemple, l'utilisation d'un histogramme pour la collecte des suffixes peut prendre moins d'espace, car il suffit d'enregistrer chaque mot une seule fois, quelle que soit sa fréquence dans le texte. Dans certains cas, l'économie d'espace peut également accélérer le fonctionnement d'un programme et, à l'extrême, ce dernier peut ne pas fonctionner du tout en cas de mémoire saturée. Néanmoins, pour de nombreuses applications, l'espace mémoire reste une considération secondaire par rapport au temps d'exécution.

Une dernière réflexion : dans cette discussion, nous avons laissé entendre qu'une seule structure de données était à exploiter à la fois pour l'analyse et le développement. Parce que ces deux phases sont distinctes, il est envisageable d'utiliser une structure pour l'analyse et de la convertir ensuite en une autre pour le développement. Le gain sera net si le temps épargné pendant le développement dépasse celui consacré à la conversion.

Le paquet Julia DataStructures implémente 24 structures de données (voir la documentation sur les structures de données et DataStructures.jl).

13-10 - Débogage

Lors du débogage d'un programme, cinq attitudes sont à adopter en particulier lorsque le bogue est très difficile à débusquer :

la lecture examinez votre code, relisez-le avec détachement, comme si c'était le programme de quelqu'un d'autre et vérifiez ce qu'il exécute réellement ;

le fonctionnement expérimentez, en introduisant de petites modifications et en exécutant différentes versions. Souvent, si vous affichez le bon élément au bon endroit dans le programme, le problème saute aux yeux. Parfois, il faut construire un canevas ;

la réflexion prenez le temps de réfléchir. De quel type d'erreur s'agit-il : syntaxique, d'exécution ou sémantique ? Quelles informations pouvez-vous tirer des messages d'erreur ou du rendu du programme ? Quel type d'erreur pourrait causer le problème que vous constatez ? Qu'avez-vous modifié en dernier lieu, avant que le problème n'apparaisse ?(34)

la méthode du canard en plastique (rubber ducking) si vous expliquez le problème à quelqu'un d'autre, vous trouvez parfois la réponse avant même d'avoir fini de poser la question. Souvent, vous n'avez même pas besoin d'une autre personne. Vous pourriez simplement parler à un canard en plastique. Là, se trouve l'origine de la stratégie bien connue appelée « débogage par la méthode du canard en plastique »(35) ;

le « lâcher prise » à un moment donné, la meilleure chose à faire est de reculer et d'annuler les changements récents jusqu'à revenir à un programme qui fonctionne et que vous comprenez. Ensuite, une nouvelle période de reconstruction pourra commencer.

Les programmeurs débutants se trouvent parfois bloqués sur une de ces activités et oublient les autres. Chaque activité a ses propres limites. Par exemple, la lecture du code peut aider si le problème est d'ordre typographique, mais pas si le problème résulte d'un malentendu conceptuel. Si vous ne comprenez pas l'exécution de votre programme, vous pouvez le lire 100 fois et ne jamais repérer l'erreur, car celle-ci est mentale.

Faire des expériences peut aider, surtout en procédant par de petits tests simples. En revanche, si vous faites des essais sans stratégie ou sans lire votre code, vous risquez de basculer dans de la « programmation en mode aléatoire », qui consiste à faire des changements aléatoires jusqu'à ce que le programme fasse ce à quoi il est destiné. Il va sans dire que la programmation en mode aléatoire est très chronophage et sans réelle garantie de résultat.

Vous devez prendre le temps de réfléchir. Le débogage est de même essence qu'une science expérimentale. Vous devez avoir au moins une hypothèse sur la nature du problème. S'il y a deux possibilités ou plus, essayez de penser à un test discriminant.

Pour tout dire, même les meilleures techniques de débogage échouent lorsqu'il y a trop d'erreurs ou si le code que vous essayez de corriger est trop volumineux et trop complexe. Parfois, la meilleure option est de battre en retraite, en simplifiant le programme jusqu'à arriver à un code qui fonctionne et qui est parfaitement compris.

Les programmeurs débutants sont souvent réticents à rebrousser chemin parce qu'ils ne supportent pas d'effacer une ligne de code (même lorsqu'elle est erronée). Si cela vous rassure, sauvegardez votre programme dans un autre fichier avant de commencer à le décortiquer. Vous pourrez ensuite recopier un à un les blocs analysés et corrects.

Pour trouver un bogue difficile, il est nécessaire de lire, d'expérimenter, de réfléchir et parfois… de reculer. Si vous êtes bloqué sur l'une de ces activités, n'hésitez pas à essayer les autres.

13-11 - Glossaire

déterministe terme qui se rapporte à un programme qui produit le même résultat à chaque exécution, avec les mêmes apports (voir la notion de fonction déterministe en informatique).

pseudoaléatoire terme se rapportant à une séquence de nombres qui semble être aléatoire, mais qui est conçue par un programme déterministe ;

valeur par défaut valeur donnée à un paramètre optionnel si aucun argument n'est fourni ;

outrepassement (override) remplacement d'une valeur par défaut par un argument fourni par l'utilisateur ;

analyse comparative (benchmarking) processus consistant à choisir entre les structures de données en implémentant des alternatives et en les testant sur un échantillon d'entrées possibles.

méthode du canard en plastique (rubber ducking) méthode de débogage où le programmeur explique à un objet inanimé (tel qu'un canard en plastique) son problème en le décomposant pour se rendre compte de la ou des faille(s).

13-12 - Exercices

13-12-1 - Exercice 13-9

Le « rang » d'un mot est sa position dans un tableau de mots triés par fréquence : le mot le plus courant a le rang 1, le deuxième le plus courant, le rang 2, etc.

La loi de Zipf décrit une relation entre le rang et la fréquence des mots dans les langues naturelles. Plus précisément, elle prédit que la fréquence f du mot de rang r s'exprime comme :

où s et c sont des paramètres qui dépendent de la langue et du texte. Si vous prenez le logarithme des deux membres de cette équation, vous obtenez :

Si vous tracez en fonction de , vous devriez obtenir une droite de pente interceptant l'ordonnée à l'origine à .

Rédigez un programme qui lit un texte à partir d'un fichier, comptabilise la fréquence des mots et affiche une ligne pour chaque mot par ordre décroissant de fréquence, avec et .

Installez la bibliothèque graphique Plots comme indiqué dans l'annexe B.

Son utilisation est très facile :

1. julia> using Plots

2. julia> x = 0:10

3. julia> y = x .^2

4. julia> plot(x, y)

5. julia> savefig("/home/chemin_vers_repertoire/quadratic.pdf")

La figure 13.12.1 présente le graphique quadratic.pdf.

FIGURE 13.12.1 – Graphique « Plots » de la fonction pour

Utilisez la bibliothèque Plots pour mettre en graphique les résultats et vérifier qu'ils forment bien une droite.

14 - Fichiers

Ce chapitre introduit la notion de programmes persistants, capables de conserver les données en stockage permanent. Il montre comment utiliser différents types de stockage permanent tels que les fichiers et les bases de données.

14-1 - Persistance

La plupart des programmes que nous avons étudiés jusqu'à présent sont transitoires : ils fonctionnent pendant une courte période et produisent un résultat. Cependant, lorsqu'ils se terminent, leurs données disparaissent. Lors d'une nouvelle exécution, le programme repart ut nulla.

D'autres programmes sont persistants : ils s'exécutent sur de longues périodes (voire continuellement) et conservent au moins une partie de leurs données sur un support de stockage permanent (un disque dur, par exemple). S'ils s'arrêtent puis redémarrent, ils reprennent au point d'arrêt.

Les systèmes d'exploitation qui fonctionnent pratiquement à chaque fois qu'un ordinateur est allumé sont des exemples de programmes persistants. C'est encore plus vrai pour les serveurs web, qui fonctionnent en permanence, attendant des requêtes en provenance du réseau.

Un des moyens les plus simples pour que les programmes conservent leurs données consiste à lire et à écrire des fichiers « texte ». Jusqu'ici, nous avons vu des programmes qui lisent des fichiers texte. Dans ce chapitre, nous en étudions dont le comportement consiste à écrire dans des fichiers.

Une autre possibilité consiste à stocker l'état d'un programme dans une base de données. Nous étudions donc également la manière de tirer avantage d'une base de données simple.

14-2 - Lire et écrire

Un fichier texte est une séquence de caractères enregistrée sur un support permanent comme un disque dur ou une mémoire flash. Nous avons vu comment ouvrir et lire un fichier dans la section 9.1.

Pour créer un fichier et y écrire, il faut l'ouvrir avec le mode "w" (writing) en deuxième paramètre :

1. julia> fout = open("output.txt", "w")

2. IOStream(<file output.txt>)

Si le fichier existe déjà, l'ouverture en mode écriture efface irréversiblement les anciennes données. Prudence, donc ! Si le fichier n'existe pas, il est nouvellement créé. La fonction open retourne un objet fichier et la fonction d'écriture "w" permet d'écrire diverses données dans le fichier.

1. julia> line1 = "Voici quatre lions, deux rouges, deux noirs,\n";

2. julia> write(fout, line1)

3. 45

La valeur numérique retournée représente le nombre de caractères écrits dans le fichier. L'objet fichier conserve une trace de l'endroit où il se trouve dans l'arborescence de l'ordinateur. Par conséquent, si elle est à nouveau appelée, la fonction write ajoute les nouvelles données à la suite des précédentes :

1. julia> line2 = "Tels qu'ils figurent sur les armoiries d'Élouges.\n";

2. julia> write(fout, line2)

3. 51

Une fois la saisie terminée, il est pertinent de fermer le fichier.

1. julia> close(fout)

Si la fermeture du fichier n'a pas eu lieu, la terminaison du programme s'en chargera. L'état du fichier peut être contrôlé en l'ouvrant avec un éditeur de texte.

14-3 - Formatage

L'argument en écriture doit impérativement être une chaîne de caractères. Ainsi, pour écrire d'autres valeurs, il est nécessaire de les convertir en chaînes de caractères. Le moyen le plus simple est de procéder via l'interpolation de chaînes :

1. julia> fout = open("output.txt", "w")

2. IOStream(<file output.txt>)

3. julia> write(fout, string(7370))

4. 4

Une autre possibilité consiste à utiliser la famille de fonctions print(ln).

1. julia> chapelles = 2

2. julia> println(fout, "À Élouges, il y a $chapelles chapelles.")

La macro @printf est très puissante du fait qu'elle utilise des commandes de formatage de style C. Voir la documentation de printf.

14-4 - Noms de fichiers et chemins

Les fichiers sont organisés en répertoires (également appelés « dossiers »). Chaque programme en cours d'exécution possède un répertoire courant, c'est-à-dire celui par défaut pour la plupart des opérations. Par exemple, lorsqu'un fichier est ouvert aux fins de lecture, Julia le cherche dans le répertoire courant.

La fonction pwd retourne le nom du répertoire courant :(36)

1. julia> cwd = pwd()

2. "/home/aquarelle/Julia/Penser_en_Julia"

cwd signifie current working directory (répertoire de travail courant). Dans cet exemple, le résultat est /home/aquarelle/Julia/Penser_en_Julia, où /home/aquarelle/ est le répertoire d'origine d'un utilisateur(37) nommé aquarelle.

Une chaîne comme /home/aquarelle/Julia/ identifie un répertoire et constitue un chemin d'accès absolu.

Un simple nom de fichier, comme output.txt, est considéré comme un chemin relatif, car il se rapporte au répertoire courant. Si le répertoire courant est /home/aquarelle/Julia/Penser_en_Julia/, le nom de fichier output.txt fera référence à /home/aquarelle/Julia/Penser_en_Julia/output.txt.

Un chemin qui commence par / ne dépend pas du répertoire courant. Il s'agit d'un chemin absolu. Pour trouver le chemin absolu d'un fichier, il convient d'utiliser abspath :

1. julia> abspath("output.txt")

2. "/home/aquarelle/Julia/Penser_en_Julia/output.txt"

Julia propose d'autres fonctions pour travailler avec les noms de fichiers et les chemins d'accès. Par exemple, ispath vérifie si un fichier ou un répertoire existe :

1. julia> ispath("/home/aquarelle/Julia/Penser_en_Julia")

2. true

3. julia> ispath("/home/aquarelle/Julia/Penser_en_Julia/mots_FR.txt")

4. true

isdir vérifie l'existence d'un sous-répertoire appartenant au répertoire courant (les répertoires situés au-dessus du répertoire courant, y compris ce dernier, ne sont pas testés) :

1. julia> isdir("Chimie_Julia")

2. false

3. julia> isdir("Figures")

4. true

De même, isfile vérifie l'existence d'un fichier.

readdir retourne un tableau des fichiers (et autres répertoires) dans le répertoire courant :

1. julia> isfile("Figures/quadratic_1.pdf")

2. true

3. julia> readdir(cwd)

4. 7-element Array{String,1}:

5. "#Penser_en_Julia_v-0-24.lyx#"

6. "Solutions_Exercices"

7. "Figures"

8. "Penser_en_Julia_v-0-24.lyx"

9. "Penser_en_Julia_v-0-24.lyx~"

10. "Penser_en_Julia_v-0-24.pdf"

11. "Tests"

12. "mots_FR.txt"

13. "notre_dame_de_paris.txt"

Pour illustrer l'action de ces fonctions, la fonction walk explore un répertoire, affiche le nom de tous les fichiers et s'appelle récursivement au niveau des sous-répertoires.

1. function walk(dirname)

2. for name in readdir(dirname)

3. path = joinpath(dirname, name)

4. if isfile(path)

5. println(path)

6. else

7. walk(path)

8. end

9. end

10. end

La fonction interne joinpath prend un répertoire et un nom de fichier pour les réunir en un chemin complet.

Julia propose une fonction appelée walkdir (voir la page de documentation de walkdir) qui est similaire à walk tout en s'avérant polyvalente. À titre d'exercice, lisez la documentation de walkdir et utilisez cette fonction pour afficher les noms des fichiers dans un répertoire donné et ses sous-répertoires.

14-5 - Levée des exceptions

Lorsque des fichiers sont manipulés en lecture et en écriture, des erreurs peuvent survenir. Tenter d'ouvrir un fichier qui n'existe pas conduit à une SystemError :

1. julia> fin = open("fichier_inexistant.txt")

2. ERROR: SystemError: opening file "bad_file": Aucun fichier ou dossier de ce type

Si vous ne disposez pas des droits d'accès à un fichier (en l'occurrence en écriture), sous GNU/Linux par exemple, une erreur se produit :

1. julia> fout = open("/etc/passwd", "w")

2. ERROR: SystemError: opening file "/etc/passwd": Permission non accordée

Afin d'éviter ces erreurs, il serait envisageable d'utiliser des fonctions comme ispath et isfile. Hélas, cela nécessiterait beaucoup de temps et de code pour vérifier toutes les possibilités.

La fonction try offre une solution élégante. La syntaxe est similaire à celle d'une instruction if :

1. try

2. fin = open("fichier_inexistant.txt")

3. catch exc

4. println("Une erreur s'est produite: $exc")

5. end

Julia commence par exécuter la clause try. Si tout se passe normalement, le système sort de la clause try et poursuit l'exécution du programme. Si une exception se produit, Julia passe à l'exécution de la clause catch.

Le traitement d'une exception avec une clause try s'appelle la levée d'une exception. Dans cet exemple, la clause d'exception affiche un message d'erreur (peu utile). Cependant, en général, capter une exception apporte une chance réelle de résoudre le problème, d'essayer à nouveau ou, au moins, de terminer le programme sobrement.

Dans un code qui effectue des changements d'état ou qui utilise des ressources telles que des fichiers, un travail de nettoyage (fermeture de fichiers) doit généralement être réalisé lorsque le code est terminé. Les exceptions compliquent potentiellement cette tâche, car elles peuvent provoquer la sortie d'un bloc de code avant d'atteindre sa fin normale. Le mot-clé finally permet d'exécuter un code lorsqu'un bloc se termine, quelle que soit la manière dont il prend fin :

1. f = open("output.txt")

2. try

3. line = readline(f)

4. println(line)

5. finally

6. close(f)

7. end

De cette sorte, la fonction close est toujours exécutée.

14-6 - Bases de données

Une base de données est un fichier spécialement organisé pour conserver des données. De nombreuses bases de données sont structurées comme un dictionnaire : elles établissent une correspondance entre des clés et leurs valeurs. La plus grande différence entre une base de données et un dictionnaire vient de ce que la première réside sur un support permanent. Par conséquent, elle persiste après la fin du programme.

ThinkJulia fournit une interface au gestionnaire de base de données GDBM (GNU dbm(38)) pour la création et la mise à jour des fichiers de base de données. À titre d'exemple, créons une base de données qui contient des légendes associées à des fichiers contenant des images.

L'ouverture d'une base de données est similaire à l'ouverture de fichiers classiques :

1. julia> using ThinkJulia

3. julia> db = DBM("légendes", "c")

4. DBM(<légendes>)

Le mode "c" signifie que la base de données doit être créée si elle n'existe pas déjà. Le résultat est un objet « base de données » qui peut être utilisé (pour la plupart des opérations) à l'instar d'un dictionnaire.

Lorsqu'un nouvel élément est créé, le gestionnaire GDBM met à jour le fichier de la base de données :

1. julia> db["rabelais.png"] = "Portrait de François Rabelais."

2. "Portrait de François Rabelais."

Lorsque vous accédez à un des éléments, le gestionnaire GDBM lit le fichier :

1. julia> db["rabelais.png"]

2. "Portrait de François Rabelais."

Si vous effectuez une nouvelle affectation à une clé existante, le gestionnaire GDBM remplace l'ancienne valeur :

1. julia> db["rabelais.png"] = "Portrait de François Rabelais promenant le chat « Raminagrobis »."

2. "Portrait de François Rabelais promenant le chat « Raminagrobis »."

3. julia> db["rabelais.png"]

4. "Portrait de François Rabelais promenant le chat « Raminagrobis ». "

Certaines fonctions ayant un dictionnaire comme argument, comme key et value, sont inopérantes avec les objets de la base de données. Toutefois, l'itération avec une boucle for fonctionne :

1. for (key, value) in db

2. println(key, ": ", value)

3. end

Comme pour les autres fichiers, in fine, il est nécessaire de fermer la base de données :

1. julia> close(db)

14-7 - Sérialisation

Une des limites du gestionnaire GDBM provient du fait que les clés et les valeurs doivent être des chaînes de caractères ou des tableaux d'octets. Si un autre type est utilisé, Julia retourne une erreur.

Cependant, les fonctions serialize et deserialize contribuent à contourner cette limitation. Elles traduisent presque tout type d'objet en un tableau d'octets (IOBuffer(39)) adapté au stockage dans une base de données. Ensuite, elles retranscrivent les tableaux d'octets en objets :

1. julia> using Serialization

2. julia> io = IOBuffer();

4. julia> t = [1, 2, 3];

6. julia> serialize(io, t)

7. 24

8. julia> print(take!(io))

9. UInt8[0x37, 0x4a, 0x4c, 0x07, 0x04, 0x00, 0x00, 0x00, 0x15, 0x00, 0x08, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]

Le format n'est pas évident pour nous ; il est essentiellement censé être facile à interpréter pour Julia.

deserialize reconstitue l'objet :

1. julia> io = IOBuffer();

3. julia> t1 = [1, 2, 3];

5. julia> serialize(io, t1)

6. 24

7. julia> s = take!(io);

9. julia> t2 = deserialize(IOBuffer(s));

11. julia> print(t2)

12. [1, 2, 3]

serialize et deserialize écrivent et lisent un objet IOBuffer qui représente un flux d'entrées/sorties en mémoire. La fonction take! récupère le contenu du tampon d'entrées/sorties sous la forme d'un tableau d'octets et remet le tampon dans son état initial.

Bien que le nouvel objet ait la même valeur que l'ancien, il ne s'agit pas du même objet (du moins en général) :

1. julia> t1 == t2

2. true

3. julia> t1 ≡ t2

4. false

En d'autres termes, la sérialisation puis la désérialisation ont le même effet que la copie de l'objet.

Vous pouvez l'utiliser pour enregistrer des structures de données autres que des chaînes de caractères dans une base de données.

En fait, le stockage de structures de données autres que des chaînes de caractères dans une base de données est si courant qu'il a été encapsulé dans un paquet appelé JLD2 (voir la documentation de JLD2).

14-8 - Objets de commande

La plupart des systèmes d'exploitation fournissent une interface en ligne de commande, également appelée « shell »(40). Les shells fournissent entre autres des commandes pour naviguer dans le système de fichiers et aussi, pour lancer des applications. Par exemple, les systèmes permettent de changer de répertoire avec cd, d'afficher le contenu d'un répertoire avec ls, et de lancer un navigateur web en saisissant une commande comme firefox (par exemple) dans une console.

Tout programme pouvant être lancé depuis le shell l'est également depuis Julia à l'aide d'un objet de commande :

1. julia> cmd = `echo Julia est un langage très puissant.`

2. `echo Julia est un langage très puissant.`

Les guillemets inversés sont utilisés pour délimiter la commande. La fonction run exécute cette dernière :

1. julia> run(cmd)

2. Julia est un langage très puissant.

La partie « Julia est un langage très puissant. » représente la sortie de la commande echo, envoyée à STDOUT(41). La fonction run elle-même retourne un objet processus et émet une ErrorException si la commande externe ne s'exécute pas correctement.

Si vous souhaitez lire la sortie de la commande externe, il convient d'utiliser read au lieu de run :

1. julia> a = read(cmd, String)

2. "Julia est un langage très puissant.\n"

La plupart des systèmes fournissent une commande appelée md5sum ou md5 qui lit le contenu d'un fichier et calcule une « somme de contrôle ». Vous pouvez en apprendre davantage sur la page MD5 de Wikipédia. Cette commande fournit un moyen efficace de vérifier si deux fichiers présentent le même contenu. La probabilité que des contenus différents donnent la même somme de contrôle est extrêmement faible.

Vous pouvez utiliser une commande pour lancer md5 à partir de Julia et obtenir le résultat :

1. julia> filename = "output.txt"

2. "output.txt"

3. julia> cmd = `md5 $filename`

4. ` md5 output.txt`

5. julia> res = read(cmd, String)

6. "MD5 (output.txt) = d41d8cd98f00b204e9800998ecf8427e\n"

14-9 - Modules

Supposons que vous ayez un fichier nommé wc.jl contenant les 9 lignes de code suivant :

1. function linecount(filename)

2. count = 0

3. for line in eachline(filename)

4. count += 1

5. end

6. count

7. end

9. print(linecount("wc.jl"))

À l'exécution, ce programme se lit lui-même et affiche le nombre de lignes du fichier, soit 9. Il est permis de l'inclure dans le REPL comme ceci :

1. julia> include("wc.jl")

2. 9

Julia introduit des modules permettant de créer des espaces de travail variables distincts, c'est-à-dire avec de nouvelles portées générales.

Un module commence par le mot-clé module et se termine par end. Les conflits de noms sont évités entre vos propres définitions de haut niveau et celles trouvées dans le code de quelqu'un d'autre. import permet de contrôler quels noms d'autres modules sont visibles et export spécifie les noms de fichiers qui sont publics parmi les vôtres, c'est-à-dire ceux qui peuvent être utilisés en dehors du module sans être préfixés par le nom du module.

1. module LineCount

2. export linecount

3. function linecount(filename)

4. count = 0

5. for line in eachline(filename)

6. count += 1

7. end

8. count

9. end

10. end

L'objet LineCount fournit la commande linecount :

1. julia> using LineCount

2. julia> linecount("wc.jl")

3. 11

14-9-1 - Exercice 14-1

Saisissez cet exemple dans un fichier nommé wc.jl, incluez-le dans le REPL et entrez using LineCount.

Si vous réimportez un module, Julia ne réagira pas et ne relira pas le fichier, même en cas de modification. Si vous voulez recharger un module, vous devez redémarrer le REPL. Il existe un paquet Revise qui permet de prolonger la durée des sessions (voir Revise.jl).

14-10 - Débogage

Lorsque des fichiers sont utilisés en lecture et en écriture, des problèmes d'espacement sont susceptibles d'apparaître. Ces erreurs sont souvent difficiles à déboguer, car les espaces, les tabulations et les nouvelles lignes sont normalement invisibles :

1. julia> s = "1 2\t 3\n 4";

3. julia> println(s)

4. 1 2     3

5. 4

Les fonctions internes repr ou dump sont des auxiliaires pratiques. Elles prennent tout objet en argument et retournent une représentation de l'objet sous forme de chaîne.

1. julia> repr(s)

2. "\"1 2\t 3\n 4\""

3. julia> dump(s)

4. String "1 2\t 3\n 4"

Cela peut être utile pour le débogage.

Un autre problème susceptible d'être rencontré tient au fait que divers systèmes utilisent différents caractères pour indiquer la fin d'une ligne. Certains utilisent une nouvelle ligne, représentée par \n, d'autres un caractère de retour, représenté \r, d'autres encore les deux. Si des fichiers sont échangés entre différents systèmes, des incohérences de ce type peuvent survenir.

Pour la plupart des systèmes, il existe des applications permettant la conversion d'un format à un autre. Vous pouvez les trouver (et en apprendre plus sur cette question) en consultant la page Wikipédia traitant la fin de ligne. Bien entendu, vous pouvez en écrire une vous-même.

14-11 - Glossaire

persistant terme qui se rapporte à un programme qui fonctionne « indéfiniment » et qui conserve au moins une partie de ses données sur un support permanent.

fichier texte séquence de caractères enregistrée en permanence sur un disque dur (par exemple).

répertoire collection de fichiers nommés (parfois appelée « dossier »).

chemin d'accès chaîne de caractères qui identifie un fichier.

chemin relatif chemin d'accès qui part du répertoire courant.

chemin absolu chemin d'accès qui part du répertoire racine (ou root, symbolisé par / sur les systèmes , et BSD).

catch commande pour éviter qu'une exception ne mette fin à un programme en utilisant les déclarations try ... catch ... finally.

base de données fichier dont le contenu est organisé comme un dictionnaire avec des clés correspondant à des valeurs.

shell programme qui permet aux utilisateurs de saisir des commandes et de les exécuter en lançant d'autres programmes (comme bash, csh, sh).

objet de commande objet qui représente une commande shell, permettant à un programme Julia d'exécuter des commandes et d'en lire le résultat.

14-12 - Exercices

14-12-1 - Exercice 14-2

Écrivez une fonction appelée sed qui prend comme arguments une chaîne de caractères modèle, une chaîne de remplacement et deux noms de fichiers. Cette fonction doit lire le premier fichier et écrire le contenu dans le second, en le créant si nécessaire. Si la chaîne modèle apparaît quelque part dans le fichier, elle doit être substituée par la chaîne de remplacement.

Si une erreur se produit lors de l'ouverture, de la lecture, de l'écriture ou de la fermeture d'un fichier, votre programme doit capter l'exception, afficher un message d'erreur, puis quitter le flux de programmation.

14-12-2 - Exercice 14-3

Si vous avez résolu l'exercice 12.10.2, vous savez comment créer un dictionnaire qui établit une correspondance entre une chaîne de lettres triée et la série de mots pouvant être orthographiés avec ces lettres. Par exemple, "spot" correspond au tableau ["post", "pots", "stop", "tops"].

Écrivez un module qui importe anagramsets et qui fournit deux nouvelles fonctions : (i) storeanagrams qui doit enregistrer le dictionnaire des anagrammes en utilisant JLD2, (ii) readanagrams qui doit rechercher un mot et retourner un tableau de ses anagrammes.

14-12-3 - Exercice 14-4

Dans une grande collection de fichiers MP3, il peut y avoir plusieurs copies d'une même chanson stockées dans différents répertoires ou avec des noms de fichiers différents. L'objectif de cet exercice est de rechercher les doubles.

- Écrivez un programme qui effectue une recherche dans un répertoire ainsi que tous ses sous-répertoires (donc, de manière récursive) et qui retourne un tableau de chemins complets pour tous les fichiers ayant un suffixe donné (comme .mp3).

- Pour reconnaître les doubles, vous pouvez utiliser md5sum ou md5 pour calculer une « somme de contrôle » associée à chaque fichier. Si deux fichiers ont la même somme de contrôle, ils ont plus que probablement le même contenu.

- Pour effectuer une double vérification, vous pouvez utiliser la commande diff.

15 - Structures et objets

À ce stade, nous savons comment utiliser d'une part les fonctions pour organiser le code et d'autre part les types intégrés (ou internes) afin d'organiser les données. L'étape suivante consiste à apprendre à construire nos propres types pour organiser à la fois code et données. C'est un sujet important : quelques chapitres seront nécessaires pour y parvenir.

15-1 - Types composites

Jusqu'ici, nous avons utilisé de nombreux types internes à Julia. Nous allons maintenant définir un type personnel. À titre d'exemple, nous allons créer un type appelé Point qui représente un point dans un espace bidimensionnel. En notation mathématique, les points sont souvent écrits entre parenthèses avec une virgule séparant les coordonnées. Par exemple, dans un repère cartésien , représente l'origine et un point dont l'abscisse vaut et l'ordonnée .

Il y a plusieurs façons de traiter cette information en Julia :

1. Les coordonnées pourraient être enregistrées séparément dans deux variables, x et y ;

2. Elles pourraient se retrouver sous forme d'éléments au sein d'un tableau ou d'un tuple ;

3. Enfin, nous pourrions créer un type personnalisé pour représenter les points sous forme d'objets.

La création d'un nouveau type est plus compliquée que les autres options, mais elle présente des avantages qui apparaîtront bientôt.

Un type composite défini par le programmeur est également appelé une structure. La définition struct pour représenter un point ressemble à ceci :

1. struct Point

2. x

3. y

4. end

L'en-tête indique que la nouvelle structure s'appelle Point. Le corps définit les attributs ou les champs de la structure. La structure Point possède deux champs : x et y.

Une structure fonctionne comme une usine créant des objets. Pour créer un point, on peut appeler Point comme s'il s'agissait d'une fonction ayant pour arguments les valeurs des champs. Lorsque Point est utilisé comme une fonction, on l'appelle un constructeur.

1. julia> p = Point(3.0, 4.0)

2. Point(3.0, 4.0)

La valeur retournée est une référence à un objet Point, que nous affectons à p. La création d'un nouvel objet est une instanciation et l'objet est une instance du type. Lorsqu'une instance est affichée, Julia indique à quel type elle appartient et quelles sont les valeurs des attributs. Chaque objet est une instance d'un certain type, les termes « objet » et « instance » sont donc interchangeables. Cependant, dans ce chapitre, le terme « instance » sera utilisé pour désigner un type défini par le programmeur.

Un diagramme d'état qui rend compte d'un objet et de ses champs est appelé un diagramme d'objet (voir la figure 15.1.1).

FIGURE 15.1.1 – Diagramme d'objet pour le type Point avec la valeur de ses attributs.

15-2 - Les structures sont persistantes

La valeur des champs (ou attributs) peut être extraite en utilisant la notation . de cette manière :

1. julia> x = p.x

2. 3.0

3. julia> y = p.y

4. 4.0

L'expression p.x signifie : « Allez à l'objet auquel p se réfère et extrayez la valeur de x ». Dans l'exemple, nous attribuons cette valeur à une variable nommée x. Il n'y a pas de conflit entre la variable x et le champ x.

La notation par points peut être exploitée dans le cadre de n'importe quelle expression. Par exemple, en considérant p.x et p.y comme des longueurs, le théorème de Pythagore est applicable :

1. julia> distance = sqrt(p.x^2 + p.y^2)

2. 5.0

Ceci dit, par défaut, les structures sont persistantes. Après construction, les champs ne peuvent pas changer de valeur :

1. julia> p.y = 1.0

2. ERROR: setfield! immutable struct of type Point cannot be changed

Le caractère persistant des structures présente plusieurs avantages :

- l'efficacité ;

- l'impossibilité de violer les invariants fournis par les constructeurs du type (voir la section 17.4) ;

- la facilité à raisonner sur du code utilisant des objets persistants.

15-3 - Structures non persistantes

S'il y échet, des types composites non persistants peuvent être déclarés avec le mot-clé mutable struct. Voici la définition d'un point modifiable (MPoint pour « Mutable ») :

1. mutable struct MPoint

2. x

3. y

4. end

Ainsi, il devient possible d'attribuer des valeurs à une instance d'une structure mutable en utilisant la notation par points :

1. julia> blank = MPoint(0.0, 0.0)

2. MPoint(0.0, 0.0)

3. julia> blank.x = 3.0

4. 3.0

5. julia> blank.y = 4.0

6. 4.0

15-4 - Rectangles

Régulièrement, les champs d'un objet sont évidents. Néanmoins, dans certaines circonstances, il faut opérer des choix. Imaginons qu'il faille concevoir un type pour représenter des rectangles. Quels champs utiliser afin de spécifier l'emplacement et la taille d'un rectangle ? Pour simplifier les choses, supposons que le rectangle est disposé soit verticalement, soit horizontalement (pour éviter de traiter le problème de son inclinaison).

Il existe au moins deux possibilités :

- nous pouvons définir un coin du rectangle (ou son centre), la largeur et la hauteur ;

- nous pouvons définir deux coins opposés.

À ce stade, il est difficile de dire quel est le meilleur choix. À titre d'exemple, mettons en œuvre le premier cas de figure.

1. """

2. Représentation d'un rectangle.

3. champs: largeur, hauteur, coin.

4. """

5. struct Rectangle

6. largeur

7. hauteur

8. coin

9. end

La mini-documentation précise les champs : la largeur et la hauteur sont des nombres, tandis que coin (qui requiert deux coordonnées) est un objet Point qui définit le coin inférieur gauche.

Pour représenter un rectangle, il faut instancier un objet Rectangle :

1. julia> origine = MPoint(0.0, 0.0)

2. MPoint(0.0, 0.0)

3. julia> box = Rectangle(100.0, 200.0, origine)

4. Rectangle(100.0, 200.0, MPoint(0.0, 0.0))

Le diagramme d'objet (voir la figure 15.4.1) montre l'état de l'objet box. Un objet tel que coin, champ d'un autre objet, est dit intégré (embedded). Étant donné que l'attribut coin fait référence à un objet non persistant (MPoint), ce dernier est dessiné en dehors de l'objet Rectangle.

FIGURE 15.4.1 – Diagramme d'objet pour l'objet Rectangle.

15-5 - Instances en argument

Il est possible de faire passer une instance comme un argument selon la méthode habituelle. Par exemple :

1. function printpoint(p)

2. println("($(p.x), $(p.y))")

3. end

printpoint prend une instance Point comme argument et l'affiche en notation mathématique. Pour l'invoquer, vous pouvez passer p comme argument :

1. julia> printpoint(blank)

2. (3.0, 4.0)

Si un objet struct non persistant est passé à une fonction en tant qu'argument, cette fonction peut modifier les champs de l'objet. Par exemple, movepoint! prend un objet Point non persistant et deux nombres, dx et dy, et ajoute — respectivement — ces nombres aux attributs x et y de Point :

1. function movepoint!(p, dx, dy)

2. p.x += dx

3. p.y += dy

4. nothing

5. end

Voici un exemple qui en démontre l'effet :

1. julia> origine = MPoint(0.0, 0.0)

2. MPoint(0.0, 0.0)

3. julia> movepoint!(origine, 1.0, 2.0)

5. julia> origine

6. MPoint(1.0, 2.0)

À l'intérieur de la fonction, p est un alias d'origine. Par conséquent, lorsque la fonction modifie p, l'objet origine est aussi affecté.

Passer un objet Point persistant à movepoint! provoque une erreur :

1. julia> movepoint!(p, 1.0, 2.0)

2. ERROR: setfield! immutable struct of type Point cannot be changed

Cependant, il est permis de modifier la valeur d'un attribut non persistant au sein d'un objet persistant. Par exemple, moverectangle! a comme arguments un objet Rectangle et deux nombres (dx et dy). Cette fonction utilise movepoint! pour déplacer le coin du rectangle :

1. function moverectangle!(rect, dx, dy)

2. movepoint!(rect.coin, dx, dy)

3. end

De ce fait, p dans movepoint! est un alias de rect.coin. Aussi, quand p est modifié, rect.coin l'est-il également :

1. julia> box

2. Rectangle(100.0, 200.0, MPoint(0.0, 0.0))

3. julia> moverectangle!(box, 1.0, 2.0)

5. julia> box

6. Rectangle(100.0, 200.0, MPoint(1.0, 2.0))

Vous ne pouvez pas réaffecter un attribut non persistant associé à un objet persistant :

1. julia> box.coin = MPoint(1.0, 2.0)

2. ERROR: setfield: immutable struct of type Rectangle cannot be changed

15-5-1 - Exercice 15-1

Écrivez une fonction appelée distancebetweenpoints qui prend deux points comme arguments et retourne la distance entre eux.

15-6 - Instances en tant que valeurs retournées

Les fonctions peuvent renvoyer des instances. Par exemple, findcenter prend Rectangle comme argument et retourne un Point qui contient les coordonnées du centre du rectangle :

1. function findcenter(rect)

2. Point(rect.coin.x + rect.largeur / 2, rect.coin.y + rect.hauteur / 2)

3. end

L'expression rect.coin.x signifie : « Allez à l'objet rect et sélectionnez le champ nommé coin, puis rendez-vous à cet objet et sélectionnez le champ nommé x ».

Voici un exemple qui passe box en argument et affecte le Point résultant à centre :

1. julia> centre = findcenter(box)

2. Point(51.0, 102.0)

15-7 - Copies

L'aliasing peut rendre un programme difficile à lire, parce que les modifications apportées à un endroit peuvent avoir des effets inattendus ailleurs dans le code. Il est difficile de garder une trace de toutes les variables qui pourraient se référer à un objet donné.

La copie d'un objet est souvent une alternative judicieuse à l'aliasing. Julia propose une fonction appelée deepcopy qui peut dupliquer tout objet :

1. julia> p1 = MPoint(3.0, 4.0)

2. MPoint(3.0, 4.0)

3. julia> p2 = deepcopy(p1)

4. MPoint(3.0, 4.0)

5. julia> p1 ≡ p2

6. false

7. julia> p1 == p2

8. false

L'opérateur ≡ indique que p1 et p2 ne constituent pas le même objet, ce qui est attendu. En revanche, on aurait pu attendre que == donne true, car ces points contiennent les mêmes données. Dans ce cas, il faut être attentif au fait que, pour les objets non persistants, le comportement par défaut de l'opérateur == est le même que celui de l'opérateur ===. L'opérateur == vérifie l'identité de l'objet et non son équivalence. C'est parce que, pour les types composites non persistants, Julia ne sait pas ce qui doit être considéré comme équivalent (du moins, pas encore).

15-7-1 - Exercice 15-2

Créez une instance de Point, faites-en une copie et vérifiez l'équivalence ainsi que l'égalité de la copie et de son original. Le résultat peut vous surprendre, mais il montre pourquoi l'aliasing ne constitue pas un problème pour un objet persistant.

15-8 - Débogage

Lorsqu'on commence à travailler avec des objets, de nouvelles exceptions sont susceptibles d'apparaître. Par exemple, une tentative d'accès à un champ qui n'existe pas se solde par une erreur :

1. julia> p = Point(3.0, 4.0)

2. Point(3.0, 4.0)

3. julia> p.z = 1.0

4. ERROR: type Point has no field z Stacktrace: [1] setproperty!(::Point, ::Symbol, ::Float64) at ./sysimg.jl:19 [2] top-level scope at none:0

Au cas où vous ne seriez pas sûr du type d'objet, il convient de le demander à Julia :

1. julia> typeof(p)

2. Point

La fonction isa est également utilisable pour vérifier qu'un objet est bel et bien une instance d'un type :

1. julia> p isa Point

2. true

S'il n'est pas sûr qu'un objet possède un attribut particulier, la fonction interne fieldnames s'avère utile :

1. julia> fieldnames(Point)

2. (:x, :y)

Alternativement, la fonction isdefined est utilisable :

1. julia> isdefined(p, :x)

2. true

3. julia> isdefined(p, :z)

4. false

Le premier argument désigne tout objet. Le deuxième argument est composé du symbole : suivi du nom du champ.

15-9 - Glossaire

struct type composite.

constructeur fonction portant le même nom qu'un type qui crée des instances de ce type.

instance objet qui appartient à un type.

instancier créer un nouvel objet.

attribut ou champ une des valeurs nommées associées à un objet.

objet intégré objet « enchassé » dans le champ d'un autre objet.

duplication (deep copy) action de copier le contenu d'un objet, ainsi que tous les objets qui y sont intégrés et ainsi de suite. La duplication est mise en œuvre par la fonction deepcopy.

diagramme d'objet diagramme qui montre les objets, leurs champs et les valeurs des champs.

15-10 - Exercices

15-10-1 - Exercice 15-3

1. Rédigez une définition pour un type nommé Circle avec des champs center et radius, où center est un objet Point et radius est un nombre.

2. Instancier un objet circle qui représente un cercle dont le centre est à (150, 100) et le rayon vaut 75.

3. Écrivez une fonction appelée pointincircle qui prend un objet Circle et un objet Point et qui retourne true si le point se trouve dans ou sur la circonférence du cercle.

4. Écrivez une fonction appelée rectincircle qui prend un objet Cercle et un objet Rectangle et qui retourne true si le rectangle se trouve entièrement dans le cercle ou touche la circonférence.

5. Écrivez une fonction appelée rectcircleoverlap qui prend un objet Cercle et un objet Rectangle et retourne true si l'un des coins du rectangle se trouve à l'intérieur du cercle. Ou, dans une version plus complexe, elle retourne true si une partie quelconque du rectangle intersecte le cercle.

15-10-2 - Exercice 15-4

1. Écrivez une fonction appelée drawrect qui prend un objet « turtle » et un objet Rectangle et utilise la tortue pour dessiner le rectangle. Voir le chapitre 4 pour des exemples d'utilisation d'objets Turtle.

2. Écrivez une fonction appelée drawcircle qui prend un objet Turtle et un objet Cercle et qui dessine le cercle.

16 - Structures et fonctions

Dès lors que nous savons créer de nouveaux types composites, l'étape suivante consiste à écrire des fonctions qui prennent des objets définis par le programmeur comme paramètres et les retournent en tant que résultats. Dans ce chapitre, nous présentons également le « style de programmation fonctionnelle » et deux nouveaux plans de développement de programmes.

16-1 - Heures, minutes et secondes

Comme autre exemple de type composite, nous allons définir une struct appelée MyTime qui enregistre l'heure associée à un moment de la journée. Voici la définition de la structure :

1. """

2. Représentation d'un moment de la journée.

3. Champs: heure, minute, seconde

4. """

5. struct MyTime

6. heure

7. minute

8. seconde

9. end

Le mot Time étant déjà utilisé dans Julia, choisissons MyTime pour éviter tout conflit et créons un nouvel objet MyTime :

1. julia> time = MyTime(11, 59, 30)

2. MyTime(11, 59, 30)

Le diagramme d'objet pour MyTime est représenté à la figure 16.1.1.

FIGURE 16.1.1 – Diagramme d'objet associé à MyTime.

16-1-1 - Exercice 16-1

Écrivez une fonction appelée printtime qui prend un objet MyTime et l'affiche sous la forme heure:minute:seconde. La macro @printf du module StdLib Printf affiche un entier avec le format "%02d" en utilisant au moins deux chiffres, y compris un zéro de tête si nécessaire.

16-1-2 - Exercice 16-2

Écrivez une fonction booléenne appelée isafter qui prend deux objets MyTime, t1 et t2, et qui retourne true si t1 suit chronologiquement t2 et false dans le cas contraire. Défi : n'utilisez pas de test if.

16-2 - Fonctions pures

Dans les prochaines sections, nous allons écrire deux fonctions qui additionnent des valeurs de temps. Le but est d'appréhender deux types de fonctions : les fonctions pures et les modificateurs. Nous verrons également un plan de développement appelé prototype et correctifs. Un plan de développement est un procédé permettant de s'attaquer à un problème complexe en commençant par un prototype simple et en y incorporant graduellement des éléments qui le complexifient.

Voici un prototype simple addtime :

1. function addtime(t1, t2)

2. MyTime(t1.heure + t2.heure, t1.minute + t2.minute, t1.seconde + t2.seconde)

3. end

La fonction crée un nouvel objet MyTime, initialise ses champs et retourne une référence au nouvel objet. On parle de fonction pure, car elle ne modifie aucun des objets qui lui sont transmis en tant qu'arguments. En outre, elle n'a aucun autre effet (tel l'affichage d'une valeur ou l'obtention d'une entrée utilisateur) que le renvoi d'une valeur.

Pour tester cette fonction, créons deux objets MyTime :

- start contient l'heure de début d'un film, comme Le nom de la rose de Jean-Jacques Annaud ;

- duration contient la durée du film (2 heures 11 minutes).

addtime indique quand le film sera terminé.

1. julia> start = MyTime(9, 55, 0);

3. julia> duration = MyTime(2, 11, 0);

4. julia> done = addtime(start, duration);

6. julia> printtime(done)      # voir exercice 16.1.1

7. 11:66:00

Le résultat 11:66:00 est à peine inattendu. Le problème vient de ce que cette fonction ne traite pas les cas où le nombre de secondes ou de minutes dépasse 60. Lorsque cela se produit, il est nécessaire de « reporter » les secondes supplémentaires dans la colonne des minutes et/ou les minutes supplémentaires dans la colonne des heures. Voici une version améliorée :

1. function addtime(t1, t2)

2. seconde = t1.seconde + t2.seconde

3. minute = t1.minute + t2.minute

4. heure = t1.heure + t2.heure

5. if seconde >= 60

6. seconde -= 60

7. minute += 1

8. end

9. if minute >= 60

10. minute -= 60

11. heure += 1

12. end

13. MyTime(heure, minute, seconde)

14. end

Bien que cette fonction soit correcte, elle commence à s'allonger significativement. Ultérieurement (section 16.4), nous considérerons une version plus courte.

16-3 - Modificateurs

Il est parfois utile pour une fonction de modifier les objets qu'elle reçoit en paramètre. Dans ce cas, les modifications sont visibles pour l'appelant. Les fonctions qui procèdent de cette manière sont appelées des modificateurs.

La fonction increment! qui ajoute un nombre donné de secondes à un objet MyTime peut être écrite naturellement comme un modificateur. Voici une ébauche :

1. function increment!(time, secondes)

2. time.seconde += secondes

3. if time.seconde >= 60

4. time.seconde -= 60

5. time.minute += 1

6. end

7. if time.minute >= 60

8. time.minute -= 60

9. time.heure += 1

10. end

11. end

La première ligne effectue l'opération de base ; le reste traite des cas spéciaux que nous avons vus auparavant.

Cette fonction est-elle correcte ? Que se passe-t-il si secondes est bien supérieur à 60 ?

Dans ce cas, il ne suffit pas d'effectuer l'opération une seule fois. Nous devons poursuivre jusqu'à ce que time.second soit inférieur à soixante. Une solution consiste à remplacer les déclarations if par des déclarations while. Cela rendrait la fonction correcte, mais peu efficace.

Tout ce qui peut être fait avec des modificateurs peut également l'être avec des fonctions pures. En fait, certains langages de programmation n'autorisent que des fonctions pures. Il est prouvé que les programmes qui utilisent des fonctions pures sont plus rapides à développer et moins sujets aux erreurs que ceux recourant aux modificateurs. Cependant, les modificateurs sont parfois pratiques et les programmes fonctionnels ont tendance à être moins efficaces à l'exécution.

En général, il est recommandé d'écrire des fonctions pures chaque fois que cela est raisonnable et de ne recourir aux modificateurs que s'il existe un avantage incontestable. Cette approche pourrait être appelée : style de programmation fonctionnelle.

16-3-1 - Exercice 16-3

Écrivez une version correcte de la fonction increment! qui ne contienne aucune boucle.

16-3-2 - Exercice 16-4

Écrivez une version pure de la fonction increment, qui crée et retourne un nouvel objet MyTime plutôt que de modifier le paramètre.

16-4 - Prototypage ou planification ?

Le plan de développement exposé ci-dessus est dénommé « prototype et correctifs » (prototype and patches). Pour chaque fonction, nous avons écrit un prototype qui a effectué un calcul de base. Ensuite après l'avoir testé, nous en avons corrigé les erreurs graduellement.

Cette approche peut être efficace notamment lorsque le programmeur n'a pas encore une compréhension approfondie du problème. Ceci étant, les corrections incrémentales peuvent conduire à un code inutilement compliqué — puisqu'il traite de nombreux cas particuliers — et peu fiable — étant donné qu'il est difficile de connaître toutes les erreurs traitées.

Une autre option consiste en un développement planifié dans lequel une compréhension de haut niveau du problème peut rendre la programmation nettement plus aisée. Dans ce cas, l'idée est qu'un objet Time est en réalité un nombre composé de trois nombres en base 60 (voir le système sexagécimal).

Lorsque nous avons écrit addtime and increment!, nous faisions effectivement de l'addition en base 60, c'est pourquoi nous devions passer d'une colonne à l'autre.

Cette observation suggère une autre approche de l'ensemble du problème : nous pouvons convertir les objets MyTime en nombres entiers et profiter du fait que l'ordinateur est capable de faire de l'arithmétique des nombres entiers.

Voici une fonction qui convertit les objets MyTime en nombres entiers :

1. function timetoint(time)

2. minutes = time.heure * 60 + time.minute

3. secondes = minutes * 60 + time.seconde

4. end

Et voici une fonction qui convertit un entier en un objet de type MyTime (rappelons que divrem divise le premier argument par le second et retourne le quotient ainsi que le reste sous forme de tuple) :

1. function inttotime(secondes)

2. (minutes, seconde) = divrem(secondes, 60)

3. heure, minute = divrem(minutes, 60)

4. MyTime(heure, minute, seconde)

5. end

Il faudra peut-être réfléchir un peu et faire quelques tests pour se convaincre que ces fonctions sont correctes. Une façon de les tester est de vérifier que timetoint(inttotime(x)) == x pour de nombreuses valeurs de x. Ceci constitue un exemple de contrôle de cohérence.

Une fois convaincu que ces fonctions sont correctes, il devient possible de les utiliser afin de réécrire addtime :

1. function addtime(t1, t2)

2. secondes = timetoint(t1) + timetoint(t2)

3. inttotime(secondes)

4. end

Cette version est plus courte que l'originale et plus facile à vérifier.

Il est plus difficile de passer de la base 60 à la base 10 (et inversement) que de jongler avec les heures, minutes et secondes. La conversion de base est plus abstraite. La manipulation de valeurs heures/minutes/secondes est plus spontanée.

Cependant, si nous pouvions traiter ces valeurs comme des nombres en base 60(42) et faisions l'investissement d'écrire les fonctions de conversion (timetoint et inttotime), nous obtiendrions un programme plus court, plus facile à lire et à déboguer et… plus fiable.

Il est également plus aisé d'ajouter des fonctions par la suite. Par exemple, imaginons qu'il faille soustraire deux MyTime pour trouver la durée qui les sépare. Une approche naïve serait de mettre en œuvre la soustraction par emprunt. L'utilisation des fonctions de conversion serait plus facile en base 60 et aurait plus de chances d'être correcte.(43)

16-4-1 - Exercice 16-5

Réécrivez increment! en utilisant timetoint et inttotime.

16-5 - Débogage

Un objet MyTime est bien formé si les valeurs minute et seconde sont comprises entre 0 et 60 (y compris 0, mais pas 60) et si l'attribut heure est positif. heure et minute devraient être des valeurs entières, mais nous pourrions permettre à seconde d'être exprimée avec une partie fractionnaire.

De telles exigences sont appelées des invariants, car elles doivent toujours être vraies. En d'autres termes, si elles ne sont pas vraies, c'est que quelque chose dysfonctionne.

L'écriture d'un code pour vérifier les invariants peut aider à détecter les erreurs et à en trouver les causes. Par exemple, nous pourrions disposer d'une fonction comme isvalidtime qui prend un objet MyTime et retourne false si ce dernier viole un invariant :

1. function isvalidtime(time)

2. if time.heure < 0 || time.minute < 0 || time.seconde < 0

3. return false

4. end

5. if time.minute >= 60 || time.seconde >= 60

6. return false

7. end

8. true

9. end

Au début de chaque fonction, il est pertinent de vérifier les arguments pour s'assurer qu'ils sont valides :

1. function addtime(t1, t2)

2. if !isvalidtime(t1) || !isvalidtime(t2)

3. error("objet MyTime non valide dans add_time")

4. end

5. seconds = timetoint(t1) + timetoint(t2)

6. inttotime(secondes)

7. end

Une macro @assert peut être utilisée à la place, afin de vérifier un invariant donné et d'émettre une exception en cas d'échec :

1. function addtime(t1, t2)

2. @assert(isvalidtime(t1) && isvalidtime(t2), "objet MyTime non valide dans add_time")

3. secondes = timetoint(t1) + timetoint(t2)

4. inttotime(secondes)

5. end

Les macros @assert sont utiles, car elles permettent de distinguer le code qui traite des conditions normales de celui qui vérifie les erreurs.

16-6 - Glossaire

prototype et correctifs plan de développement qui implique la rédaction d'une ébauche de programme, le test et la correction des erreurs au fur et à mesure que ces dernières sont détectées.

planification plan de développement qui implique une compréhension de haut niveau d'un problème et une planification plus poussée que le développement progressif ou le développement de prototypes/correctifs.

fonction pure fonction qui ne modifie aucun des objets qu'elle reçoit comme arguments. La plupart des fonctions pures possèdent une valeur de retour.

modificateur fonction qui modifie un ou plusieurs des objets qu'elle reçoit comme arguments. La plupart des modificateurs sont vides (ou nuls), c'est-à-dire qu'ils ne retournent rien.

style de programmation fonctionnelle style de conception de programme dans lequel la majorité des fonctions sont pures.

invariant paramètre ou attribut qui ne devrait jamais changer pendant l'exécution d'un programme.

16-7 - Exercices

16-7-1 - Exercice 16-6

Écrivez une fonction appelée multime qui prend un objet MyTime ainsi qu'un nombre et qui retourne un nouvel objet MyTime contenant le produit du MyTime original et du nombre.

Utilisez ensuite multime pour écrire une fonction qui prend un objet MyTime représentant le temps d'arrivée dans une course ainsi qu'un nombre figurant la distance. Cette fonction doit retourner un objet MyTime qui donne l'allure moyenne (durée par kilomètre parcouru).

16-7-2 - Exercice 16-7

Julia fournit des objets « temps » similaires à MyTime, développé dans ce chapitre. Toutefois, ils offrent un riche ensemble de fonctions et d'opérateurs. Lisez la documentation à l'adresse Dates.

1. Écrivez un programme qui recueille la date du jour et affiche le jour de la semaine.

2. Écrivez un programme qui accepte une date d'anniversaire en entrée et affiche l'âge de l'utilisateur ainsi que le nombre de jours, d'heures, de minutes et de secondes jusqu'à son prochain anniversaire.

3. Pour deux personnes nées à des jours différents, il existe un jour où l'une d'elles est deux fois plus âgée que l'autre. Écrivez un programme qui prend deux dates anniversaires et calcule DoubleJour.

4. Défi : écrivez une version plus générale qui calcule le jour où une personne est n fois plus âgée qu'une autre.

17 - Dispatch multiple

Julia offre la possibilité d'écrire du code capable de fonctionner sur différents types, ce qui est connu comme de la programmation générique. Dans ce chapitre, nous abordons l'utilisation des déclarations de type en Julia et nous présentons des méthodes permettant d'implémenter différents comportements pour une fonction selon les types associés à ses arguments. Il s'agit là du dispatch multiple (multiple dispatch).

17-1 - Déclarations de types

L'opérateur :: associe des annotations de type aux expressions et aux variables :

1. julia> (1 + 2) :: Float64

2. ERROR: TypeError: in typeassert, expected Float64, got Int64

3. julia> (1 + 2) :: Int64

4. 3

Cela permet de confirmer qu'un programme fonctionne de manière adéquate.

Par ailleurs, l'opérateur :: peut être ajouté dans le membre de gauche d'une affectation ou dans le cadre d'une déclaration.

1. julia> function returnfloat()

2. x::Float64 = 100

3. x

4. end

5. returnfloat (generic function with 1 method)

6. julia> x = returnfloat()

7. 100.0

8. julia> typeof(x)

9. Float64

En l'occurrence, la variable x est toujours de type Float64 et la valeur est convertie en virgule flottante si nécessaire.

Une annotation de type peut également être jointe à l'en-tête de la définition d'une fonction :

1. function sinc(x)::Float64

2. if x == 0

3. return 1

4. end

5. sin(x)/(x)

6. end

La valeur de retour de sinc est toujours convertie en type Float64.

Par défaut, lorsque les types ne sont pas précisés, Julia considère les valeurs comme étant de type quelconque (Any).

17-2 - Méthodes

Dans la figure 16.1.1, nous avons défini une structure appelée MyTime et dans la section 16.1, nous avons écrit une fonction appelée printtime :

1. using Printf

3. struct MyTime

4. heure :: Int64

5. minute :: Int64

6. seconde :: Int64

7. end

9. function printtime(time)

10. @printf("%02d:%02d:%02d", time.heure, time.minute, time.seconde)

11. end

Comme on peut le constater, les déclarations de type peuvent (et, pour des raisons de performance, devraient) être associées aux champs d'une définition de structure.

Pour appeler la fonction printtime, il est nécessaire de passer un objet MyTime en argument :

1. julia> start = MyTime(9, 55, 0)

2. MyTime(9, 55, 0)

3. julia> printtime(start)

4. 09:55:00

Pour ajouter à la fonction printtime une méthode qui n'accepte comme seul argument qu'un objet MyTime, il suffit d'ajouter :: suivi de MyTime à l'argument time dans la définition de la fonction :

1. function printtime(time::MyTime)

2. @printf("%02d:%02d:%02d", time.heure, time.minute, time.seconde)

3. end

Une méthode est une définition de fonction avec une signature spécifique : printtime a un argument de type MyTime.

Appeler la fonction printtime avec un objet MyTime produit le même résultat :

1. julia> printtime(start)

2. 09:55:00

À présent, nous pouvons redéfinir la première méthode sans l'annotation de type ::, ce qui permet l'usage d'un argument de type quelconque :

1. function printtime(time)

2. println("Je ne sais pas comment afficher l'argument time.")

3. end

Si nous appelons la fonction printtime avec un objet différent de MyTime, nous obtenons :

1. julia> printtime(150)

2. Je ne sais pas comment afficher l'argument time.

17-2-1 - Exercice 17-1

Réécrivez time et inttotime pour spécifier leur argument (voir la section 16.4).

17-3 - Exemples supplémentaires

Voici une version de la fonction increment (voir la section 16.3) réécrite pour spécifier ses arguments :

1. function increment(time::MyTime, secondes::Int64)

2. secondes += timetoint(time)

3. inttotime(secondes)

4. end

À présent, il s'agit d'une fonction pure et non plus d'un modificateur.

Voici comment cette fonction increment peut être invoquée :

1. julia> start = MyTime(9, 45, 0)

2. MyTime(9, 45, 0)

3. julia> increment(start, 1337)

4. MyTime(10, 7, 17)

Si les arguments apparaissent dans le mauvais ordre, Julia retourne une erreur :

1. julia> increment(1337, start)

2. ERROR: MethodError: no method matching increment(::Int64, ::MyTime)

En effet, la signature de la méthode est increment(time::MyTime, seconds::Int64) et non increment(seconds::Int64, time::MyTime).

Réécrire isafter pour agir uniquement sur les objets MyTime est tout aussi aisé :

1. function isafter(t1::MyTime, t2::MyTime)

2. (t1.heure, t1.minute, t1.seconde) > (t2.heure, t2.minute, t2.seconde)

3. end

Au fait, les arguments optionnels sont implémentés comme syntaxe pour les définitions de méthodes multiples. Par exemple, cette définition :

1. function f(a=1, b=2)

2. a + 2b

3. end

se traduit par les trois méthodes suivantes :

1. f(a, b) = a + 2b

2. f(a) = f(a, 2)

3. f() = f(1, 2)

En Julia, ces expressions sont des définitions valides de méthode. Il s'agit d'une notation abrégée pour la définition des fonctions/méthodes.

17-4 - Constructeurs

Un constructeur est une fonction spéciale qui est appelée pour créer un objet. Les méthodes de constructeur par défaut de MyTime ont les signatures suivantes :

1. MyTime(heure, minute, seconde)

2. MyTime(heure::Int64, minute::Int64, seconde::Int64)

Nous pouvons également ajouter nos propres méthodes de construction externes :

1. function MyTime(time::MyTime)

2. MyTime(time.heure, time.minute, time.seconde)

3. end

Cette dernière méthode est appelée constructeur de copie, car le nouvel objet MyTime est une copie de son argument.

Pour forcer l'usage des invariants, il est nécessaire de recourir à la méthode des constructeurs internes :

1. struct MyTime

2. heure :: Int64

3. minute :: Int64

4. seconde :: Int64

5. function MyTime(heure::Int64=0, minute::Int64=0, seconde::Int64=0)

6. @assert(0 ≤ minute < 60, "Minute n'est pas entre 0 et 60.")

7. @assert(0 ≤ seconde < 60, "Seconde n'est pas entre 0 et 60.")

8. new(heure, minute, seconde)

9. end

10. end

La structure MyTime dispose maintenant de quatre méthodes à constructeurs internes :

1. MyTime()

2. MyTime(heure::Int64)

3. MyTime(heure::Int64, minute::Int64)

4. MyTime(hour::Int64, minute::Int64, seconde::Int64)

Une méthode à constructeur interne est toujours définie à l'intérieur du bloc d'une déclaration de type. Elle a accès à une fonction spéciale appelée new qui crée des objets du type nouvellement déclaré.

Le constructeur par défaut n'est pas disponible si un constructeur interne est défini. Il faut écrire explicitement tous les constructeurs internes dont on a besoin.

Une deuxième méthode avec la fonction locale new sans arguments existe :

1. mutable struct MyTime

2. heure :: Int

3. minute :: Int

4. seconde :: Int

5. function MyTime(heure::Int64=0, minute::Int64=0, seconde::Int64=0)

6. @assert(0 ≤ minute < 60, "Les minutes sont comprises entre 0 et 60.")

7. @assert(0 ≤ seconde < 60, "Les secondes sont comprises entre 0 et 60.")

8. time = new()

9. time.heure = heure

10. time.minute = minute

11. time.seconde = seconde

12. time

13. end

14. end

Cela permet de construire des structures de données récursives, c'est-à-dire une structure dont un des champs est la structure elle-même. Dans ce cas, la structure doit être non persistante puisque ses champs sont modifiés après l'instanciation.

17-5 - show

show est une fonction spéciale qui retourne une représentation en chaîne d'un objet. Par exemple, voici une méthode show pour les objets MyTime :

1. using Printf

3. function Base.show(io::IO, time::MyTime)

4. @printf(io, "%02d:%02d:%02d", time.heure, time.minute, time.seconde)

5. end

Le préfixe Base est nécessaire parce que nous voulons ajouter une nouvelle méthode à la fonction Base.show.

Lorsqu'un objet est affiché, Julia invoque la fonction show :

1. julia> time = MyTime(9, 45)

2. 09:45:00

Lorsque nous écrivons un nouveau type composite, il est pertinent :

- de commencer presque toujours par écrire un constructeur extérieur (ceci facilite l'instanciation des objets) ;

- d'utiliser show (qui est utile pour le débogage).

17-5-1 - Exercice 17-2

Écrivez une méthode de construction extérieure pour la classe Point qui prend x ainsi que y comme paramètres optionnels et les affecte aux champs correspondants.

17-6 - Surcharge d'opérateurs

En définissant les méthodes des opérateurs, nous pouvons spécifier leur comportement sur des types définis par le programmeur. Par exemple, si nous définissons une méthode nommée + avec deux arguments MyTime, nous pouvons utiliser l'opérateur + capable d'additionner des objets MyTime.

Voici à quoi pourrait ressembler la définition :

1. import Base.+

3. function +(t1::MyTime, t2::MyTime)

4. secondes = timetoint(t1) + timetoint(t2)

5. inttotime(secondes)

6. end

La déclaration d'importation ajoute l'opérateur + au champ d'application local afin que des méthodes puissent être ajoutées.

Voici comment nous pouvons l'utiliser :

1. julia> start = MyTime(9, 55)

2. 09:55:00

3. julia> duration = MyTime(2, 11, 0)

4. 02:11:00

5. julia> start + duration

6. 12:06:00

Lorsque l'opérateur + est appliqué aux objets MyTime, Julia invoque la méthode nouvellement ajoutée. Lorsque le REPL affiche le résultat, Julia invoque show. Il se passe donc beaucoup de choses en coulisses.

L'ajout au comportement d'un opérateur pour qu'il fonctionne avec des types définis par le programmeur s'appelle la surcharge de l'opérateur.

17-7 - Dispatch multiple

Dans la section précédente, nous avons additionné deux objets MyTime. Cependant, il est également possible d'ajouter un entier à un objet MyTime :

1. function +(time::MyTime, secondes::Int64)

2. increment(time, secondes)

3. end

Voici un exemple qui utilise l'opérateur + avec un objet MyTime et un entier :

1. julia> start = MyTime(9, 55)

2. 09:55::00

3. julia> start + 1337

4. 10:07:17

L'addition étant un opérateur commutatif, il faut donc ajouter une méthode complémentaire.

1. function +(secondes::Int64, time::MyTime)

2. time + secondes

3. end

Nous obtenons alors le même résultat :

1. julia> 1337 + start

2. 10:07:17

Le choix de la méthode à exécuter lorsqu'une fonction est appliquée s'appelle un dispatch. Julia permet au processus de dispatching de choisir la méthode d'une fonction à appeler selon le nombre d'arguments passés et les types de chacun des arguments de la fonction. L'utilisation de tous les arguments d'une fonction pour laisser le choix de la méthode à invoquer est connue sous le nom de dispatch multiple(44).

17-7-1 - Exercice 17-3

Écrivez des méthodes + pour les objets Point (voir le chapitre 15) :

1. Si les deux opérandes sont des objets point, la méthode doit retourner un nouvel objet Point dont la coordonnée x est la somme des coordonnées x des opérandes, de même pour les coordonnées y ;

2. Si le premier ou le second opérande est un tuple, la méthode doit ajouter le premier élément du tuple à la coordonnée x et le second élément à la coordonnée y, et retourner un nouvel objet point avec le résultat.

17-8 - Programmation générique (généricité)

Lorsqu'il est nécessaire, le dispatch multiple est d'une grande utilité. Ce n'est malheureusement pas toujours le cas. Souvent, il est possible de l'éviter en écrivant des fonctions qui se comportent correctement pour des arguments de types différents.

De nombreuses fonctions que nous avons écrites pour des chaînes de caractères fonctionnent également pour d'autres types de séquences. Par exemple, dans la section 11.2, nous avons utilisé la fonction histogram pour compter le nombre d'occurrences de chaque lettre apparaissant dans un mot.

1. function histogram(s)

2. d = Dict()

3. for c in s

4. if c ∉ keys(d)

5. d[c] = 1

6. else

7. d[c] += 1

8. end

9. end

10. d

11. end

Cette fonction agit également sur les tableaux, les tuples et même les dictionnaires à condition que les éléments de s soient hachables, afin qu'ils puissent être utilisés comme clés dans le dictionnaire d.

1. julia> t = ("poêle", "œuf", "poêle", "poêle", "jambon", "poêle")

2. ("poêle", "œuf", "poêle", "poêle", "poêle", "poêle")

3. julia> histogram(t)

4. Dict{Any,Any} with 3 entries :

5. "jambon"  =>  1

6. "poêle"   =>  4

7. "œuf"     =>  1

Les fonctions capables de manipuler plusieurs types sont dites polymorphiques. Le polymorphisme contribue à la réutilisation du code.

Par exemple, la fonction interne sum, qui ajoute les éléments d'une séquence, remplit son rôle tant que les éléments de la séquence supportent l'addition.

Comme une méthode + est fournie pour les objets MyTime, ceux-ci fonctionnent avec sum :

1. julia> t1 = MyTime(1, 7, 2)

2. 01:07:02

3. julia> t2 = MyTime(1, 5, 8)

4. 01:05:08

5. julia> t3 = MyTime(1, 5, 0)

6. 01:05:00

7. julia> sum((t1, t2, t3))

8. 03:17:10

En général, si toutes les opérations à l'intérieur d'une fonction remplissent leur rôle avec un type donné, la fonction fera de même avec ce type.

Le meilleur type de polymorphisme est le type involontaire, où vous découvrez qu'une fonction que vous avez déjà écrite peut être appliquée à un type jusque-là imprévu.

17-9 - Interface et implémentation

Un des objectifs du dispatch multiple est de rendre les logiciels plus faciles à gérer et à entretenir. Cela signifie qu'il est possible d'une part de continuer à faire fonctionner le programme lorsque d'autres parties du système changent et, de l'autre, de modifier le programme pour répondre à de nouvelles exigences.

Un principe de conception qui contribue à atteindre cet objectif consiste à garder les interfaces séparées des implémentations. Autrement dit, les méthodes ayant un argument annoté avec un type ne doivent pas dépendre de la façon dont les champs de ce type sont représentés.

Par exemple, dans ce chapitre, nous avons développé une structure qui représente un moment de la journée. Les méthodes ayant un argument annoté avec ce type comprennent timetoint, isafter et +.

Nous pourrions implémenter ces méthodes de plusieurs manières. Les détails de l'implémentation dépendent de la façon dont nous représentons MyTime. Dans ce chapitre, les champs d'un objet MyTime étaient l'heure, la minute et la seconde.

Comme autre option, nous aurions pu remplacer ces champs par un seul entier représentant le nombre de secondes depuis minuit. Cette implémentation rendrait certaines fonctions, comme isafter, plus faciles à écrire. En revanche, d'autres seraient plus difficiles à développer.

Après avoir déployé un nouveau type, vous pourriez découvrir une meilleure implémentation. Si d'autres parties du programme utilisent votre type, cela peut être chronophage et provoquer des erreurs par changement de l'interface.

Cependant, si vous avez conçu l'interface avec soin, vous pouvez modifier l'implémentation sans changer l'interface. La conséquence immédiate est que les autres parties du programme ne doivent pas être modifiées.

17-10 - Débogage

Appeler une fonction avec les bons arguments peut être ardu lorsque plus d'une méthode est spécifiée pour la fonction. Julia permet d'effectuer une introspection des signatures associées aux méthodes d'une fonction.

Pour savoir quelles méthodes sont disponibles pour une fonction donnée, la fonction methods vient à point :

1. julia> methods(printtime)

2. # 2 methods for generic function "printtime":

3. [1] printtime(time::MyTime) in Main at REPL[3]:2

4. [2] printtime(time) in Main at REPL[4]::2

17-11 - Glossaire

annotation de type l'opérateur :: suivi d'un type indiquant qu'une expression ou une variable est de ce type.

méthode définition d'un comportement possible d'une fonction.

dispatch choix de la méthode à mettre en œuvre lorsqu'une fonction est exécutée.

signature c'est le nombre et le type des arguments d'une méthode permettant au dispatch de sélectionner la méthode la plus spécifique d'une fonction lors de l'appel de fonction.

constructeur externe constructeur défini en dehors de la définition de type pour spécifier les méthodes utiles à la création d'un objet.

constructeur interne constructeur défini à l'intérieur de la définition de type pour imposer des invariants ou pour construire des objets récursifs.

constructeur par défaut constructeur interne disponible lorsqu'aucun constructeur interne défini par le programmeur n'est fourni.

constructeur de copie méthode de construction extérieure d'un type avec comme seul argument un objet du type. Cette méthode crée un nouvel objet, une copie de l'argument.

surcharge d'opérateur extension du comportement d'un opérateur (comme +) pour que celui fonctionne avec un type défini par le programmeur.

dispatch multiple (multiméthode) dispatch basé sur l'ensemble des arguments d'une fonction.

programmation générique (généricité) rédaction d'un code susceptible d'opérer avec plusieurs types.

17-12 - Exercices

17-12-1 - Exercice 17-4

Modifiez les champs de MyTime pour qu'il s'agisse d'un seul nombre entier représentant les secondes depuis minuit. Ensuite, modifiez les méthodes définies dans ce chapitre pour qu'elles fonctionnent avec la nouvelle implémentation.

17-12-2 - Exercice 17-5

Rédigez une définition pour un type nommé Kangaroo avec un champ nommé putinpocket du type Array et les méthodes suivantes :

1. Un constructeur qui initialise pouchcontents dans un tableau vide.

2. Une méthode nommée putinpouch qui prend un objet Kangaroo et un objet de n'importe quel type et l'ajoute à pouchcontents.

3. Une méthode show qui retourne une représentation en chaîne de caractères de l'objet Kangaroo et du contenu de la poche.

Testez votre code en créant deux objets Kangaroo, en les affectant à des variables nommées kanga et roo, puis en ajoutant roo au contenu de la poche de kanga.

18 - Sous-typage

Dans le chapitre précédent, nous avons présenté le dispatch multiple et les méthodes polymorphes. En ne précisant pas le type d'arguments, on obtient une méthode qui peut être appelée avec des arguments de tout type. La spécification d'un sous-ensemble de types autorisés dans la signature d'une méthode est l'étape suivante logique.

Dans ce chapitre, nous explicitons la notion de sous-typage en utilisant des types qui représentent des cartes à jouer, des jeux de cartes et des « mains » au poker.

Si vous ne jouez pas au poker, vous pouvez vous informer sur le lien Wikipédia : Poker. Cependant, ce n'est pas obligatoire ; tout au long des exercices, nous irons à l'essentiel.

18-1 - Cartes

Dans un paquet, il y a cinquante-deux cartes, chacune appartenant à une des quatre couleurs et à un des treize rangs (ou hauteurs, voir l'exercice 18.13.3). Au poker, les couleurs sont pique (♠), cœur (♥), carreau (♦) et trèfle (♣). Les rangs sont : as (A), 2, 3, 4, 5, 6, 7, 8, 9, 10, valet (J(45)), dame (Q) et roi (K). Selon le jeu auquel on participe, un as peut être supérieur au roi ou inférieur à 2.

Si nous souhaitons définir un nouvel objet pour représenter une carte à jouer, il est évident que les attributs doivent être le rang et la couleur. En revanche, le type de ces attributs n'est pas aussi intuitif à déterminer. Une option consiste à utiliser des chaînes de caractères contenant des mots comme « pique » pour les couleurs et « dame » pour les rangs. Un problème relatif à cette mise en œuvre provient de la difficulté de comparer les cartes en termes de préséance de rang ou de couleur.

Une autre option consiste à utiliser des nombres entiers pour coder les rangs et les couleurs. Dans ce contexte, « coder » signifie que nous allons définir une correspondance entre des nombres et les couleurs ainsi qu'entre des nombres et les rangs. Ce type de codage n'est pas secret, autrement, il s'agirait d'un chiffrement.

Par exemple, ce tableau illustre la correspondance « couleurs nombres entiers » :

- ♠ 4

- ♥ 3

- ♦ 2

- ♣ 1

Cette correspondance permet de comparer facilement les cartes. La hiérarchie des couleurs correspond à la hiérarchie des nombres. Nous procédons de la même manière pour les rangs avec 13 nombres (13 roi, 12 dame, etc.).

Le symbole est employé pour indiquer clairement que ces correspondances ne font pas partie du programme Julia. Elles relèvent de la conception du programme, sans toutefois apparaître explicitement dans le code.

La définition de la struct pour Carte se présente de cette manière :

1. struct Carte

2. couleur :: Int64

3. rang :: Int64

4. function Carte(couleur::Int64, rang::Int64)

5. @assert(1 ≤ couleur ≤ 4, "la couleur n'est pas entre 1 et 4")

6. @assert(1 ≤ rang ≤ 13, "le rang n'est pas entre 1 et 13")

7. new(couleur, rang)

8. end

9. end

Pour créer une carte, nous appelons Carte avec la couleur et le rang souhaités :

1. julia> dame_de_carreau = Carte(2, 12)

2. Carte(2, 12)

18-2 - Variables globales

Afin d'afficher les objets Carte de sorte que tout le monde puisse les lire facilement, il faut établir une correspondance entre les couleurs et leurs entiers ainsi qu'une correspondance entre les rangs et leurs entiers. Une manière naturelle de procéder consiste à utiliser deux tableaux de chaînes de caractères(46) :

1. const noms_couleurs = ["♣", "♦", "♥", "♠"]

2. const noms_rangs = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "V", "D", "R"]

Les variables noms_couleurs et noms_rangs sont globales. La déclaration const signifie que la variable ne peut être attribuée qu'une seule fois. Cela résout le problème de la performance des variables globales.

Dès lors, nous pouvons mettre en œuvre une méthode show idoine :

1. function Base.show(io::IO, carte::Carte)

2. print(io, noms_rangs[carte.rang], noms_couleurs[carte.couleur])

3. end

L'expression noms_rangs[carte.rang] signifie « utiliser le champ rang de l'objet carte comme indice dans le tableau noms_rangs et sélectionner la chaîne appropriée ».

Avec les méthodes dont nous disposons à ce stade, nous pouvons créer et afficher des cartes :

1. julia> Carte(3, 11)

2. V♥

18-3 - Comparaison de cartes

Pour les types internes, il existe des opérateurs relationnels (<, >, ==, etc.) qui comparent les valeurs et déterminent quand l'une est supérieure, inférieure ou égale à l'autre. Pour les types définis par le programmeur, nous pouvons remplacer le comportement des opérateurs intégrés en fournissant une méthode nommée : <.

La préséance correcte des cartes n'est pas évidente. Par exemple, du 3 de trèfle ou du 2 de carreau qui l'emporte ? L'un a un rang plus élevé, mais l'autre a une couleur plus élevée. Pour comparer les cartes, il faut décider si c'est le rang ou la couleur qui l'emporte.

La réponse peut dépendre du jeu. Pour simplifier, nous supposons que la couleur a priorité sur le rang. De la sorte, toutes les piques l'emportent sur toutes les carreaux, etc. Ainsi, dans l'exemple cité au début de cette section, 3♣ < 2♦.

Ceci fixé, nous pouvons écrire < :

1. import Base.<

3. function <(c1::Carte, c2::Carte)

4. (c1.couleur, c1.rang) < (c2.couleur, c2.rang)

5. end

18-3-1 - Exercice 18-1

Écrivez une méthode < pour les objets MyTime. Vous pouvez utiliser la comparaison de tuples, mais vous pourriez aussi considérer la comparaison d'entiers.

18-4 - Tests unitaires

Les tests unitaires (T.U.) permettent de vérifier l'exactitude d'un code en comparant les résultats effectifs de ce dernier à ce qu'on en attend. Cela peut être utile pour s'assurer que le code est toujours correct après modifications. Cette technique est aussi une manière de prédéfinir le comportement correct du code pendant la phase de développement.

Des tests unitaires simples peuvent être effectués avec les macros @test :

1. julia> using Test

3. julia> @test Carte(1, 4) < Carte(2, 4)

4. Test Passed

5. julia> @test Carte(1, 3) < Carte(1, 4)

6. Test Passed

@test retourne « Test Passed » si l'expression est true, « Test Failed » si elle est false et « Error Result » si elle ne peut pas être évaluée.

18-5 - Paquets de cartes

Maintenant que nous avons les cartes, l'étape suivante consiste à créer des paquets de cartes. Il est naturel que chaque paquet contienne un tableau de cartes comme attribut.

Voici une structure composite de Paquet. Le constructeur crée les cartes des champs et produit un jeu de 52 cartes :

1. struct Paquet

2. cartes :: Array{Carte, 1}

3. end

5. function Paquet()

6. paquet = Paquet(Carte[])

7. for couleur in 1:4

8. for rang in 1:13

9. push!(paquet.cartes, Carte(couleur, rang))

10. end

11. end

12. paquet

13. end

La manière la plus simple de constituer un paquet consiste à utiliser deux boucles. La boucle extérieure permet de créer les couleurs (de 1 à 4) et la boucle intérieure permet de produire les rangs (de 1 à 13). Chaque itération crée une nouvelle carte qui entre dans paquet.cartes. Ainsi, la première carte produite est l'as de trèfle, puis le 2 de trèfle, etc., jusqu'à la dernière carte, le roi de pique.

Voici la méthode show pour Paquet :

1. function Base.show(io::IO, paquet::Paquet)

2. for carte in paquet.cartes

3. print(io, carte, " ")

4. end

5. println()

6. end

Voici le résultat :

1. julia> Paquet()

2. A♣ 2♣ 3♣ 4♣ 5♣ 6♣ 7♣ 8♣ 9♣ 10♣ V♣ D♣ R♣ A♦ 2♦ 3♦ 4♦ 5♦ 6♦ 7♦ 8♦ 9♦ 10♦ V♦ D♦ R♦ A♥ 2♥ 3♥ 4♥ 5♥ 6♥ 7♥ 8♥ 9♥ 10♥ V♥ D♥ R♥ A♠ 2♠ 3♠ 4♠ 5♠ 6♠ 7♠ 8♠ 9♠ 10♠ V♠ D♠ R♠

18-6 - Ajouter, supprimer, mélanger et trier

Pour distribuer des cartes, il faudrait pouvoir utiliser une fonction qui retire une carte du jeu et l'affiche. La fonction pop! est utile à cet égard :

1. function Base.pop!(paquet::Paquet)

2. pop!(paquet.cartes)

3. end

Vu son mode d'action (retrait du dernier élément d'une structure de données), pop! agit sur le bas de la pile.

Ajouter une carte peut se faire en utilisant la fonction push! :

1. function Base.push!(paquet::Paquet, carte::Carte)

2. push!(paquet.cartes, carte)

3. paquet

4. end

Une telle méthode qui exploite une autre méthode en n'effectuant que peu de traitement supplémentaire s'appelle du placage. La métaphore vient de la marqueterie, où un placage est une fine couche de bois noble collée à la surface d'une pièce de bois de moindre qualité afin d'en améliorer l'apparence.

Dans ce cas, push! est une méthode « légère » qui adapte une opération sur les tableaux au cas des paquets de cartes. Elle améliore l'interface de l'implémentation.

Comme autre exemple, pour battre les cartes, nous pouvons écrire une méthode appelée shuffle! en utilisant la fonction Random.shuffle! :

1. using Random

2. function Random.shuffle!(paquet::Paquet)

3. shuffle!(paquet.cartes)

4. paquet

5. end

18-6-1 - Exercice 18-2

Écrivez une fonction appelée sort! qui utilise la fonction sort! pour trier les cartes dans un Paquet. sort! utilise la méthode < que nous avons définie pour déterminer l'ordre de préséance des cartes (voir la section 18.3).

18-7 - Types abstraits et sous-typage

Pour jouer aux cartes, il faut créer une « main », c'est-à-dire un ensemble de cartes détenues par un joueur. Pour cela, il est nécessaire d'y associer un type. Or, une main est similaire à un jeu de cartes : les deux sont composés d'une collection de cartes et les deux recourent à des opérations comme l'ajout et le retrait de cartes.

Toutefois, il existe des différences. Certaines opérations spécifiques aux mains n'ont pas de sens pour un jeu de cartes complet. Par exemple, au poker, on peut comparer deux mains pour déterminer laquelle est gagnante. Au bridge, on peut calculer le score d'une main pour faire une offre.

Nous devons donc trouver un moyen de regrouper les types spécifiques lorsqu'ils sont apparentés. En Julia, et pour notre problème, la technique consiste à définir un type abstrait qui sert de parent à la fois pour le paquet de cartes complet (Paquet) et pour une main (UneMain(47)). Cette manière de procéder s'appelle le sous-typage.

Nommons le type abstrait EnsembleDeCartes :

1. abstract type EnsembleDeCartes end

Un nouveau type abstrait est créé avec le mot-clé abstract type. Dans la déclaration que nous venons de faire, un type « parent » peut être facultativement spécifié en précisant (après le nom) le symbole <: suivi lui-même du nom d'un type abstrait existant.

Lorsqu'aucun supertype n'est indiqué, Julia utilise le supertype par défaut Any, un type abstrait prédéfini. Tous les objets en sont des instances. Tous les types en sont des sous-types.

À présent, exprimons que Paquet est un sous-type descendant du type abstrait EnsembleDeCartes :

1. struct Paquet <: EnsembleDeCartes

2. cartes :: Array{Carte, 1}

3. end

5. function Paquet()

6. paquet = Paquet(Carte[])

7. for couleur in 1:4

8. for rang in 1:13

9. push!(paquet.cartes, Carte(couleur, rang))

10. end

11. end

12. paquet

13. end

L'opérateur isa vérifie si un objet est d'un type donné :

1. julia> paquet = Paquet();

3. julia> paquet isa EnsembleDeCartes

4. true

Une main est aussi un sous-type du type parent EnsembleDeCartes :

1. struct UneMain <: EnsembleDeCartes

2. cartes :: Array{Carte, 1}

3. label :: String

4. end

6. function UneMain(label::String="")

7. UneMain(Carte[], label)

8. end

Au lieu de peupler une main avec 52 nouvelles cartes, le constructeur associé à UneMain initialise cartes sous la forme d'un tableau vide. Un argument optionnel peut être passé au constructeur, ce qui permet de donner une étiquette à UneMain.

1. julia> main = UneMain("nouvelle main")

2. UneMain(Carte[], "nouvelle main")

18-8 - Types abstraits et fonctions

Ceci fait (section 18.7), nous pouvons écrire les opérations communes à Paquet et UneMain comme des fonctions ayant pour argument EnsembleDeCartes :

1. function Base.show(io::IO, edc::EnsembleDeCartes)

2. for carte in edc.cartes

3. print(io, carte, " ")

4. end

5. end

7. function Base.pop!(edc::EnsembleDeCartes)

8. pop!(edc.cartes)

9. end

11. function Base.push!(edc::EnsembleDeCartes, carte::Carte)

12. push!(edc.cartes, carte)

13. nothing

14. end

À présent, nous pouvons utiliser pop! et push! pour distribuer une carte :

1. julia> paquet = Paquet()

2. julia> shuffle!(paquet)

3. julia> carte = pop!(paquet)

4. julia> push!(main, carte)

L'étape suivante consiste naturellement à encapsuler ce code dans une fonction appelée move! :

1. function move!(edc1::EnsembleDeCartes, edc2::EnsembleDeCartes, n::Int)

2. @assert 1 ≤ n ≤ length(edc1.cartes)

3. for i in 1:n

4. carte = pop!(edc1)

5. push!(edc2, carte)

6. end

7. nothing

8. end

La fonction move! prend trois arguments : deux objets EnsembleDeCartes et le nombre de cartes à distribuer. Elle modifie les deux objets EnsembleDeCartes et renvoie nothing.

Dans certains jeux, les cartes sont déplacées d'une main à l'autre alors que, dans d'autres jeux, elles sont échangées depuis une main vers le paquet. La fonction move! est donc utilisable pour ces opérations : edc1 et edc2 peuvent être soit de type Paquet, soit de type UneMain.

18-9 - Diagrammes de types

Jusqu'à présent, nous avons vu des diagrammes de pile (qui résume l'état d'un programme) et des diagrammes d'objet (qui mettent en évidence les attributs d'un objet et leurs valeurs). Ces diagrammes représentent un instantané de l'exécution d'un programme. En conséquence, ils évoluent au fur et à mesure de l'exécution. Ils sont également très détaillés, voire trop pour certains usages.

Un diagramme de types est une esquisse plus abstraite de la structure d'un programme. Au lieu de dépeindre des objets individuels, il représente de manière synthétique les types et les relations qu'ils entretiennent (voir la figure 18.9.1).

Il existe plusieurs catégories de relation entre les types :

- les objets d'un type spécifique peuvent contenir des références à des objets d'un autre type. Par exemple, chaque Rectangle (voir la section 15.4) contient une référence à un Point. Dans le présent chapitre, chaque Paquet contient des références à un tableau de Carte. Ce type de relation est appelé HAS-A, comme dans l'expression en anglais : « a Rectangle has a Point » ;

- un type spécifique peut avoir un type abstrait comme supertype. Cette relation est appelée IS-A, comme dans la formulation en anglais : « UneMain is a kind of EnsembleDeCartes » ;

- un type peut dépendre d'un autre dans la mesure où les objets d'un type prennent les objets du second type comme paramètres ou, alors, utilisent les objets du second type dans le cadre d'un calcul. Ce type de relation est appelé une dépendance.

FIGURE 18.9.1 – Diagramme de types et relations entre types.

Dans la figure 18.9.1, les flèches à extrémité creuse représentent une relation IS-A. Dans le cas présent, elles indiquent qu'UneMain et Paquet ont comme supertype EnsembleDeCartes.

Les flèches à extrémité pleine représentent une relation HAS-A. Dans le cas présent, Paquet a des références aux objets Carte.

L'étoile () près de la pointe d'une flèche désigne une multiplicité. Elle indique que Paquet ou UneMain possèdent un certain nombre de Cartes. La multiplicité peut être un simple nombre (comme 52), une fourchette (5:7) ou une étoile (). Ce dernier cas indique que Paquet peut avoir un nombre quelconque de Cartes.

Il n'y a pas de dépendances dans ce diagramme. Elles sont normalement indiquées par une flèche pointillée. Cependant, si les dépendances sont nombreuses, il est judicieux d'omettre leur représentation.

Un diagramme plus détaillé peut montrer qu'un Paquet contient un tableau de Carte, mais les types internes à Julia comme les tableaux et les dictionnaires ne sont généralement pas repris dans les diagrammes de types.

18-10 - Débogage

Le sous-typage peut rendre le débogage difficile. En effet, lorsqu'une fonction est appelée avec un objet comme argument, il peut être compliqué de déterminer quelle méthode est invoquée.

Supposons que nous écrivions une fonction qui manipule des objets UneMain. Idéalement, il faudrait qu'elle fonctionne avec toute sorte de type UneMain, comme PokerUneMain, BridgeUneMain, etc. Si nous invoquons une méthode comme sort!, il se pourrait que nous travaillions effectivement avec celle définie pour le type abstrait UneMain. Toutefois, s'il existe une méthode sort! ayant comme argument un des sous-types, nous manipulerons cette version plutôt que celle définie pour le type abstrait UneMain. Ce comportement est généralement sain, mais, a priori, il peut s'avérer déroutant.

1. function Base.sort!(main::UneMain)

2. sort!(main.cartes)

3. end

Lorsqu'on n'est pas sûr de la manière dont l'exécution d'un programme procède, la solution la plus simple consiste à ajouter des instructions d'affichage au début des méthodes concernées. Si shuffle! imprime un message tel que « Running shuffle! Paquet », cela signifie que le programme est exécuté correctement.

Une meilleure manière de pratiquer consiste à utiliser la macro @which :

1. julia> @which sort!(main)

2. sort!(main::UneMain) in Main at REPL[5]:1

Donc, la méthode sort! associée à main a pour argument un objet de type UneMain.

À ce stade, voici une suggestion relative à la conception d'un programme. Lorsque vous passez outre une méthode, l'interface de la nouvelle méthode doit être la même que l'interface de l'ancienne. Elle devrait prendre les mêmes paramètres, retourner le même type et obéir aux mêmes conditions a priori et a posteriori. Si vous suivez cette règle, vous constaterez que toute fonction conçue pour utiliser une instance d'un supertype (comme EnsembleDeCartes) fonctionnera également avec des instances de ses sous-types (comme Paquet et UneMain).

Si vous enfreignez cette règle, connue comme le « principe de substitution de Liskov », votre code s'effondrera comme un château de cartes.

La fonction supertype peut être utilisée pour trouver le supertype direct d'un type.

1. julia> supertype(Paquet)

2. EnsembleDeCartes

18-11 - Encapsulation de données

Les chapitres 15 et 16 présentent un plan de développement qui pourrait s'appeler « conception orientée type ». Nous avons identifié les objets à traiter — comme Point, Rectangle et MyTime — et nous avons défini des structures pour les représenter. Dans chaque cas, il existait une correspondance évidente entre l'objet et une entité du monde réel (du moins, sa représentation mathématique).

Ceci étant, il est parfois moins facile de déterminer les objets dont nous avons besoin et comment ils doivent interagir. S'il en va ainsi, il faut un plan de développement mieux adapté que la conception orientée type. De la même manière que nous avons découvert les interfaces de fonctions par encapsulation et généralisation, nous allons découvrir les interfaces de types par encapsulation de données.

L'analyse de Markov (section 13.8) fournit un bon exemple. Supposons que vous téléchargiez le code sous ce lien. Il est facile de constater que deux variables, suffixes et prefix, sont utilisables en lecture et écriture par plusieurs fonctions.

1. suffixes = Dict()

2. prefix = []

Du fait que ces variables sont globales, nous ne pouvons effectuer qu'une seule analyse à la fois. S'il arrivait que deux textes soient lus, leurs préfixes et suffixes seraient ajoutés aux mêmes structures de données (ce qui produirait un texte intéressant).

Pour effectuer plusieurs analyses séparément, il convient d'encapsuler l'état de chaque analyse dans un objet. Voici comment procéder :

1. struct Markov

2. order :: Int64

3. suffixes :: Dict{Tuple{String,Vararg{String}}, Array{String, 1}}

4. prefix :: Array{String, 1}

5. end

7. function Markov(order::Int64=2)

8. new(order, Dict{Tuple{String,Vararg{String}}, Array{String, 1}}(), Array{String, 1}())

9. end

Ensuite, les fonctions sont transformées en méthodes. Par exemple, voici processword :

1. function processword(markov::Markov, word::String)

2. if length(markov.prefix) < markov.order

3. push!(markov.prefix, word)

4. return

5. end

6. get!(markov.suffixes, (markov.prefix...,), Array{String, 1}())

7. push!(markov.suffixes[(markov.prefix...,)], word)

8. popfirst!(markov.prefix)

9. push!(markov.prefix, word)

10. end

La transformation conduisant à un programme comme celui-ci — à savoir, changer la conception sans modifier le comportement — constitue un autre exemple de refonte (voir la section 4.7).

Cet exemple propose un plan de développement pour la conception des types :

- commencer par écrire des fonctions qui lisent et écrivent des variables globales (si nécessaire) ;

- une fois le programme opérationnel, rechercher les associations entre les variables globales et les fonctions qui les utilisent ;

- encapsuler les variables afférentes sous forme de champs d'une structure composite ;

- transformer les fonctions associées en méthodes avec comme argument les objets du nouveau type.

18-11-1 - Exercice 18-3

Téléchargez le code Markov sous ce lien. Suivez les étapes décrites ci-dessus pour encapsuler les variables globales comme attributs d'une nouvelle structure appelée Markov.

18-12 - Glossaire

encoder représenter un ensemble de valeurs à l'aide d'un autre ensemble de valeurs en établissant une correspondance entre elles.

test unitaire moyen normalisé de tester l'exactitude d'un code.

placage méthode (ou fonction) qui fournit une interface différente à une autre fonction sans ajouter de calculs supplémentaires.

sous-typage possibilité de définir une hiérarchie de types apparentés.

type abstrait type qui peut agir comme parent pour un autre type.

type spécifique type qui peut être construit à partir d'un type abstrait.

sous-type type qui a comme parent un type abstrait.

supertype type abstrait, parent d'un autre type.

relation IS-A relation entre un sous-type et son supertype.

relation HAS-A relation entre deux types où les instances d'un type contiennent des références aux instances de l'autre.

dépendance relation entre deux types où les instances d'un type utilisent des instances d'un autre type, mais ne les enregistrent pas en tant que champs.

diagramme de type diagramme qui montre les types d'un programme et les relations entre eux.

multiplicité notation dans un diagramme de types qui montre, pour une relation HAS-A, combien de références existent à des instances d'une autre classe.

encapsulation de données plan de développement d'un programme qui comprend un prototype utilisant des variables globales et une version finale transformant les variables globales en champs d'instance.

18-13 - Exercices

18-13-1 - Exercice 18-4

Pour le programme suivant, dessinez un diagramme de types qui décrit ces types et les relations entre eux.

1. abstract type PingPongParent end

3. struct Ping <: PingPongParent

4. pong :: PingPongParent

5. end

7. struct Pong <: PingPongParent

8. pings :: Array{Ping, 1}

9. function Pong(pings=Array{Ping, 1}())

10. new(pings)

11. end

12. end

14. function addping(pong::Pong, ping::Ping)

15. push!(pong.pings, ping)

16. nothing

17. end

19. pong = Pong()

20. ping = Ping(pong)

21. addping(pong, ping)

18-13-2 - Exercice 18-5

Rédigez une méthode appelée deal! qui prend trois paramètres : Paquet, le nombre de mains et le nombre de cartes par main. Elle doit créer le nombre ad hoc d'objets UneMain, distribuer le nombre approprié de cartes par main et retourner un tableau de UneMain.

18-13-3 - Exercice 18-6

Voici les mains possibles au poker (avec cinq cartes), par ordre décroissant de valeur et par ordre croissant de probabilité (voir le classement des mains au poker) :

Quinte flush royale il s'agit d'une suite de la même couleur (♠, ♥, ♦, ou ♣) allant du 10 à l'as. C'est la combinaison la plus forte au poker et d'une grande rareté. Par exemple : A♥, R♥, D♥, V♥, 10♥ ;

Quinte flush toute quinte de la même couleur (♠, ♥, ♦, ou ♣) inférieure à la quinte flush royale. Par exemple : 9♠, 8♠, 7♠, 6♠, 5♠ ;

Carré toute combinaison de quatre cartes identiques de même rang. Si deux joueurs ont le même carré en même temps (ce qui veut dire que le carré est déjà sur la table), c'est le joueur avec la plus forte carte en main (le kicker) qui remporte le pot. Si la cinquième plus forte carte possible est aussi la cinquième du tapis, il y a égalité et le pot est partagé. Par exemple : 4♠, 4♥, 4♦, 4♣, R♥ ;

Full house un full house (« main pleine » en français) est l'association d'un brelan et d'une paire, c'est-à-dire trois cartes identiques de n'importe quelle couleur et deux autres cartes identiques à côté. On évalue toujours la force du brelan en premier pour comparer deux full. Par exemple : A♥, A♠, A♣, R♠, R♥.

Couleur (flush en anglais) toute combinaison de cinq cartes de la même couleur (qui ne se suivent pas, sinon c'est une quinte flush). La carte la plus haute de la couleur détermine sa force. L'exemple montre une « couleur hauteur as », la plus forte couleur possible. Si deux joueurs ont une couleur de la même hauteur, on compare alors leur deuxième carte de la couleur la plus forte et ainsi de suite. Exemple : A♠, 10♠, 7♠, 6♠, 2♠ ;

Quinte suite de cinq cartes consécutives qui ne sont pas toutes de la même couleur. Les as peuvent tout autant compter pour une quinte basse (A-2-3-4-5) qu'on appelle aussi la roue, ou pour une quinte haute (10-V-D-R-A). Plus la quinte est haute, plus elle est forte. Exemple : 5♣, 4♦, 3♠, 2♥, A♥ ;

Brelan trois cartes identiques. L'exemple montre un brelan d'as, avec un roi et une dame en kickers (cartes accompagnantes), soit le meilleur brelan possible : A♥, A♠, A♣, R♠, D♥ ;

Double paire deux cartes identiques, accompagnées de deux autres cartes identiques. L'exemple montre la meilleure combinaison de deux paires possible, une double paire as — rois. Lorsqu'on compare une double paire, on commence toujours par la paire la plus forte. Ainsi, une double paire A-A-5-5 est plus forte qu'une double paire R-R-V-V. Exemple : A♠, A♥, R♥, R♠, D♦ ;

Paire 2 cartes de rang identique. L'exemple montre la meilleure main possible à une paire : A♥, A♣, R♥, D♠, V♦.

Hauteur toute main qui ne possède aucune des combinaisons citées ci-dessus. Dans ce cas, on évalue la main par rapport à sa carte la plus forte. Par exemple, une main R-V-9-4-2 (qui ne sont pas de la même couleur) est une « hauteur roi ». Si deux joueurs ont la même « hauteur », on regarde ensuite la deuxième carte la plus forte. Exemple : A♥, R♥, D♦, V♣, 9♠.

L'objectif de cet exercice est d'estimer la probabilité de ces différentes mains.

1. Ajoutez les méthodes appelées haspair, hastwopair, etc. qui retournent true ou false selon que la main répond ou non aux critères pertinents. Votre code devrait fonctionner correctement pour des « mains » qui contiennent un nombre quelconque de cartes (bien que cinq et sept soient les configurations les plus courantes).

2. Rédigez une méthode appelée classify qui détermine le classement de plus haute valeur pour une main et définissez le champ label en conséquence. Par exemple, si une main de sept cartes contenait une « flush » et une « paire », elle devait être étiquetée « flush ».

3. Lorsque vous êtes convaincu que vos méthodes de classement fonctionnent, l'étape suivante consiste à estimer les probabilités des différentes mains. Rédigez une fonction qui mélange un jeu de cartes, le divise en mains, classe les mains et compte le nombre de fois que les différents classements apparaissent.

4. Imprimez un tableau des classements et de leurs probabilités. Exécutez votre programme avec un nombre de mains de plus en plus important jusqu'à ce que les valeurs de sortie convergent vers un degré de précision raisonnable. Comparez vos résultats aux valeurs sur classement des mains au poker ou Hand Ranking (EN).

19 - Bonus : à propos de la syntaxe

Un des objectifs de ce livre a été d'enseigner l'essentiel de Julia. Lorsque deux façons de procéder coexistaient, l'une a été choisie tout en évitant de mentionner l'autre. Parfois, la seconde méthode faisait partie d'un exercice.

À présent, revenons sur quelques éléments que nous avons négligés. Julia fournit un certain nombre de fonctionnalités pas toujours nécessaires. Il est possible d'écrire un bon code sans elles, mais leur usage permet parfois d'écrire un code plus concis, plus lisible, plus efficace. Ces trois qualités peuvent même se conjuguer.

Ce chapitre et le suivant abordent les éléments laissés en suspens dans les chapitres précédents :

- des suppléments relatifs à la syntaxe ;

- les fonctions, types et macros directement disponibles dans Base ;

- les fonctions, types et macros de la bibliothèque standard (Standard Library).

19-1 - Tuples nommés

On peut désigner les composants d'un tuple en lui attribuant un nom :

1. julia> x = (a=1, b=1+1)

2. (a = 1, b = 2)

3. julia> x.a

4. 1

Avec les tuples nommés, il devient possible d'accéder aux champs par leur nom en utilisant la syntaxe par point (x.a).

19-2 - Fonctions

En Julia, les fonctions peuvent être définies à l'aide d'une syntaxe compacte :

1. julia> f(x,y) = x + y

2. f (generic function with 1 method)

19-2-1 - Fonctions anonymes

Une fonction peut être définie de manière non nominative :

1. julia> x -> x^2 + 2x - 1

2. #1 (generic function with 1 method)

3. julia> function (x)

4. x^2 + 2x - 1

5. end

6. #3 (generic function with 1 method)

Ces exemples correspondent à des fonctions « anonymes ». Les fonctions anonymes sont souvent utilisées comme argument pour une autre fonction :

1. julia> using Plots

2. julia> plot(x -> x^2 + 2x - 1, 0, 10, xlabel="x", ylabel="y")

La figure 19.2.1 montre le résultat de ces instructions.

FIGURE 19.2.1 – Graphique de la fonction anonyme x-> x² + 2x -1 avec Plots.

19-2-2 - Arguments nommés

Les arguments de fonctions peuvent également être nommés :

1. julia> function myplot(x, y; style="solid", width=1, color="black")

2. ###

3. end

4. myplot (generic function with 1 method)

5. julia> myplot(0:10, 0:10, style="dotted", color="blue")

Les arguments nommés dans une fonction sont spécifiés après un point-virgule dans la signature, mais peuvent être appelés avec une virgule.

19-2-3 - Fermetures

Une fermeture est une technique qui permet à une fonction de capturer une variable définie en dehors du champ d'application de la fonction.

1. julia> foo(x) = ()->x

2. foo (generic function with 1 method)

4. julia> bar = foo(1)

5. #1 (generic function with 1 method)

7. julia> bar()

8. 1

Dans cet exemple, la fonction foo retourne une fonction anonyme qui a accès à l'argument x de la fonction foo. bar pointe sur la fonction anonyme et retourne la valeur de l'argument de foo.

19-3 - Blocs

Un bloc est un moyen de regrouper un certain nombre de déclarations. Un bloc commence par le mot-clé begin et se termine par end.

La macro @svg a été présentée dans le chapitre 4 :

1. 🐢 = Turtle()

2. @svg begin

3. forward(🐢, 100)

4. turn(🐢, -90)

5. forward(🐢, 100)

6. end

Dans cet exemple, la macro @svg possède un seul argument, c'est-à-dire un bloc regroupant trois appels de fonction.

19-3-1 - Bloc let

Un bloc let s'avère utile pour créer de nouvelles associations (ou ligatures), c'est-à-dire des variables locales pointant vers des valeurs.

1. julia> x, y, z = -1, -1, -1;

3. julia> let x = 1, z

4. @show x y z;

5. end

6. x = 1

7. y = -1

8. ERROR: UndefVarError: z not defined

9. julia> @show x y z;

10. x = 1

11. y = -1

12. z = -1

Dans cet exemple, la première macro @show affiche x en local, y en global et z (indéfini) en local. Les variables globales ne sont pas touchées.

19-3-2 - Blocs do

Dans la section 14.2, nous avons montré qu'il fallait fermer les fichiers après avoir effectué les opérations d'écriture. En réalité, cela peut être exécuté automatiquement en utilisant un bloc do :

1. julia> data = "Appréciez votre liberté,\nou vous la perdrez!\n"

2. "Appréciez votre liberté,\nou vous la perdrez!\n"

3. julia> open("output.txt", "w") do fout

4. write(fout, data)

5. end

6. 47

Dans cet exemple(48), fout est le flux de fichiers utilisé pour la sortie. En termes de fonctionnement, ceci est équivalent à :

1. julia> f = fout -> begin

2. write(fout, data)

3. end

4. #3 (generic function with 1 method)

5. julia> open(f, "output.txt", "w")

6. 47

La fonction anonyme est utilisée comme premier argument de la fonction open :

1. function open(f::Function, args...)

2. io = open(args...)

3. try

4. f(io)

5. finally

6. close(io)

7. end

8. end

Un bloc do peut « capturer » des variables à l'extérieur de son périmètre d'application. Par exemple, dans l'exemple ci-dessus, la variable data de « open ... do » est capturée alors qu'elle figure comme variable globale.

19-4 - Structure de contrôle

19-4-1 - Opérateur ternaire

L'opérateur ternaire ?: est une alternative à une déclaration if-elseif lorsqu'un choix entre plusieurs valeurs d'une seule expression s'avère nécessaire.

1. julia> a = 150

2. 150

3. julia> a % 2 == 0 ? println("even") : println("odd")

4. even

L'expression précédant le signe ? désigne une condition. Si la condition est true, l'expression précédant : est évaluée, sinon l'expression suivant : est exécutée.

19-4-2 - Évaluation en court-circuit

Les opérateurs && et || effectuent une évaluation en mode court-circuit : l'argument qui les suit n'est évalué que si nécessaire pour déterminer la valeur finale.

Par exemple, une routine factorielle récursive pourrait être définie ainsi :

1. function fact(n::Integer)

2. n >= 0 || error("n ne peut pas être negatif")

3. n == 0 && return 1

4. n * fact(n-1)

5. end

19-4-3 - Tâches ou coroutines

Une tâche est une structure de contrôle qui peut passer le contrôle de manière coopérative sans retour. En Julia, une tâche peut être implémentée comme une fonction ayant en premier argument un objet Channel. Un canal est utilisé pour transmettre des valeurs de la fonction à l'énoncé qui l'appelle.

La suite de Fibonacci peut être calculée à l'aide d'une tâche.

1. function fib(c::Channel)

2. a = 0

3. b = 1

4. put!(c, a)

5. while true

6. put!(c, b)

7. (a, b) = (b, a+b)

8. end

9. end

put! enregistre les valeurs dans un objet canal et take! lit les valeurs à partir de celui-ci :

1. julia> fib_gen = Channel(fib);

3. julia> take!(fib_gen)

4. 0

5. julia> take!(fib_gen)

6. 1

7. julia> take!(fib_gen)

8. 1

9. julia> take!(fib_gen)

10. 2

11. julia> take!(fib_gen)

12. 3

Le constructeur Channel crée la tâche. La fonction fib est suspendue après chaque appel put! et reprise après take! Pour des raisons de performance, plusieurs valeurs de la séquence sont mises en mémoire tampon dans l'objet canal pendant un cycle de reprise/suspension.

Un objet canal peut également être utilisé comme itérateur :

1. julia> for val in Channel(fib)

2. print(val, " ")

3. val > 20 && break

4. end

5. 0 1 1 2 3 5 8 13 21

19-5 - Types

19-5-1 - Types primitifs

Un type spécifique (ou particulier) composé de bits est appelé un type primitif. Contrairement à la plupart des langages, Julia permet de déclarer nos propres types primitifs. Les types primitifs standard sont définis de la même manière :

1. primitive type Float64 <: AbstractFloat 64 end

2. primitive type Bool <: Integer 8 end

3. primitive type Char <: AbstractChar 32 end

4. primitive type Int64 <: Signed 64 end

Ces déclarations précisent le nombre de bits requis.

L'exemple suivant crée un type primitif Byte et un constructeur :

1. julia> primitive type Byte 8 end

3. julia> Byte(val::UInt8) = reinterpret(Byte, val)

4. Byte

5. julia> b = Byte(0x01)

6. Byte(0x01)

La fonction reinterpret sert à enregistrer les bits d'un entier non signé de huit bits (UInt8) dans l'octet.

19-5-2 - Types paramétriques

Le système de types de Julia est paramétrique, ce qui signifie que les types peuvent avoir des paramètres.

Les paramètres des types sont introduits après le nom du type, entouré d'accolades :

1. struct Point{T<:Real}

2. x::T

3. y::T

4. end

Ceci définit un nouveau type paramétrique, Point{T<:Real}, contenant deux « coordonnées » de type T, qui peut être n'importe quel type ayant Real comme supertype.

1. julia> Point(0.0, 0.0)

2. Point{Float64}(0.0, 0.0)

En plus des types composites, les types abstraits et les types primitifs peuvent également avoir un paramètre de type.

Pour des raisons de performance, il est vivement recommandé d'avoir des types spécifiques pour les champs de structure. C'est là une bonne méthode pour rendre Point à la fois rapide et flexible.

19-5-3 - Unions de types

À l'image d'une structure, une union de types est un regroupement d’objets de types différents. À la différence d'une structure, une union de types ne peut contenir qu'un seul de ses membres à la fois :

1. julia> IntOrString = Union{Int64, String}

2. Union{Int64, String}

3. julia> 150 :: IntOrString

4. 150

5. julia> "Julia" :: IntOrString

6. "Julia"

Julia permet à ses utilisateurs de tirer parti des unions, car un code efficace est produit en terme d'utilisation de la mémoire. L’accès aux champs d’une union est simplifié lorsque l'union est rendue « anonyme » au sein d’une autre structure ou d’une autre union.

19-6 - Méthodes

19-6-1 - Méthodes paramétriques

Les définitions de méthodes peuvent également comporter des paramètres de type caractérisant leur signature :

1. julia> isintpoint(p::Point{T}) where {T} = (T === Int64)

2. isintpoint (generic function with 1 method)

3. julia> p = Point(1, 2)

4. Point{Int64}(1, 2)

5. julia> isintpoint(p)

6. true

19-6-2 - Objets foncteurs

En Julia, tous les objets sont « appelables ». Ces objets « appelables » sont des foncteurs (functors), c'est-à-dire des objets qui peuvent être traités à la manière d'une fonction.

1. struct Polynomial{R}

2. coeff::Vector{R}

3. end

5. function (p::Polynomial)(x)

6. val = p.coeff[end]

7. for coeff in p.coeff[end-1:-1:1]

8. val = val * x + coeff

9. end

10. val

11. end

Pour évaluer le polynôme :

1. julia> p = Polynomial([1,10,100])

2. Polynomial{Int64}([1, 10, 100])

3. julia> p(3)

4. 931

19-7 - Constructeurs

Les types paramétriques peuvent être construits explicitement ou implicitement :

1. julia> Point(1,2)                # implicit T

2. Point{Int64}(1, 2)

3. julia> Point{Int64}(1, 2)   # explicit T

4. Point{Int64}(1, 2)

5. julia> Point(1,2.5)             # implicit T

6. ERROR: MethodError: no method matching Point(::Int64, ::Float64)

Des constructeurs internes et externes par défaut sont générés pour chaque T :

1. struct Point{T<:Real}

2. x::T

3. y::T

4. Point{T}(x,y) where {T<:Real} = new(x,y)

5. end

7. Point(x::T, y::T) where {T<:Real} = Point{T}(x,y);

x et y doivent tous deux être du même type.

Lorsque x et y ont un type différent, le constructeur extérieur peut être défini comme suit :

1. Point(x::Real, y::Real) = Point(promote(x,y)...);

La fonction promote est traitée dans la sous-section 19.8.2.

19-8 - Conversions et promotions

Julia dispose d'un système permettant de promouvoir des arguments de différentes sortes en un type commun. Bien qu'elle ne soit pas automatique, la promotion peut facilement être effectuée.

19-8-1 - Conversion

Une valeur peut être convertie d'un type vers un autre :

1. julia> x = 12

2. 12

3. julia> typeof(x)

4. Int64

5. julia> convert(UInt8, x)

6. 0x0c

7. julia> typeof(ans)

8. UInt8

Nous pouvons ajouter nos propres méthodes de conversion :

1. julia> Base.convert(::Type{Point{T}}, x::Array{T, 1}) where {T<:Real} = Point(x...)

3. julia> convert(Point{Int64}, [1, 2])

4. Point{Int64}(1, 2)

19-8-2 - Promotion

La promotion est la conversion des valeurs de types mixtes en un seul type commun :

1. julia> promote(1, 2.5, 3)

2. (1.0, 2.5, 3.0)

Les méthodes pour la fonction de promotion ne sont normalement pas directement définies. Cependant, la fonction auxiliaire promote_rule est utilisée pour spécifier les règles de promotion :

1. promote_rule(::Type{Float64}, ::Type{Int32}) = Float64

19-9 - Métaprogrammation

Le code Julia peut être représenté comme une structure de données du langage lui-même. Cela permet à un programme de transformer et de produire son propre code.

19-9-1 - Expressions

En Julia, chaque programme commence par une chaîne :

1. julia> prog = "1 + 2"

2. "1 + 2"

L'étape suivante consiste à analyser chaque chaîne de caractères en un objet appelé expression, représenté par le type Expr :

1. julia> ex = Meta.parse(prog)

2. :(1 + 2)

3. julia> typeof(ex)

4. Expr

5. julia> dump(ex)

6. Expr

7. head: Symbol call

8. args: Array{Any}((3,))

9. 1: Symbol +

10. 2: Int64 1

11. 3: Int64 2

La fonction dump affiche les objets Expr avec des annotations.

Les expressions sont construites directement avec le préfixe : suivi de parenthèses ou bien en utilisant un bloc quote :

1. julia> ex = quote

2. 1 + 2

3. end;

19-9-1-1 - eval

Julia est à même d'évaluer un objet d'expression en utilisant eval :

1. julia> eval(ex)

2. 3

Chaque module a sa propre fonction d'évaluation qui calcule les expressions dans son champ d'application.

Lorsque le nombre de recours à la fonction eval est élevé, cela signifie souvent qu'un programme est mal conçu. L'usage d'eval est considéré comme une « mauvaise pratique ».

19-9-2 - Macros

Les macros peuvent inclure le code produit dans un programme. Une macro associe un ensemble d'objets Expr directement à une expression compilée.

Voici une macro simple :

1. macro containervariable(container, element)

2. return esc(:($(Symbol(container,element)) = $container[$element])) end

Les macros sont appelées en préfixant leur nom par le symbole @. L'appel de macro @containervariable letters 1 est remplacé par :

1. :(letters1 = letters[1])

@macroexpand @containervariable letters 1 retourne cette expression, qui se révèle extrêmement utile pour le débogage.

Cet exemple illustre comment une macro accède au nom de ses arguments, ce qu'une fonction ne peut pas faire. L'expression retournée nécessite l'usage d'un échappement avec esc, du fait qu'elle doit être résolue dans l'environnement d'appel de la macro.

Pourquoi utiliser des macros ? Les macros produisent et incluent des fragments de code personnalisé pendant que l'analyse a lieu, c'est-à-dire avant que le programme complet ne soit exécuté.

19-9-3 - Fonctions générées

La macro @generated crée un code spécialisé pour les méthodes en fonction des types d'arguments :

1. @generated function square(x)

2. println(x)

3. :(x * x)

4. end

Le corps retourne une expression citée comme une macro.

Pour l'appelant, la fonction générée se comporte comme une fonction régulière :

1. julia> x = square(2);     # note: sortie après l'instruction println() dans le corps

2. Int64

3. julia> x                  # maintenant nous écrivons x

4. 4

5. julia> y = square("bon");

6. String

7. julia> y

8. "bonbon"

19-10 - Valeurs manquantes

Les valeurs manquantes peuvent être représentées via l'objet missing, qui est l'instance unique du type Missing.

Les tableaux sont susceptibles de contenir des valeurs manquantes :

1. julia> a = [1, missing]

2. 2-element Array{Union{Missing, Int64},1}:

3. 1

4. missing

Le type d'élément d'un tel tableau est Union{Missing, T} avec T le type des valeurs non manquantes.

Les fonctions de réduction retournent les valeurs manquantes lorsqu'elles sont invoquées sur des tableaux qui contiennent des valeurs manquantes :

1. julia> sum(a)

2. missing

Dans ce cas, il convient d'employer la fonction skpimissing pour passer outre les valeurs manquantes :

1. julia> sum(skipmissing([1, missing]))

2. 1

19-11 - Appel de code C et Fortran

Une quantité considérable de code est écrite en C ainsi qu'en Fortran. Réutiliser du code testé est une meilleure pratique que d'écrire sa propre version d'un algorithme. Julia peut appeler directement des bibliothèques C ou Fortran existantes en utilisant la syntaxe ccall.

Dans la section 14.6 (relative aux bases de données), nous avons introduit une interface Julia à la bibliothèque GDBM des fonctions de base de données. La bibliothèque est écrite en C. Pour fermer la base de données, un appel de fonction à close(db) doit être effectué :

1. Base.close(dbm::DBM) = gdbm_close(dbm.handle)

3. function gdbm_close(handle::Ptr{Cvoid})

4. ccall((::gdbm_close, "libgdbm"), Cvoid, (Ptr{Cvoid},), handle)

5. end

Un objet dbm a un champ handle de type Ptr{Cvoid}. Ce champ contient un pointeur C qui fait référence à la base de données. Pour fermer la base de données, la fonction C gdbm_close doit être invoquée en ayant comme seul argument le pointeur C pointant vers la base de données et aucune valeur de retour. Julia effectue cela directement avec la fonction ccall ayant comme arguments :

- un tuple consistant en un symbole contenant le nom de la fonction que nous voulons appeler : :gdbm_close et la bibliothèque partagée spécifiée sous forme de chaîne : "libgdm" ;

- le type de retour : Cvoid ;

- un tuple des types d'arguments : (Ptr{Cvoid},) ;

- les valeurs de l'argument : handle.

La cartographie complète de la bibliothèque GDBM peut être trouvée à titre d'exemple dans les sources de ThinkJulia.

19-12 - Glossaire

fermeture fonction qui saisit les variables dans sa zone de portée.

bloc let bloc créant de nouvelles associations (ou ligatures) de variables locales vers des valeurs.

fonction anonyme fonction définie de manière non nominative.

tuple nommé tuple dont les composants sont nommés.

arguments nommés arguments identifiables par leur nom, c'est-à-dire pas seulement par leur position.

bloc do construction syntaxique utilisée pour définir et appeler une fonction anonyme similaire à un bloc de code usuel.

opérateur ternaire opérateur de flux de contrôle prenant trois opérandes : une condition, une expression à exécuter lorsque la condition retourne true et une autre expression à exécuter lorsque la condition retourne false.

évaluation en court-circuit évaluation d'un opérateur booléen pour lequel le deuxième argument est exécuté ou évalué, uniquement si le premier argument ne suffit pas à déterminer la valeur de l'expression.

tâche (ou coroutine) fonction de contrôle du flux qui permet la suspension et la reprise des calculs de manière flexible.

type primitif type spécifique dont les données sont constituées de simples bits.

type union type qui inclut comme objets toutes les instances de l'un ou l'autre de ses paramètres de type.

type paramétrique type qui possède des paramètres (autrement dit : type paramétré).

foncteur objet avec une méthode associée lui permettant d'être invoqué.

conversion procédé permettant de convertir une valeur d'un type en un autre.

promotion conversion des valeurs de types différents en un seul type commun.

expression type Julia qui contient une construction linguistique.

macro procédé permettant d'inclure du code généré dans le corps final d'un programme.

fonctions générées fonctions capables de produire un code spécialisé selon les types d'arguments.

valeurs manquantes instances qui représentent des données sans qu'une valeur leur soit attribuée.

20 - Bonus : bibliothèque de base et standard

Outre un environnement en développement permanent, Julia est livrée — de base — avec de nombreux outils. Le module de base contient les fonctions, les types et les macros les plus utiles. Julia fournit également un grand nombre de modules spécialisés dans sa bibliothèque standard (dates, calcul distribué, algèbre linéaire, profilage, nombres aléatoires, etc.). Les fonctions, types et macros définis dans la bibliothèque standard doivent être importés avant d'être utilisés :

- import Module importe le module souhaité et Module.fn(x) appelle la fonction fn ;

- using Module importe toutes les fonctions, types et macros du Module.

Des fonctionnalités supplémentaires sont ajoutées à partir d'une collection croissante de paquets (voir Julia Observer).

Ce chapitre ne remplace pas la documentation officielle de Julia. Ne sont cités que quelques exemples pour illustrer ce qui est possible sans toutefois être exhaustif. Les fonctions déjà introduites ailleurs ne sont pas incluses. Une vue d'ensemble complète est disponible sur Julia Documentation.

20-1 - Mesures de performance

Nous avons vu que certains algorithmes sont plus performants que d'autres. La fonction fibonacci en section 11.6 (Mémos) est beaucoup plus rapide que fib écrite en section 6.7. La macro @time permet de quantifier la différence :

1. julia> fib(1)

2. 1

3. julia> fibonacci(1)

4. 1

5. julia> @time fib(40)

6. 0.567546 seconds (5 allocations: 176 bytes)

7. 102334155

8. julia> @time fibonacci(40)

9. 0.000012 seconds (8 allocations: 1.547 KiB)

10. 102334155

@time affiche le temps d'exécution de la fonction, le nombre d'allocations et la mémoire allouée avant de retourner le résultat. La version « mémo » est effectivement beaucoup plus rapide, mais elle requiert davantage de mémoire.

« Rien n'est gratuit ».

En Julia, lors de sa première exécution, une fonction est compilée. La comparaison de deux algorithmes requiert que ceux-ci soient être implémentés en tant que fonctions pour être compilés et la première fois qu'ils sont appelés doit être exclue de la mesure de performance, sinon le temps de compilation est pris en compte. Le paquet BenchmarkTools fournit la macro @btime qui permet de faire de l'analyse de performance de la bonne manière. Utilisez-le.(49)

20-2 - Collections et structures de données

Dans la section 13.6 (Soustraction de dictionnaires), des dictionnaires ont été utilisés pour trouver les mots qui apparaissent dans un document, mais pas dans un tableau de mots. La fonction que nous avons écrite prend d1 contenant les mots du document comme clés et d2 qui renferme le tableau de mots. Elle retourne un dictionnaire qui contient les clés de d1 absentes dans d2.

1. function subtract(d1, d2)

2. res = Dict()

3. for key in keys(d1)

4. if key ∉ keys(d2)

5. res[key] = nothing

6. end

7. end

8. res

9. end

Dans tous ces dictionnaires, les valeurs sont nothing parce qu'elles ne sont jamais utilisées. Par conséquent, nous gaspillons un peu d'espace de stockage.

Julia propose un autre type interne appelé « ensemble ». Ce type se comporte comme un ensemble de clés de dictionnaire sans valeurs. L'ajout d'éléments à un ensemble est rapide, tout comme la vérification d'appartenance à un ensemble. Les ensembles fournissent des fonctions et des opérateurs pour y effectuer des opérations courantes.

Par exemple, la soustraction d'un ensemble est disponible sous la forme d'une fonction appelée setdiff. Nous pouvons donc réécrire la soustraction comme suit :

1. function subtract(d1, d2)

2. setdiff(d1, d2)

3. end

Le résultat est un ensemble au lieu d'un dictionnaire.

Certains des exercices de ce livre peuvent être réécrits de manière concise et efficace avec des ensembles. Par exemple, voici une solution pour hasduplicates, de l'exercice 10.15.7 qui utilise un dictionnaire :

1. function hasduplicates(t)

2. d = Dict()

3. for x in t

4. if x ∈ d

5. return true

6. end

7. d[x] = nothing

8. end

9. false

10. end

Lorsqu'un élément apparaît pour la première fois, il est ajouté au dictionnaire. Si le même élément apparaît à nouveau, la fonction retourne true.

En utilisant des ensembles, la même fonction peut être réécrite comme ceci :

1. function hasduplicates(t)

2. length(Set(t)) < length(t)

3. end

Un élément ne peut apparaître qu'une seule fois dans un ensemble. Ainsi, si un élément apparaît plus d'une fois dans t, l'ensemble sera plus petit que t. S'il n'y a pas de répétitions d'élément, l'ensemble aura la même taille que t.

Nous pouvons également utiliser des ensembles pour faire certains des exercices du chapitre 9. Par exemple, voici une version d'usesonly (section 9.3) avec une boucle :

1. function usesonly(word, available)

2. for letter in word

3. if letter ∉ available

4. return false

5. end

6. end

7. true

8. end

usesonly vérifie si toutes les lettres contenues dans word se trouvent dans available. Cette fonction peut être réécrite ainsi :

1. function usesonly(word, available)

2. Set(word) ⊆ Set(available)

3. end

L'opérateur (\subseteq TAB) vérifie si un ensemble est inclus dans un autre, en ce compris la possibilité qu'ils soient égaux. Dans ce dernier cas, cela signifie que toutes les lettres de word apparaissent dans available.

20-2-1 - Exercice 20-1

Réécrivez la fonction avoids (section 9.3) avec les ensembles.

20-3 - Mathématiques

Les nombres complexes sont pris en charge par Julia. La constante globale im est liée au nombre complexe i (avec ).

L'identité d'Euler est vérifiable :

1. julia> e^(im*𝜋)+1

2. 0.0 + 1.2246467991473532e-16im

Le symbole (\euler TAB) est la base des logarithmes naturels.

Illustrons le caractère complexe des fonctions trigonométriques :

Nous pouvons tester cette formule pour différentes valeurs de x.

1. julia> x = 0:0.1:2𝜋

2. 0.0:0.1:6.2

3. julia> cos.(x) == 0.5*(e.^(im*x)+e.^(-im*x))

4. true

Ceci est un autre exemple d'application de l'opérateur « point ». Julia permet également de juxtaposer des constantes numériques avec des identificateurs sous forme de coefficients comme dans 2π.

20-4 - Chaînes

Dans les chapitres 8 et 9, nous avons mené quelques recherches élémentaires dans les objets de type String. Cependant, Julia gère des expressions rationnelles compatibles avec le langage Perl. Ceci facilite la recherche de motifs complexes dans les chaînes de caractères.

La fonction usesonly peut être mise en œuvre comme une expression rationnelle :

1. function usesonly(word, available)

2. r = Regex("[^$(available)]")

3. !occursin(r, word)

4. end

L'expression régulière recherche un caractère qui n'est pas dans la chaîne available et occursin retourne true si le motif est trouvé dans le mot.

1. julia> usesonly("bonbon", "bno")

2. true

3. julia> usesonly("bonbons", "bno")

4. false

Les expressions rationnelles peuvent également être construites comme des chaînes de caractères non normalisées préfixées par un r :

1. julia> match(r"[^bno]", "bonbon")

3. julia> m = match(r"[^bno]", "bonbons")

4. RegexMatch("s")

Dans ce cas, l'interpolation de chaînes n'est pas autorisée. La fonction match ne retourne rien si le motif (une commande) n'est pas trouvé et retourne un objet RegexMatch dans le cas contraire.

Nous pouvons extraire les informations suivantes d'un objet RegexMatch :

- la sous-chaîne entière correspondant : m.match ;

- les sous-chaînes capturées comme un tableau de chaînes de caractères : m.captures ;

- le décalage auquel commence l'ensemble de la correspondance : m.offset ;

- les décalages des sous-chaînes capturées sous la forme d'un tableau : m.offsets.

1. julia> m.match

2. "s"

3. julia> m.offset

4. 7

Les expressions rationnelles constituent un outil très puissant. La page de manuel de Perl fournit tous les détails pour mener à bien des recherches très poussées, voire sophistiquées.

20-5 - Tableaux

Dans le chapitre 10, nous avons utilisé l'objet Array (tableau) comme conteneur unidimensionnel avec des indices permettant de retrouver ses éléments. Julia manipule aussi les tableaux multidimensionnels.

Créons une matrice de 2 par 3 contenant des zéros :

1. julia> z = zeros(Float64, 2, 3)

2. 2×3 Array{Float64,2}:

3. 0.0 0.0 0.0

4. 0.0 0.0 0.0

5. julia> typeof(z)

6. Array{Float64,2}

Le type de cette matrice est un tableau à deux dimensions contenant des nombres à virgule flottante.

La fonction size renvoie un tuple décrivant le nombre d'éléments dans chaque dimension :

1. julia> size(z)

2. (2, 3)

La fonction ones construit une matrice avec des éléments de valeur 1 :

1. julia> s = ones(String, 1, 3)

2. 1×3 Array{String,2}:

3. ""   ""   ""

L'élément unitaire de chaîne de caractères est une chaîne vide, "".

s n'est pas un tableau unidimensionnel :

1. julia> s == ["", "", ""]

2. false

s est une matrice à une ligne, tandis que ["", "", ""] est une matrice à une colonne.

Une matrice peut être saisie directement en utilisant un espace pour séparer les éléments d'une ligne et un point-virgule ; pour séparer les lignes :

1. julia> a = [1 2 3; 4 5 6]

2. 2×3 Array{Int64,2}:

3. 1 2 3

4. 4 5 6

Pour traiter des éléments individuels, des crochets sont utilisables :

1. julia> z[1,2] = 1

2. 1

3. julia> z[2,3] = 1

4. 1

5. julia> z

6. 2×3 Array{Float64,2}:

7. 0.0 1.0 0.0

8. 0.0 0.0 1.0

La sélection d'un sous-groupe d'éléments peut être réalisée par segmentation :

1. julia> u = z[:,2:end]

2. 2×2 Array{Float64,2}:

3. 1.0 0.0

4. 0.0 1.0

L'opérateur . effectue une distribution sur chaque élément dans toutes les dimensions :

1. julia> e.^(im*u)

2. 2×2 Array{Complex{Float64},2}:

3. 0.540302+0.841471im            1.0+0.0im

4. 1.0+0.0im            0.540302+0.841471im

20-6 - Interfaces

Julia tire parti de certaines interfaces informelles pour définir des comportements, c'est-à-dire des méthodes ayant un objectif spécifique. Lorsque ces méthodes sont étendues à un type, des objets de ce type peuvent être utilisés pour construire ces comportements.

« Si ça ressemble à un canard, si ça nage comme un canard et si ça cancane comme un canard, c'est un canard. »

Dans la section 6.7, nous avons mis en œuvre la fonction fib qui retourne le nième élément de la suite de Fibonacci. La recherche des valeurs d'une suite constitue une de ces interfaces. Créons un itérateur qui retourne la suite de Fibonacci :

1. struct Fibonacci{T<:Real} end

2. Fibonacci(d::DataType) = d<:Real ? Fibonacci{d}() : error("No Real type!")

4. Base.iterate(::Fibonacci{T}) where {T<:Real} = (zero(T), (one(T), one(T)))

5. Base.iterate(::Fibonacci{T}, state::Tuple{T, T}) where {T<:Real} = (state[1], (state[2], state[1] + state[2]))

Nous avons mis en œuvre un type paramétrique sans champ Fibonacci, un constructeur extérieur et deux méthodes iterate. La première est appelée pour initialiser l'itérateur et retourne un tuple composé de la première valeur (0) et d'un état. L'état est un tuple contenant la deuxième et la troisième valeur : 1 et 1.

La seconde itération est appelée pour obtenir la valeur suivante de la séquence de Fibonacci et retourne un tuple avec comme premier élément la valeur suivante et comme second élément un état qui consiste en un tuple avec les deux valeurs suivantes.

À ce stade, nous pouvons appeler Fibonacci dans une boucle for :

1. julia> for e in Fibonacci(Int64)

2. e > 100 && break

3. print(e, " ")

4. end

5. 0 1 1 2 3 5 8 13 21 34 55 89

Cela semble magique, mais l'explication est simple. Une boucle for en Julia :

1. for i in iter

2. # corps de la boucle

3. end

est convertie en :

1. next = iterate(iter)

2. while next !== nothing

3. (i, state) = next

4. # corps de la boucle

5. next = iterate(iter, state)

6. end

C'est là un très bon exemple de la façon dont une interface bien conçue permet à une implémentation d'utiliser toutes les fonctions disponibles via cette interface.

20-7 - Utilitaires interactifs

Nous avons déjà rencontré le module InteractiveUtils dans la section 18.10. La macro @which n'est que la partie émergée de l'iceberg.

Le code Julia est transformé par la bibliothèque LLVM (Low Level Virtual Machine) en code machine en plusieurs étapes. Nous pouvons directement visualiser la sortie de chaque étape.

Donnons un exemple simple :

1. function squaresum(a::Float64, b::Float64)

2. a^2 + b^2

3. end

La première étape consiste à examiner le code de bas niveau :

1. julia> using InteractiveUtils

3. julia> @code_lowered squaresum(3.0, 4.0)

4. CodeInfo(

5. 1 ─ %1 = (Core.apply_type)(Base.Val, 2)

6. │ %2 = (%1)()

7. │ %3 = (Base.literal_pow)(:^, a, %2)

8. │ %4 = (Core.apply_type)(Base.Val, 2)

9. │ %5 = (%4)()

10. │ %6 = (Base.literal_pow)(:^, b, %5)

11. │ %7 = %3 + %6

12. └──     return %7

13. )

La macro @code_lowered retourne sous forme d'un tableau une représentation intermédiaire du code utilisé par le compilateur pour produire du code optimisé.

L'étape suivante ajoute des informations sur le type :

1. julia> @code_typed squaresum(3.0, 4.0)

2. CodeInfo(

3. 1─ %1 = (Base.mul_float)(a, a)::Float64

4. │ %2 = (Base.mul_float)(b, b)::Float64

5. │ %3 = (Base.add_float)(%1, %2)::Float64

6. └──     return %3 ) => Float64

Nous observons que le type de résultats intermédiaires et la valeur de retour sont correctement déduits.

Cette représentation du code est transformée en code LLVM :

1. julia> @code_llvm squaresum(3.0, 4.0)

2. ; @ none:2 within `squaresum'

3. define double @julia_squaresum_14821(double, double) {

4. top:

5. ; ┌ @ intfuncs.jl:243 within `literal_pow'

6. ; │┌ @ float.jl:399 within `*'

7. %2 = fmul double %0, %0

8. %3 = fmul double %1, %1

9. ; └└

10. ; ┌ @ float.jl:395 within `+'

11. %4 = fadd double %2, %3

12. ; └

13. ret double %4

14. }

Finalement, le code machine est produit :

1. julia> @code_native squaresum(3.0, 4.0)

2. .section __TEXT,__text,regular,pure_instructions

3. ; ┌ @ none:2 within `squaresum'

4. ; │┌ @ intfuncs.jl:243 within `literal_pow'

5. ; ││┌ @ none:2 within `*'

6. vmulsd %xmm0, %xmm0, %xmm0

7. vmulsd %xmm1, %xmm1, %xmm1

8. ; │└└

9. ; │┌ @ float.jl:395 within `+'

10. vaddsd %xmm1, %xmm0, %xmm0

11. ; │└ retl

12. nopl      (%eax)

13. ; └

20-8 - Débogage

Les macros Logging fournissent un substitut aux canevas avec des déclarations d'affichage :

1. julia> @warn "Abandon printf debugging, all ye who enter here!"

2. ┌ Warning: Abandon printf debugging, all ye who enter here!

3. └ @ Main REPL[1]:1

Les déclarations de débogage ne doivent pas être retirées du code. Par exemple, contrairement à @warn ci-dessus, le code :

1. julia> @debug "The sum of some values $(sum(rand(100)))"

ne produira, par défaut, aucun résultat. Dans ce cas, sum(rand(100)) ne sera jamais évaluée à moins que la journalisation du débogage ne soit activée.

Le niveau de journalisation peut être sélectionné par une variable d'environnement JULIA_DEBUG :

1. $ JULIA_DEBUG=all julia -e '@debug "The sum of some values $(sum(rand(100)))"'

2. ┌ Debug: The sum of some values 47.116520814555024

3. └ @ Main none:1

En l'occurrence, nous avons utilisé all pour extraire toutes les informations de débogage. Cependant, il est possible de ne produire que les informations associées à un fichier ou à un module spécifique.

20-9 - Glossaire

regex expression rationnelle, une séquence de caractères qui définit un modèle de recherche.

matrice tableau à deux dimensions.

représentation intermédiaire structure de données utilisée en interne par un compilateur pour représenter le code source.

code machine instructions qui peuvent être exécutées directement par l'unité centrale d'un ordinateur.

enregistrement de débogage stockage des messages de débogage dans un journal.

21 - Débogage

Lors du débogage, il est nécessaire de distinguer les différents types d'erreurs afin de les repérer rapidement :

- les erreurs de syntaxe sont découvertes par le compilateur lors de la traduction à la volée du code source en code compilé. Elles indiquent une formulation erronée au sein de la structure du programme. Il en va ainsi, par exemple, de l'omission du mot-clé end à la fin d'un bloc de fonction qui engendre le message d'erreur quelque peu redondant : ERROR: LoadError: syntax: incomplete: function requires end ;

- les erreurs d'exécution sont produites par le compilateur si un dysfonctionnement apparaît au cours de l'exécution d'un programme. La plupart des messages d'erreur d'exécution contiennent des informations sur la localisation de l'erreur et les fonctions en cours d'exécution. Par exemple, une récursion infinie provoque une erreur d'exécution telle que ERROR: StackOverflowError :

- les erreurs sémantiques sont liées à un programme qui s'exécute sans produire de messages d'erreur, mais qui ne fait pas ce à quoi il est destiné. À titre d'exemple, une expression peut ne pas être évaluée dans l'ordre prévu, ce qui donne un résultat incorrect.

La première étape du débogage consiste à déterminer le type d'erreur auquel on est confronté. Bien que les sections suivantes soient organisées par type d'erreur, certaines techniques sont applicables dans plus d'une situation.

21-1 - Erreurs de syntaxe

Les erreurs de syntaxe sont généralement faciles à corriger dès qu'on a compris de quoi il s'agit. Malheureusement, d'une manière générale, les messages d'erreur ne sont pas très explicites. Les messages les plus courants sont du type ERROR: LoadError: syntax: incomplete: premature end of input et ERROR: LoadError: syntax: unexpected "=". Aucune des deux formulations n'est véritablement instructive.

En revanche, le message indique l'endroit du programme où le problème s'est produit. En fait, il indique où Julia a remarqué un problème, ce qui n'est pas nécessairement l'endroit exact où se situe l'erreur. Il n'est pas rare que celle-ci se trouve en amont de l'emplacement indiqué par le message d'erreur (souvent sur la ligne précédente).

Si vous construisez un programme de manière incrémentale, vous devriez avoir une bonne idée de l'endroit où se trouve l'erreur. Elle se cache soit dans la dernière ligne, soit dans le dernier bloc ajouté.

Si vous copiez le code depuis un livre ou une page de l'Internet, commencez par comparer très soigneusement votre code à celui contenu dans la source. Vérifiez chaque caractère. En même temps, n'oubliez pas que la source peut elle-même contenir un code erroné. Si bien que si vous voyez quelque chose qui ressemble à une erreur de syntaxe, il peut s'agir de ce dernier cas de figure.

Voici quelques moyens d'éviter les erreurs de syntaxe les plus courantes :

1. Assurez-vous que vous n'employez pas un mot-clé de Julia pour un nom de variable ;

2. Vérifiez que le mot-clé end clôture chaque déclaration composée, y compris les blocs for, while, if et les blocs de fonction ;

3. Assurez-vous que toutes les chaînes de caractères du code sont entre guillemets et que tous les guillemets sont des "droits" et non “bouclés” ;

4. Si vous avez écrit des chaînes multilignes avec des guillemets triples, assurez-vous que vous avez clôturé la chaîne correctement. Une chaîne inachevée peut provoquer une erreur « invalid token » à la fin d'un programme. En outre, elle peut traiter la partie suivante du programme comme une chaîne jusqu'à ce qu'elle parvienne à la chaîne suivante. Dans ce dernier cas, il se peut que la compilation ne produise aucun message d'erreur ;

5. Un opérateur d'ouverture sans son vis-à-vis — (, { ou [ — amène Julia à considérer la ligne suivante comme appartenant à la déclaration courante. En général, une erreur se produit presque immédiatement à la ligne suivante ;

6. Vérifiez le classique = au lieu de == à l'intérieur d'un test conditionnel ;

7. Si des caractères non ASCII se trouvent dans le code (y compris des chaînes de caractères et des commentaires), cela peut poser un problème bien que Julia gère généralement les caractères non ASCII. Soyez attentif si vous collez du texte provenant d'une page web ou d'une autre source.

Si rien de ceci ne fonctionne, passez à la section suivante.

21-1-1 - Je continue à faire des changements, mais sans effet

Si le REPL indique qu'il y a une erreur et que vous ne la détectez pas, c'est peut-être parce que vous ne considérez pas le même code que le REPL. Vérifiez votre environnement de programmation afin de déterminer si le programme que vous éditez est bien celui que Julia tente d'exécuter.

Si vous n'en êtes pas sûr, essayez d'introduire une erreur de syntaxe évidente et délibérée au début du programme. Exécutez-le à nouveau. Dans le cas où le REPL ne trouve pas la nouvelle erreur, vous pouvez conclure que vous n'exécutez pas le nouveau code.

Il y a quelques coupables potentiels :

- vous avez édité le fichier et oublié de sauvegarder les modifications avant de l'exécuter à nouveau. Certains environnements de programmation le font pour vous, d'autres non ;

- vous avez changé le nom du fichier, cependant vous utilisez toujours l'ancien nom ;

- quelque chose dans votre environnement de développement est mal configuré ;

- si vous écrivez un module et que vous l'utilisez, assurez-vous de ne pas donner à votre module le même nom qu'un des modules standard de Julia ;

- si vous employez le mot-clé using pour importer un module, n'oubliez pas qu'il est impératif de redémarrer le REPL lorsque vous modifiez le code du module. Si vous importez à nouveau le module, il ne se passe rien.

Si vous êtes bloqué et que vous ne pouvez pas comprendre ce qui se produit, une approche consiste à recommencer avec un nouveau programme comme « Hello, World! » et à vous assurer que vous pouvez faire fonctionner un programme opérationnel connu. Ensuite, ajoutez progressivement les éléments du programme d'origine au nouveau programme.

21-2 - Erreurs d'exécution

Dès que votre programme est syntaxiquement correct, Julia peut le lire et a minima commencer à l'exécuter. Qu'est-ce qui alors pourrait dysfonctionner ?

21-2-1 - Mon programme ne fait absolument rien

Ce problème est assez fréquent lorsque votre fichier se compose de fonctions et de classes sans invoquer réellement une fonction pour en lancer l'exécution. Cela peut être intentionnel si vous prévoyez d'importer ce module uniquement pour fournir des classes et des fonctions.

Si ce n'est pas le cas, assurez-vous qu'un appel de fonction est bien présent dans le programme. Ensuite, vérifiez que le flux d'exécution l'atteint bel et bien (voir le paragraphe 21.2.2.3).

21-2-2 - Mon programme est « suspendu »

Si un programme s'arrête et semble ne rien faire, il est « suspendu ». Souvent, cela signifie qu'il est engagé dans une boucle ou une récursion infinie :

- si vous soupçonnez qu'une boucle particulière est à l'origine du problème, ajoutez un message d'affichage immédiatement avant la boucle qui stipule « entrer dans la boucle » et un autre immédiatement après tel que « sortir de la boucle ».
Exécutez à nouveau le programme. Si vous obtenez le premier message et non le second, vous êtes confronté à une boucle infinie. Passez au paragraphe 21.2.2.1 ci-dessous,
la plupart du temps, une récursion infinie entraînera l'exécution du programme pendant un certain temps, puis produira une ERROR: LoadError: Error StackOverflowError. Si c'est le cas, passez au paragraphe 21.2.2.2 ci-dessous.
Si vous n'obtenez pas cette erreur quoique vous soupçonniez un problème avec une méthode ou une fonction récursive, vous pouvez toujours utiliser les techniques du paragraphe 21.2.2.2 ;

- si aucune de ces étapes n'apporte de solution, commencez à tester d'autres boucles ainsi que d'autres fonctions et méthodes récursives,
en cas d'échec, il est possible que vous ne compreniez pas le flux d'exécution de votre programme. Rendez-vous au paragraphe 21.2.2.3.

21-2-2-1 - Boucle infinie

Si vous pressentez une boucle infinie et que vous pensez avoir identifié la boucle responsable du problème, ajoutez une déclaration d'affichage à la fin de la boucle, qui communique les valeurs des variables de la condition et la valeur de la condition.

Par exemple :

1. while x > 0 && y < 0

2. # modifier x

3. # modifier y

4. @debug "variables" x y

5. @debug "condition" x > 0 && y < 0

6. end

Maintenant, lorsque vous exécutez le programme en mode de débogage, vous verrez la valeur des variables et la condition pour chaque passage dans la boucle. Au dernier passage, la condition devrait être fausse. Si la boucle continue, vous pourrez visualiser les valeurs de x ainsi que d'y, et vous comprendrez peut-être pourquoi elles ne sont pas mises à jour correctement.

21-2-2-2 - Récursion infinie

La plupart du temps, une récursion infinie amène un programme à calculer pendant un certain temps pour produire une ERROR: LoadError: Error StackOverflowError.

Si vous pensez qu'une fonction provoque une récursion infinie, contrôlez la présence d'un cas de base. Il devrait y avoir une condition amenant la fonction à effectuer un retour sans exécuter une invocation récursive. Si ce n'est pas le cas, vous devez repenser l'algorithme et identifier un cas de base.

Si un cas de base existe, mais que le programme ne semble pas l'atteindre, ajoutez une instruction d'affichage au début de la fonction, qui indique les paramètres. À présent, lorsque vous exécutez le programme, vous verrez quelques lignes de sortie chaque fois que la fonction est invoquée et vous pourrez analyser les valeurs des paramètres. Si les paramètres ne se déplacent pas vers le cas de base, vous aurez une idée des raisons pour lesquelles il en va ainsi.

21-2-2-3 - Flux d'exécution

Si vous n'êtes pas sûr de la manière dont le flux d'exécution percole dans votre programme, ajoutez un message au début de chaque fonction comme « entrer dans la fonction unetelle », où unetelle est le nom de la fonction.

À ce stade, lorsque vous exécutez le programme, celui-ci affichera une trace de chaque fonction au fur et à mesure qu'elle sera invoquée.

21-2-3 - Quand j'exécute mon programme, j'obtiens une exception

Si quelque chose capote pendant l'exécution, Julia affiche un message qui comprend le nom de l'exception, la ligne du programme où le problème a surgi et une trace de pile (stacktrace).

La trace de pile identifie la fonction en cours d'exécution, puis la fonction qui l'a appelée, celle qui à son tour a appelé cette dernière, et ainsi de suite. En d'autres termes, elle retrace la séquence des appels de fonction qui ont conduit à la situation à laquelle vous êtes confronté, y compris le numéro de ligne de votre fichier où chaque appel s'est produit.

La première étape consiste à examiner l'endroit du programme où l'erreur s'est produite et à déterminer si vous pouvez comprendre ce qui s'est passé. Il s'agit là de certaines des erreurs d'exécution les plus courantes :

ArgumentError un des arguments à un appel de fonction n'est pas dans l'état attendu ;

BoundsError une opération sur les indices dans un tableau a tenté d'accéder à un élément hors limites ;

DomainError l'argument d'une fonction ou d'un constructeur est en dehors du domaine de validité ;

DivideError une division entière a été tentée avec une valeur de dénominateur égale à 0 ;

EOFError il n'y avait plus de données disponibles à lire à partir d'un fichier ou d'un flux ;

InexactError ne peut pas exactement se convertir à un type ;

KeyError une opération d'indexation dans un objet AbstractDict (Dict) ou Set a tenté d'accéder ou de supprimer un élément inexistant ;

MethodError une méthode avec la signature requise n'existe pas dans la fonction générique donnée. Alternativement, il n'existe pas de méthode unique plus spécifique ;

OutOfMemoryError une opération a alloué trop de mémoire pour que le système ou le récupérateur de mémoire puisse la gérer correctement ;

OverflowError le résultat d'une expression est trop grand pour le type spécifié et provoque un débordement ;

StackOverflowError un appel de fonction tente d'utiliser plus d'espace que celui disponible dans la pile d'appels. Cela se produit généralement lorsqu'un appel est répété à l'infini ;

StringIndexError une erreur s'est produite lors de la tentative d'accès à un indice de chaîne invalide ;

SystemError un appel système a échoué avec un code d'erreur ;

TypeError erreur d'assertion de type, ou erreur produite lors de l'appel d'une fonction intégrée avec un type d'argument incorrect ;

UndefVarError un symbole dans l'environnement en cours n'est pas défini.

21-2-4 - J'ai ajouté beaucoup de déclarations d'affichage ; je suis inondé de sorties

L'utilisation intensive des instructions d'affichage pour le débogage se traduit régulièrement en une avalanche de messages. Deux façons d'y remédier : simplifier la sortie ou simplifier le programme.

Pour simplifier la sortie, il est pertinent de supprimer ou commenter les déclarations d'affichage qui n'apportent pas d'aide concrète, les combiner (le cas échéant) ou encore formater la sortie de manière à être plus facile à interpréter.

Pour simplifier un programme, il existe plusieurs approches. Tout d'abord, réduisez le problème sur lequel le programme travaille. Par exemple, si vous traitez une liste, testez une liste courte. Si le programme capte diverses données de l'utilisateur, donnez-lui l'information la plus élémentaire qui cause le problème.

Ensuite, nettoyez le programme. Supprimez le code mort et réorganisez le programme pour le rendre aussi lisible que possible. Imaginons que le problème se situe dans une partie profondément imbriquée du programme. Essayez de réécrire cette partie avec une structure allégée. Si vous présumez l'implication d'une fonction de grande taille, essayez de la diviser en fonctions de moindre taille afin de les tester séparément.

Souvent, le processus de recherche d'un test minimal vous mène au bogue. Si vous trouvez qu'un programme fonctionne dans une situation, mais pas dans une autre, cela aussi vous donne un indice.

De même, réécrire un morceau de code peut aider à débusquer des bogues malicieux. Si vous apportez une modification qui, selon vous, ne devrait pas affecter le programme alors qu'il en est ainsi, vous tenez peut-être bien un indice à défaut de tenir la solution.

21-3 - Erreurs sémantiques

Les erreurs sémantiques sont les plus difficiles à déboguer par le simple fait que le compilateur à la volée ne fournit aucune information. Vous seul savez ce que le programme est censé faire.

La première étape consiste à établir un lien entre le code du programme et le comportement observé. Il faut émettre une hypothèse concernant la manière dont le programme procède. Une des causes qui rendent le dépistage des erreurs sémantiques si difficile provient de la vitesse d'exécution des ordinateurs.

On voudrait souvent pouvoir ralentir le programme jusqu'à « vitesse humaine ». L'insertion judicieuse de quelques instructions d'affichage est souvent plus fructueuse et plus rapide que la mise en place d'un débogueur, l'insertion et la suppression de points d'arrêt et la gestion du « pas » du programme pour converger vers l'endroit où l'erreur se produit.

21-3-1 - Mon programme ne fonctionne pas

Le moment est venu de se poser quelques questions :

- le programme était-il censé faire quelque chose qui semble ne pas se produire ? Trouvez la section du code qui remplit cette fonction et vérifiez qu'il s'exécute au moment où vous pensez qu'il devrait procéder ainsi ;

- quelque chose survient-il qui ne devrait pas se produire ? Trouvez dans votre programme le code qui remplit cette fonction et assurez-vous qu'il s'exécute au moment où il ne devrait pas ;

- une partie du code produit-elle un effet qui n'est pas celui auquel vous vous attendiez ? Assurez-vous de bien comprendre le code en question, surtout s'il s'agit de fonctions ou de méthodes associées à d'autres modules de Julia. Lisez la documentation relative aux fonctions que vous appelez. Essayez-les en écrivant des cas de test simples et en vérifiant les résultats.

Pour programmer, vous devez disposer d'un modèle mental du fonctionnement des programmes. Si vous écrivez un programme qui n'exécute pas ce que vous attendez, souvent, le problème ne se situe pas dans le programme, mais dans votre modèle mental.

La meilleure façon de corriger votre modèle mental est de fractionner le programme en ses composantes (généralement les fonctions et les méthodes) et de tester chacune d'elles indépendamment. Une fois que vous avez trouvé la distorsion entre votre modèle et la réalité, vous êtes en mesure de résoudre le problème.

Bien entendu, vous devez construire et tester les composants au fur et à mesure que vous développez le programme. Si vous rencontrez un problème, il ne devrait y avoir qu'une petite quantité de nouveau code susceptible d'être incorrect.

21-3-2 - J'ai une expression embroussaillée qui ne fait pas ce que j'attends

L'écriture d'expressions complexes reste une bonne pratique pour autant qu'elles restent lisibles. Néanmoins, elles sont souvent difficiles à déboguer. En général, il est judicieux de décomposer une expression complexe en une série d'instructions comportant des variables temporaires.

Par exemple, l'instruction :

1. addcard(game.hands[i], popcard(game.hands[findneighbor(game, i)]))

peut être reformulée en :

1. neighbor = findneighbor(game, i)

2. pickedcard = popcard(game.hands[neighbor])

3. addcard(game.hands[i], pickedcard)

La version explicite est nettement plus facile à lire parce que les noms des variables fournissent une documentation en eux-mêmes. Elle est aussi plus facile à déboguer, car il est possible de vérifier les types des variables intermédiaires et d'afficher leur valeur.

Un autre problème susceptible de survenir avec les expressions de grande taille vient de l'ordre d'évaluation qui n'est peut-être pas celui auquel on s'attend a priori. Par exemple, l'expression pourrait être écrite en Julia comme ceci :

1. y = x / 2 * π

Toutefois, cette formulation est erronée, car la multiplication et la division ayant la même priorité sont évaluées de gauche à droite. Cette expression calcule donc .

Une bonne manière de déboguer les expressions consiste à ajouter des parenthèses pour rendre l'ordre d'évaluation explicite :

1. y = x / (2 * π)

Lorsque vous n'êtes pas sûr de l'ordre d'évaluation, utilisez des parenthèses. Non seulement le programme sera correct (dans le sens où il s'exécutera selon votre intention), mais, de surcroît, il sera lisible pour ceux qui n'ont pas mémorisé l'ordre des opérations.

21-3-3 - J'ai une fonction qui ne retourne pas ce que j'attends

Si vous écrivez une déclaration de retour avec une expression complexe, vous vous privez de la possibilité d'afficher la valeur de retour avant que l'instruction return soit exécutée. Là encore, il est astucieux de tirer parti d'une variable temporaire. Par exemple, au lieu de :

1. return removematches(game.hands[i])

il est approprié d'écrire :

1. count = removematches(game.hands[i])

2. return count

Ainsi, devient-il possible d'afficher la valeur de count avant que l'instruction return ne soit exécutée.

21-3-4 - Je suis vraiment, vraiment coincé et j'ai besoin d'aide

D'abord, essayez de vous éloigner de l'ordinateur pendant quelques minutes. Travailler avec un ordinateur peut provoquer ces symptômes :

- frustration et rage ;

- croyances superstitieuses (« l'ordinateur me déteste ») et pensées magiques (« le programme ne fonctionne que lorsque je porte ma casquette à l'envers ») ;

- tenter la programmation aléatoire (c'est-à-dire programmer en écrivant tous les programmes possibles et en choisissant celui qui procède de manière adéquate).

Si vous souffrez de l'un de ces symptômes, levez-vous et allez faire une promenade. Lorsque vous reviendrez au calme, pensez au programme. Que fait-il ? Quelles sont les causes possibles de son comportement ? Quand avez-vous vu le programme fonctionner correctement la dernière fois, qu'avez-vous fait ensuite ?

Parfois, il faut juste du temps pour trouver un bogue. Les très bons programmeurs trouvent régulièrement des bogues quand ils se trouvent loin de leur(s) ordinateur(s) et qu'ils laissent leur esprit vagabonder. Les meilleurs endroits pour trouver des bogues sont les trains, les douches et le lit, juste avant de s'endormir.

21-3-5 - Non, j'ai vraiment besoin d'aide

Cela arrive. Même les meilleurs programmeurs se retrouvent occasionnellement bloqués. Parfois, vous travaillez sur un programme depuis si longtemps que vous ne pouvez plus voir l'erreur. Vous avez besoin d'un regard neuf.

Avant de faire appel à quelqu'un d'autre, assurez-vous que vous êtes prêt. Votre programme doit être aussi simple que possible et vous devez travailler sur la plus petite zone possible qui déclenche l'erreur. Vous devez avoir sous la main les instructions d'affichage aux endroits appropriés (et le résultat qu'elles produisent doit être compréhensible). Vous devez comprendre le problème suffisamment bien pour le décrire de manière concise et précise.

Lorsque vous appelez à l'aide, veillez donner les informations nécessaires :

- s'il y a un message d'erreur, de quoi s'agit-il et quelle partie du programme indique-t-il ?

- quelle est la dernière modification insérée avant que cette erreur se produise ?

- quelles sont les dernières lignes de code introduites, quelle est la nouvelle batterie de tests qui échoue ?

- qu'avez-vous essayé jusque-là et qu'en avez-vous tiré ?

Lorsque vous trouvez le bogue, prenez un peu de temps pour réfléchir à ce que vous auriez pu faire pour le trouver plus rapidement. La prochaine fois que vous verrez quelque chose de similaire, vous pourrez trouver le bogue plus rapidement.

N'oubliez pas que le but n'est pas seulement de faire fonctionner le programme. L'objectif fondamental est d'apprendre à faire fonctionner le programme.

Annexe A : entrées Unicode

Le tableau suivant énumère quelques caractères Unicode parmi les nombreux qui peuvent être saisis par tabulation d'abréviations de type dans le REPL de Julia (ainsi que dans d'autres environnements associés à divers éditeurs).

Caractères

Séquence de complétion

Représentation ASCII

2

\^2

1

\_1

2

\_2

🍎

\:apple:

🍌

\:banana:

🐫

\:camel:

🍐

\:pear:

🐢

\:turtle:

∩

\cap

≡

\equiv

===

e

\euler

∈

\in

in

≥

\ge

>=

≤

\le

<=

≠

\ne

!=

∉

\notin

π

\pi

pi

⊆

\subseteq

ε

\varepsilon

Annexe B : Installation de Julia

Situation au 1er janvier 2021

Depuis la publication du livre Think Julia [1], deux modifications importantes ont été décidées par les développeurs :

- l'abandon de la gratuité sans restriction de JuliaBox (voir la section B.4), qui a des conséquences notamment sur l'utilisation de JuliaBox pour des cours Julia en direct ;

- le gel du développement de l'environnement de développement intégré Atom/Juno — toujours installable et utilisable — et la migration vers Visual Studio Code ou vers Visual Studio Codium.

Ce double choix a amené le traducteur à modifier la section « Utilisation des exemples de codes » ainsi que l'introduction du chapitre 4 et à proposer la présente annexe.

Julia en mode local

Julia est installable sur MS Windows, macOS, GNU/Linux et BSD.

Le téléchargement se fait à partir du site Download Julia. L'installation pour les différentes plateformes est décrite sur Platform Specific Instructions for Official Binaries et Platform Specific Instructions for Unofficial Binaries.

Installation de binaires sous Ubuntu

Les binaires de la version stable (par exemple : julia-1.5.3) se trouvent sous ce lien. En supposant que le fichier julia-1.5.3-linux-x86_64.tar.gz ait été téléchargé dans le répertoire $HOME/Téléchargements :

1. sudo  cp  Téléchargements/julia-1.5.3-linux-x86_64.tar.gz  /usr/local

2. cd  /usr/local

3. tar  -zxvf  julia-1.5.32-linux-x86_64.tar.gz   &&  rm julia-1.5.3-linux-x86_64.tar.gz

Ensuite, il convient de se rendre dans le fichier $HOME/.bashrc et d'y ajouter(50) :

1. PATH=/usr/local/julia-1.5.3/bin:$PATH

Pour travailler avec les caractères accentués et grecs, par exemple, repérez le fichier startup.jl et ajoutez-y :(51)

1. ENV["GKS_ENCODING"] = "utf-8"

Installation de Visual Studio Codium

À côté d'éditeurs puissants comme Vim et Emacs, Visual Studio Codium est un auxiliaire de développement très répandu. Sous Ubuntu, l'installation procède ainsi :

1. sudo snap install codium

Le lancement se fait à partir de la ligne de commande ($ codium) ou par l'icône déposée sur le bureau, ce qui permet d'obtenir une fenêtre comme celle représentée à la figure B.1. Pour obtenir cette fenêtre, il convient après l'ouverture de Codium de se rendre sur l'onglet Terminal New Terminal. L'écran principal est divisé en deux parties dont un terminal bash. Derrière l'invite, saisissez julia.

FIGURE B.1 – Écran de Visual Studio Codium.

À ce stade, cliquez sur le bouton Extensions (5e bouton de la barre verticale gauche) et installez Julia 1.0.10 — Julia Language Support (ce numéro n'est pas lié au numéro de version de Julia proprement dit).

Installation de modules

Tout au long de l'utilisation de Julia, il sera nécessaire d'installer des paquets (modules) complétant la bibliothèque standard.

L'installation de nouveaux paquets se fait dans le REPL de VS Codium (Read-Eval-Print Loop) qui se trouve dans la partie inférieure de la figure B.1 (la hauteur de cet espace de travail est ajustable).

Ancienne méthode

Si, par exemple, on souhaite utiliser le paquet Plots (voir la sous-section 13.12.1 pour réaliser des graphiques), il est possible d'appliquer la méthode suivante. Dans le REPL et une session active :

1. julia> using Pkg

2. Pkg.add("Plots")

L'installation est automatique. Julia se charge d'établir les dépendances, les télécharge et les installe dans le répertoire $HOME/.julia/packages.

Nouvelle méthode

Il existe toutefois une méthode plus moderne de procéder. Dans le REPL, derrière l'invite julia>, saisissons le caractère ] (Alt-]) :

1. julia> ]

Immédiatement, Julia nous fait basculer en mode gestionnaire de paquets (en l'occurrence pour la version 1.5) :

1. (@v1.5) pkg>

Après quoi, il est possible de demander l'incorporation d'un paquet. Par exemple, pour le paquet Plots :

1. (@v1.5) pkg > add Plots

Pour quitter le gestionnaire de paquets, on peut utiliser la touche retour arrière ou la combinaison Ctrl-C.

Carnets de travail : Jupyter et Pluto

Jupyter

Le carnet Jupyter (Jupyter Notebook) est un éditeur de feuilles de calcul qui fonctionne en local dans un navigateur. Il permet d'exécuter sur une même feuille des commandes et d'afficher leurs résultats, y compris des graphiques.

Sous Ubuntu, l'installation procède selon :

1. sudo apt-get install jupyter

De retour dans VScodium, Julia activé et en mode gestionnaire de paquet :

1. (@v1.5) pkg > add IJulia

Pour lancer Jupyter, dans un terminal bash de VS Codium :

1. $ jupyter notebook

Le navigateur présente une feuille du carnet Jupyter. En cliquant sur le bouton « Nouveau » (coin supérieur droit de la feuille) Julia-1.5.3, il est possible d'entrer du code. La figure B.2 donne un exemple.

FIGURE B.2 – Exemple d'une feuille de carnet Jupyter.

Pluto

Pluto est un carnet — écrit en Julia — et dont les caractéristiques principales sont :

- la réactivité : lors de la modification d'une fonction ou d'une variable, Pluto met automatiquement à jour toutes les cellules affectées ;

- le caractère intuitif et moderne : Pluto permet de créer des documents exportés, de belles factures avec des thèmes personnalisés.

Pour installer Pluto :

1. (@v1.5) pkg > add Pluto

Après quoi, de retour dans Julia :

1. julia> using Pluto

2. julia> Pluto.run()

La dernière commande ouvre une page du carnet dans votre navigateur. Ctrl-C permet de quitter Pluto et le retour dans une nouvelle session Julia.

Julia en nuage

Il est possible d'utiliser Julia sur le cloud à l'intervention de NextJournal, CoCalc, mybinder ou Repl.it, par exemple.

Aide et documentation

Outre quelques livres cités dans la bibliographie, notamment les références [1], [2], [3], [4], [5] et [10] (sans que cela soit exhaustif), le lecteur peut se référer à la documentation officielle pour laquelle existe un document PDF téléchargeable. Par ailleurs, Julia Discourse offre un espace de discussions techniques et d'aide extrêmement précieux.

Annexe C : notes de traduction

Plusieurs modifications, décrites ci-après, ont été introduites par rapport au document original.

Notes de bas de page

Toutes les notes de bas de page sont des compléments associés à la traduction, sauf en ce qui concerne les notes (4), (10), 14, (18), (32) et (43), qui proviennent du document original.

Modifications du texte

Les sections suivantes ont été ajoutées au texte original en anglais :

- « Droits associés à la version française » ;

- « Préface de l'édition française ».

Des modifications du texte original ont eu lieu dans :

- les sections « Utilisation des exemples de code » ici et en 2.4 (pour faire face à la situation décrite à la section B.1) ainsi que dans la section 11.2 (pour expliciter pas à pas le fonctionnement de la fonction histogram("brontosaure")) ;

- le choix du répertoire de mots français (mots_FR.txt, source : référence [8], chapitre 9) au lieu du répertoire de mots anglais words.txt ;

- le choix du roman Notre-Dame de Paris (section 13.3) au lieu du roman Emma utilisé dans l'édition anglaise ;

- le choix d'un extrait du poème Liberté (section 13.8) au lieu d'Eric, the Half a Bee dans l'édition anglaise.

Modifications des exemples de programmes

Aucune modification de noms de fonctions ou structures composites n'a eu lieu, sauf dans le chapitre 18 pour des raisons de clarté. En revanche, les arguments passés aux fonctions ou les paramètres des structures composites ont été francisés.

Ajouts de figures

Les figures 4.1.1, 4.1.2, 8.2.1, 8.6.1, 10.2.1, 11.1.1, 11.5.1, B.1 sont des ajouts associés à la traduction.

Un croquis a été inséré dans la section 7.1.

Compte tenu de la situation décrite à la section B.1, un écran de Jupyter (figure B.2) fonctionnant en mode local sans JuliaBox substitue l'écran de Jupyter de l'édition originale en anglais.

Modifications de tableaux

Les tableaux 2.1 et 5.1 sont une mise en forme des informations originales.

Annexe C : Notes de traduction

Dans la version française, la bibliographie a été concentrée dans une partie ad hoc. Les citations [1], [2], [3], [4], [5], [8] et [10] constituent des ajouts associés à la traduction.

Bibliographie

[1] Ben Lauwens et Allen B. Downey, Think Julia: How to Think Like a Computer Scientist, O'Reilly, Sebastopol CA95472, 2019

[2] Georg Root, The Julia Language Handbook, publication indépendante, 2019

[3] Andrian Salceanu, Julia Programming Projects: Learn Julia 1.x by building apps for data analysis, visualization, machine learning, and the web, Packt, Birmingham, 2018

[4] Ivo Balbaert, Adrian Salceanu, Julia 1.0 Programming Complete Reference Guide: Discover Julia, a high-performance language for technical computing, Packt, Birmingham, 2019

[5] Olivier Garet, Introduction à Julia : Programmer des mathématiques, publication indépendante, 2020

[6] Steve Oualline, Practical C Programming, O'Reilly Media, 1997

[7] Michael Sipser, Introduction to the Theory of Computation, 3e édition, Cengage Learning, 2012

[8] Christophe Pythoud, Français-GUTenberg : un nouveau dictionnaire français pour ISPELL. Problèmes résolus et intégration de contributions extérieures, Cahiers GUTenberg, n° 28-29, pp. 252-275, 1998

[9] Brian K. Kerninghan et Rob Pike, The Pratice of Programming, Addison-Wesley, 1999

[10] Avik Sengupta, Julia High Performance, Packt, Birmingham, 2e édition. Ed., 2019

Index des fonctions extrinsèques

- Symboles -

draw (1)

ispuissance (1)

processline (1), (2)

+ (1)

drawcircle (1)

isreverse (1), (2), (3)

processword (1)

+, dispatch multiple (1)

drawrect (1)

issort (1)

push! (1)

+, surcharge (1)

- E -

istriangle (1)

putinpouch (1)

<, méthode (1)

eleve_au_carre (1)

isvalidtime (1)

- R -

<, surcharge (1)

estimatepi (1)

iterate (1)

randomword (1)

- A -

evalloop (1)

last (1)

readanagrams (1)

absvalue (1), (2)

example1 (1)

linecount (1)

rectcircleoverlap (1)

ack (1)

example2 (1)

LineCount, module (1)

rectincircle (1)

addall (1)

example3 (1)

- M -

recurse (1), (2)

addping (1)

example4 (1)

Markov (1)

reinterpret (1)

addtime (1), (2), (3), (4)

example5 (1)

middle (1)

repeatlyrics (1), (2)

anagramsets (1)

- F -

mostcommon (1)

returnfloat (1)

anylowercase1 (1)

f() (1)

mostfrequent (1)

reverselookup (1)

anylowercase2 (1)

fact (1), (2), (3)-(4), (5)

move! (1)

reversepairs (1)

anylowercase3 (1)

fibonacci (1)

movepoint (1), (2)

rightjustify (1)

arc (1), (2), (3)

find (1), (2)

moverectangle (1)

rotateword (1)

area (1)

findcenter (1)

multime (1)

- S -

avoids (1), (2), (3)

first (1)

mysqrt (1)

sed (1)

- B -

firstindex (1)

MyTime (1)

seq (1)

baddeletehead (1)

- G -

- N -

shuffle! (1)

- C -

grid (1)

nestedsum (1)

sinc (1)

capitalizeall (1), (2)

- H -

- O -

sort! (1)

Carte (1), (2)

hasduplicates (1), (2), (3)

onlyupper (1)

square (1)-(2), (3), (4)

cattwice (1), (2)

hasmatch (1)

- P -

squaresum() (1)

checkfermat (1)

hasno_e (1), (2)

Paquet (1), (2), (3)

storeanagrams (1)

choosefromhist (1)

haspair (1)

pgcd (1)

subtract (1)

circle (1), (2), (3)

hastwopair (1)

pointincircle (1)

subtract() (1)

circlearea (1)

histogram (1), (2), (3), (4)

polygon (1), (2), (3)

sumall (1)

classify (1)

hypotenuse (1)

polyline (1), (2), (3)

- T -

compare (1)

- I -

Polynomial (1)

testsquareroot (1)

containervariable (1)

inbisect (1)

Pong (1)

time (1)

convert (1)

inboth (1)

pop! (1)

timetoint (1), (2), (3)

count (1)

increment (1)

pouchcontents (1)

totalwords (1)

countdown (1), (2), (3)

increment! (1), (2), (3), (4)

print (1)

type, annotation (1)

cumulsum (1)

interior (1)

printall (1)

- U -

- D -

inttotime (1), (2), (3)

printhist (1)

UneMain (1)

deal! (1)

invertdict (1), (2)

printlyrics (1), (2)

useall (1), (2)

deletehead (1)

isabecedarian (1), (2), (3)

printmostcommon (1)

usesonly (1), (2), (3), (4)

differentwords (1)

isafter (1), (2)

printn (1), (2), (3)

- W -

distance (1), (2), (3)

isanagram (1)

printpoint (1)

walk (1)

distancebetweenpoints (1)

isbetween (1)

printspam (1)

dofour (1)

isdivisible (1), (2), (3)

printtime (1), (2), (3), (4)

dotwice (1)

isintpoint (1)

printtwice (1), (2), (3)

DoubleJour (1)

ispalindrome (1), (2), (3)

processfile (1), (2), (3)

Index

- Symboles -

déclaration conditionnelle (1)

join() (1)

\r (1)

! (1)

déclaration d'affectation (1)

joinpath() (1)

racine carrée (1)

!= (1)

déclaration d'affichage (1)

journalisation, débogage (1)

Ramanujan, Srinivasa (1)

#, commentaire (1)

déclaration d'utilisation (1)

julia (1)

rand() (1)

$ (1)

déclaration globale (1), (2)

Julia Discourse (1)

Random.shuffle! (1)

$, interpolation (1), (2), (3)

décrémentation (1)

Julia Discourses (1)

read() (1)

< (1)

deepcopy() (1), (2)

Julia Language Support (1)

Read-Eval-Print Loop (1)

<= (1)

deleteat!() (1)

Julia Observer (1)

readdir() (1)

> (1)

deletehead!() (1)

Julia, cloud (1)

readline() (1), (2), (3)

>= (1)

délimiteur (1), (2)

Julia, documentation officielle (1)

réaffectation (1), (2), (3)

(1)

deserialize() (1)

JULIA_DEBUG (1)

recherche (1), (2), (3)

(1)

déterminisme (1)

JuliaBox (1), (2)

recherche binaire (1)

(1), (2)

déterministe (1)

JuliaCollections (1)

recherche directe (1), (2)

(1), (2)

développement incrémental (1), (2)

Juno (1), (2)

recherche inverse (1), (2)

(1)

développement progressif (1), (2)

Jupyter (1), (2), (3)

rect.coin.x (1)

(1), (2), (3), (4)

développement, plan de (1)

Jupyter, installation (1)

récursion (1), (2), (3), (4), (5)

(1)

diagramme d'état (1), (2), (3)

- K -

récursion infinie (1), (2), (3)

(1)

diagramme d'objet (1), (2), (3), (4)

Karpinski, Stefan (1)

redondance (1)

(1), (2)

diagramme de pile (1), (2), (3), (4), (5)

Kernighan, Brian K. (1)

réduction (1), (2), (3), (4)

(1)

diagramme de type (1), (2)

KeyError (1)

réduction, opération de (1)

(1)

Dict(), zip() (1)

keys() (1)

refactoring (1), (2)

(1)

dictionnaire (1), (2)

- L -

référence (1), (2)

% (1)

dictionnaire et tableau (1)

lâcher prise, débogage (1)

référencement (1)

&& (1), (2)

dictionnaire et tuple (1)

langage formel (1)

réflexion (1)

(@v1.x) pkg (1), (2)

dictionnaire inversé (1)

langage naturel (1)

refonte (1), (2), (3)

* (1), (2)

dictionnaire, compteur (1)

lastindex() (1)

regex (1)

+ (1)

dictionnaire, inversion (1)

Lauwens, Ben (1), (2)

Regex() (1)

+= (1)

diff (1)

\le (1)

règles de préséance (1)

::, opérateur (1)

Dijkstra, Edsger W. (1)

Le nom de la rose (1)

relation de dépendance (1)

- (1)

Disparition, La (1)

length() (1), (2)

relation HAS-A (1), (2)

-> (1), (2)

dispatch (1)

length, tableau (1)

relation IS-A (1), (2)

.bashrc (1)

dispatch multiple (1), (2), (3), (4)

let (1), (2), (3)

relecture (1)

.jl (1)

dispersion (1), (2)

levée d'exception (1), (2)

répertoire (1)

/ (1), (2)

distributivité (1)

Liberté (1)

répertoire courant (1)

; (1)

DivideError (1)

libgdm (1)

REPL (1), (2), (3), (4), (5), (6)

= (1), (2)

division (1)

ligature (1)

replace() (1)

== (1), (2), (3), (4)

division décimale (1), (2), (3)

Linux (1)

repr() (1)

=== (1)

division entière (1)

lipogramme (1)

représentation intermédiaire (1)

=> (1)

division euclidienne (1), (2)

littéralité (1)

retour, valeur (1)

=>, dictionnaire (1)

divrem() (1), (2)

LLVM (1), (2)

return (1), (2), (3), (4)

? (1)

do (1), (2)

ln (1)

rev(), sort() (1)

?, caractère (1)

docstring (1), (2)

LoadError: syntax (1)

reverse lookup (1), (2)

?, opérateur ternaire (1)

documentation interne (1)

local (1)

reverse!() (1)

@ (1)

documentation officielle (1)

locale, variable (1)

Revise.jl (1)

@assert() (1)

DomainError (1)

locate (1), (2)

ROT13 (1)

@code_llvm (1)

données, encapsulation de (1), (2)

log() (1)

rubber ducking (1), (2)

@code_lowered (1)

données, structure de (1), (2)

log10() (1), (2)

run() (1)

@code_native (1)

dot (1), (2)

logarithme népérien (1)

- S -

@code_typed (1)

dot, opérateur (1)

logarithme naturel (1)

savefig() (1)

@debug (1), (2)

dot, syntaxe (1)

logarithme, base 10 (1)

scaffolding (1), (2)

@generated (1)

Downey, Allen B. (1), (2)

Logging (1)

script (1), (2)

@printf() (1), (2)

drapeau (1), (2)

lookup (1), (2)

search (1)

@show (1), (2), (3), (4), (5)

dump() (1), (2)

Low Level Virtual Machine (1)

segment (1)

@svg (1), (2), (3), (4)

duplication (1)

lowercase() (1)

segment de chaîne (1)

@test (1)

- E -

lpad() (1)

segmentation (1), (2)

@time (1)

eachindex() (1)

ls (1)

sémantique (1)

@warn (1)

Edelman, Alan (1)

Luxor (1)

sémantique, erreur (1), (2)

@which (1), (2)

EDI (1)

- M -

sentinelle (1), (2)

[n:m] (1)

élément (1), (2)

métaprogrammation (1)

séquence (1), (2), (3)

] (1), (2)

élément, tableau (1)

méthode (1), (2), (3), (4)

séquence de séquences (1)

^ (1), (2)

else (1), (2), (3)

méthode et fonction (1)

séquence indicée (1)

_ (1)

elseif (1), (2)

méthode multiple (1)

séquence, tableau (1)

\_1 (1)

Éluard, Paul (1)

méthode paramétrique (1)

serialize() (1)

{ }, type paramétrique (1)

Emacs (1)

macOS (1)

Set (1), (2)

[ ] (1), (2), (3)

emboîtement de deux mots (1)

macro (1), (2), (3), (4)

set (1)

[ ], tuple (1)

emoji (1)

Main (1), (2), (3), (4)

Set() (1)

- A -

en-tête (1), (2)

mapping (1), (2), (3), (4), (5)

Set, structure de données (1)

abécédaire (1)

encapsulation (1), (2), (3), (4)

Markov, analyse de (1)

setdiff() (1)

abs (1)

encapsulation de données (1)

match() (1)

sexagésimale, base (1)

abs() (1), (2)

encapsuler (1)

matrice (1), (2)

Shah, Viral B. (1)

abspath() (1)

enchaînement de conditions (1)

max(), tuple (1)

shell (1), (2)

abstract type (1)

encoder (1)

maximum() (1)

show (1), (2)

AbstractDict() (1)

end (1), (2), (3), (4), (5), (6), (7)

md5 (1), (2)

show() (1)

accumulateur (1), (2), (3)

enumerate() (1)

md5sum (1), (2)

shuffle! (1)

Ackermann, fonction d' (1)

environnement de développement (1)

mémo (1), (2)

signature (1)

add (1)

EOFError (1)

Meta.parse() (1), (2)

signature spécifique (1)

addition (1)

équivalence (1), (2)

métathèse (1)

sin() (1), (2)

affectation (1)

équivalence, non-identité (1)

MethodError (1)

singleton (1), (2)

affectation de tuple (1), (2)

erreur d'exécution (1)

methods() (1)

Sipser, Michael (1)

affectation, déclaration (1)

erreur de forme (1)

min(), tuple (1)

size() (1)

agrégation (1)

erreur de syntaxe (1), (2)

minimum() (1)

sizeof() (1)

agrégation, paramètre (1)

erreur sémantique (1)

minmax() (1)

skipmissing() (1)

aka (1)

error (1)

mise à jour (1)

slice (1), (2)

aléatoire (1)

Error Result, @test (1)

mise en correspondance (1), (2), (3), (4)

slurp (1), (2)

algorithme (1), (2)

ErrorException (1), (2)

missing, objet (1)

sort!() (1), (2), (3), (4)

alias (1), (2)

esc() (1)

Missing, type (1)

sort() (1), (2), (3)

aliasing (1), (2), (3), (4)

espace de stockage (1)

MIT, licence (1)

sort, documentation (1)

aliasing, persistance (1)

euclidienne, division (1)

mode écriture (1)

sous-typage (1), (2), (3)

\alpha (1)

Euler, identité (1)

mode interactif (1)

sous-type (1), (2)

ambiguïté (1)

eval (1)

modificateur (1), (2), (3)

soustraction (1)

anagramme (1), (2), (3)

évaluation en court-circuit (1)

module (1), (2), (3), (4), (5)

soustraction d'ensembles (1)

analyse (1)

exécution alternative (1)

module(s) (1)

spirale (1)

analyse comparative (1), (2)

exécution conditionnelle (1)

modules (1)

splat (1), (2)

analyse syntaxique (1)

exécution, erreur d' (1), (2)

modulo (1), (2)

splice!() (1), (2)

Annaud, Jean-Jacques (1)

exception (1), (2), (3)

mot-clé (1)

split() (1), (2)

anniversaire, paradoxe des (1)

exception, levée (1), (2)

mots, fréquence (1)

sqrt() (1), (2), (3)

annotation de type (1), (2), (3)

exp() (1), (2)

mots, occurrence (1)

StackOverflowError (1), (2), (3), (4)

appel, graphe (1)

exponentiation (1)

mots-clés, liste (1)

stacktrace (1), (2), (3), (4), (5)

appel, graphe d' (1)

export (1), (2)

mp3 (1)

Standard Library (1)

appel, trace (1)

Expr, type (1)

MS-Windows (1)

start (1)

appel, trace d' (1)

expression (1), (2), (3)

multiméthode (1), (2)

startup.jl (1)

append!() (1)

expression booléenne (1), (2)

multiplication (1)

StdLib (1)

Archimède, spirale (1)

expression rationnelle (1)

multiplicité (1), (2)

STDOUT (1)

args... (1)

Extensions (1)

mutabilité (1)

stochasticité (1)

argument (1), (2), (3)

extrema() (1)

mutable struct (1)

stockage permanent (1)

argument annoté (1)

- F -

MyTime (1)

stockage, espace de (1)

argument facultatif (1)

factoriel (1)

- N -

String (1), (2)

argument nommé (1), (2)

false (1), (2)

\n (1)

string (1)

argument optionnel (1)

fanion (1), (2)

\ne (1)

string() (1)

argument, validité (1)

Fermat, théorème de (1)

new (1)

StringIndexError (1)

ArgumentError (1)

fermeture (1), (2)

new, méthode sans argument (1)

strip() (1)

arguments multiples, tuple (1)

fibonacci() (1)

new, première méthode (1)

Struct (1)

arrondi (1)

Fibonacci, suite de (1), (2), (3)

NEWLINE (1)

struct (1), (2), (3), (4)

ASCII (1), (2), (3)

fichier (1)

Newton, méthode de (1)

structure (1)

association (1)

fichier texte (1)

nextind() (1)

structure de contrôle (1)

Atom (1), (2)

fieldnames() (1)

NextJournal (1)

structure de données (1), (2)

attribut (1)

filtre (1), (2), (3)

nombre aléatoire (1)

structure, attribut (1)

- B -

fin de ligne (1)

nombre complexe (1)

structure, champ (1)

baremodule (1)

finally (1), (2), (3)

nombre irationnel (1)

structure, persistance (1)

Base (1)

findall() (1)

nombre rationnel (1)

structures et fonctions (1)

base de données (1), (2), (3)

findfirst() (1)

non ASCII, caractère (1)

structures et objets (1)

base sexagésimale (1)

findnext (1)

non-persistance, structure (1)

\subseteq (1)

Base.< (1)

firefox (1)

notation par . (1)

sum() (1), (2)

Base.convert() (1)

flèche à extrémité creuse (1)

notebook (1)

sum(), tuple (1)

Base.iterate (1)

flèche à extrémité pleine (1)

nothing (1), (2), (3)

supertype (1), (2)

Base.pop!() (1)

flag (1), (2)

\notin (1)

supertype() (1)

Base.push!() (1)

Float (1)

Notre-Dame de Paris (1)

suppression (1)

Base.show (1)

float() (1)

nouvelle ligne (1)

surcharge d'opérateur (1), (2)

Base.show(io::IO, ...) (1) (2)

Float64 (1)

- O -

SVG (1)

bash (1), (2)

flux d'exécution (1), (2), (3), (4)

obèle (1)

syntaxe (1)

begin (1), (2), (3)

flux de fichier (1)

objet (1), (2)

syntaxe, erreur de (1), (2), (3), (4)

Benchmark Tools (1)

foncteur (1), (2)

objet aliasé (1)

SystemError (1), (2)

benchmarking (1), (2)

fonction (1)

objet appelable (1)

- T -

BenchmarkTools (1)

fonction anonyme (1), (2), (3)

objet de commande (1)

\t (1)

BenchmarkTools.jl (1)

fonction avec retour (1), (2), (3)

objet enchassé (1)

tâche (1), (2)

Bezanson, Jeff (1)

fonction booléenne (1)

objet et instance (1)

TAB (1)

bibiothèque standard (1)

fonction de hachage (1)

objet intégré (1), (2)

table de hachage (1), (2)

bissection (1), (2)

fonction et méthode (1)

objet persistant (1)

Table UPLEt (1)

bloc (1)

fonction factorielle (1)

objet, diagramme (1), (2), (3), (4)

tableau (1), (2), (3)

bloc do (1), (2)

fonction générée (1)

objet, diagramme d'état (1)

tableau bidimensionnel (1)

bloc let (1), (2)

fonction mathématique (1)

objet, structure de données (1)

tableau et dictionnaire (1)

bogue (1), (2)

fonction nulle (1), (2)

occurrence (1), (2)

tableau et tuple (1)

Bool, type (1)

fonction polymorphique (1)

official binaries (1)

tableau imbriqué (1), (2), (3)

boucle (1)

fonction pure (1), (2), (3)

opérande (1)

tableau vide (1), (2)

boucle infinie (1), (2), (3)

fonction récursive (1)

opérateur (1)

tableau, copie de (1)

boucles, indices (1)

fonction vide (1), (2)

opérateur . (1)

tableau, diagramme d'état (1)

BoundsError (1), (2)

fonction void (1)

opérateur :: (1)

tableau, dimension (1)

box (1)

fonction, appel de (1), (2), (3)

opérateur logique (1), (2)

tableau, indice (1)

branche (1)

fonction, définition (1), (2)

opérateur point (1), (2)

tableau, non-persistance (1)

break (1), (2), (3), (4), (5), (6)

fonction, distributivité (1)

opérateur relationnel (1), (2), (3)

tableau, segment (1)

BSD (1)

fonction, objet de (1)

opérateur ternaire (1)

tableau, segmentation (1)

bug (1), (2)

fonction, syntaxe compacte (1)

opérateur ternaire ? (1)

tableau, traversée (1)

- C -

fonctionnement (1)

opérateur, distributivité (1)

tableaux, bibliothèque (1)

C (1)

for (1), (2), (3), (4)

opérateur, surcharge (1)

tables d'indice (1)

canal (1)

Fortran (1)

opérateurs arithmétiques (1)

tabulation, \t (1)

canal et itérateur (1)

forward() (1)

opérateurs, préséance (1)

take!() (1)

canard en plastique, méthode du (1), (2)

fréquence (1), (2)

opérateurs, priorité (1)

tan() (1)

canevas (1), (2)

Français-GUTenberg (1)

opération de réduction (1)

téléchargement (1)

Car Talk (1), (2), (3), (4), (5)

function (1), (2)

open() (1), (2), (3), (4)

Test Failed (1)

caractère (1)

- G -

Oualline, Steve (1)

Test Passed (1)

caractère de retour (1)

généricité (1), (2)

OutOfMemoryError (1)

test unitaire (1), (2)

caractères (1)

gauche droite (1)

outrepassement (1)

ThinkJulia (1), (2)

carnet (1)

GDBM (1)

OverflowError (1)

ThinkJulia, module (1)

Carroll, Lewis (1)

gdbm_close (1)

override (1)

time (1)

cas atypique (1)

\ge (1)

- P -

time() (1)

cas de base (1), (2)

généralisation (1), (2), (3)

périmètre d'application (1)

Torvalds, Linus (1)

cas particulier (1)

get!(), documentation (1)

paire clé-valeur (1)

trace d'appel (1), (2), (3)

catch (1), (2), (3)

get() (1)

palindrome (1)

trace de pile (1), (2), (3)

ccall (1)

Git (1)

Pallier, Christophe (1)

traversée de chaîne (1)

cd (1)

GitHub (1)

paquet (1)

true (1), (2)

chaîne conditionnelle (1)

global (1), (2), (3), (4)

paquet(s) (1)

trunc() (1), (2)

chaîne de caractères (1)

globale, variable (1)

paquets, ancienne méthode (1)

try (1), (2), (3)

chaîne multiligne (1)

(1)

paquets, gestionnaire (1)

tuple (1), (2)

chaîne vide (1)

golden (1)

paquets, installation (1)

tuple et dictionnaire (1)

chaîne, interpolation (1), (2)

graphe d'appel (1), (2)

paquets, nouvelle méthode (1)

tuple et tableau (1)

chaîne, persistance (1)

Greenfield, Larry (1)

paramètre (1), (2), (3)

tuple nommé (1), (2)

chaîne, segment (1)

guillemet triple (1)

paramètre d'agrégation (1)

tuple() (1)

chaîne, traversée de (1), (2)

guillemets bouclés (1)

paramétrique, type (1)

tuple, [ ] (1)

chaînes (1), (2)

guillemets droits (1), (1)

parcourir un tableau (1)

tuple, affectation (1)

chaînes, bibliothèque (1)

guillemets inversés (1)

parenthèses (1)

tuple, affectation de (1)

chaînes, comparaison (1)

guillemets, simples (1)

parse (1), (2)

tuple, diagramme d'état (1)

chaînes, documentation (1)

GUTenberg, projet (1)

parse() (1), (2)

tuple, persistance (1)

champ (1)

- H -

PATH (1)

Turing, Alan M. (1)

Channel (1)

hachage (1)

PEMDAS (1)

Turing, thèse de (1)

Channel, constructeur (1)

hachage, fonction (1)

pendown (1)

turn() (1)

Char (1), (2)

hachage, table (1)

penup (1)

Turtle (1), (2)

char (1)

hachage, valeur de (1)

Perec, Georges (1)

type (1), (2)

chemin absolu (1), (2)

handle (1), (2)

performances, mesure de (1)

type abstrait (1), (2)

chemin d'accès (1)

hash function (1)

persistance (1), (2), (3)

type abstrait et fonction (1)

chemin relatif (1), (2)

hash table (1), (2)

persistance, aliasing (1)

type abstrait prédéfini (1)

clé (1)

help?> (1)

persistance, programme (1)

type Bool (1)

clé-valeur (1), (2)

histogramme (1), (2), (3), (4)

persistance, tuple (1)

type composite (1)

clode(db) (1)

homophonie (1)

PGCD (1)

type paramétrique (1), (2)

close() (1), (2)

Hugo, Victor (1)

\pi (1)

type parent (1)

CMU Pronouncing Dictionary (1)

- I -

pictogramme (1)

type primitif (1), (2)

coroutine (1)

IDE (1)

Pike, Rob (1)

type primitif et constructeur (1)

CoCalc (1)

identité (1), (2)

pile, diagramme de (1), (2)

type spécifique (1), (2), (3)

codage (1)

if (1), (2), (3)

pile, trace de (1)

type union (1)

code bas niveau (1)

if, expression booléenne (1)

Pkg (1)

type, diagramme de (1), (2)

code César (1)

IJulia (1)

Pkg.add() (1)

TypeError (1)

code machine (1), (2)

IJulia, installation (1)

placage (1), (2)

typeof() (1), (2)

code mort (1)

imbrication conditionnelle (1)

plan de développement (1)

- U -

Codium (1), (2), (3), (4), (5)

imbrication de conditions (1)

planification (1)

UInt8 (1)

codium (1)

immuabilité (1)

plot() (1)

UndefVarError (1), (2)

Collatz, conjecture de (1)

implémentation (1), (2)

Plots (1)

Unicode (1), (2), (3)

collect() (1), (2)

import (1), (2), (3)

Plots() (1)

Union (1)

collect(), zip() (1)

import Base.< (1)

Plots, fonction anonyme (1)

union (1)

commentaire (1), (2), (3)

importall (1)

Plots, installation (1)

unofficial binaries (1)

complétion, séquence de (1)

importation, déclaration d' (1)

Pluto (1), (2), (3)

updatedb (1), (2)

composition (1), (2), (3)

in (1), (2)

Pluto, installation (1)

uppercase() (1)

compteur (1), (2)

\in (1)

Pluto.run() (1)

using (1), (2), (3), (4), (5)

concaténation (1), (2)

include() (1)

point, syntaxe (1)

using InteractiveUtils (1)

concaténer (1)

incrémentation (1), (2), (3), (4)

pointeur C (1)

using Object (1)

condition (1), (2)

indice (1), (2)

polymorphisme (1), (2)

using Pkg (1)

condition a posteriori (1), (2), (3)

InexactError (1)

pop! (1)

using Random (1)

condition a priori (1), (2), (3)

Inf (1)

pop!() (1), (2)

using Test (1)

condition préalable (1), (2)

initialisation (1)

popfirst!() (1), (2), (3)

UTF-8 (1), (2)

conditions imbriquées (1)

insert!() (1), (2)

préséance d'opérateurs (1)

- V -

const (1), (2), (3)

insertion (1)

prevind() (1)

valeur (1), (2), (3), (4)

constante globale (1)

instance (1), (2), (3)

primitive type (1)

valeur de retour (1), (2)

constructeur (1), (2), (3), (4), (5)

instance en argument (1)

printf() (1)

valeur par défaut (1)

constructeur de copie (1)

instanciation (1), (2), (3)

printf(), documentation (1)

valeur retournée, tuple (1)

constructeur externe (1)

instruction (1)

println() (1), (2)

valeurs manquantes (1), (2)

constructeur interne (1), (2)

instruction composée (1), (2)

profile (1)

values() (1)

constructeur par défaut (1)

Int (1)

profile, documentation (1)

\varepsilon (1)

constructeur, copie (1)

Int64 (1)

Profile.jl (1)

variable (1)

constructeur, méthode externe (1)

InteractiveUtils (1)

ProfileView.jl (1)

variable globale (1), (2), (3), (4)

constructeur, méthode par défaut (1)

interface (1), (2), (3), (4)

profondeur de récursion (1)

variable locale (1), (2), (3)

constructeur, signature (1)

interface implémentation (1)

programmation aléatoire (1)

variable temporaire (1)

continue (1), (2), (3)

interpolation (1)

programmation fonctionnelle (1)

vcat() (1), (2), (3)

conversion (1), (2)

interpolation de chaîne (1)

programmation générique (1), (2), (3)

verbose, mode (1)

copie (1)

introspection des signatures (1)

programme (1)

Verlaine, Paul (1)

Core.eval() (1)

invalid token (1)

programme suspendu (1)

Vim (1)

coroutine (1)

invariant (1), (2), (3), (4)

projet GUTenberg (1)

Visual Studio Code (1)

corps (1), (2)

invite (1)

promote() (1)

Visual Studio Codium (1), (2)

cos() (1), (2)

io (1)

promote_rule() (1)

void function (1), (2)

count() (1)

iobuffer (1)

promotion (1), (2)

vorpal (1)

count(), documentation (1)

isa() (1)

prototypageplanification (1)

VSCodium (1)

court-circuit (1)

isa, opérateur (1)

prototype et correctif (1), (2)

- W -

current working directory (1)

isafter (1)

pseudo, pseudonyme (1)

"w", mode écriture (1)

Cvoid (1)

isdefined() (1)

pseudoaléatoire (1), (2)

walkdir() (1)

cwd (1)

isdir() (1)

pseudonymie (1), (2)

where (1)

- D -

isfile() (1)

Ptr{Cvoid} (1)

while (1), (2), (3), (4), (5), (6), (7)

débogage et sous-typage (1)

ispath() (1)

push! (1)

- Z -

déclaration d'importation (1)

itérateur (1), (2), (3)

push!() (1), (2), (3), (4), (5), (6)

zip() (1)

définition de fonction (1)

itération (1), (2)

pushfirst!() (1), (2)

zip(), collect() (1)

dépendance (1), (2)

itérer (1)

pwd() (1)

zip(), Dict() (1)

Database Manager (1)

item (1), (2)

Pythagore, théorème de (1), (2)

zip, objet (1), (2)

DataStructures.jl (1)

item, tableau (1)

Pythoud, Christophe (1)

Zipf, loi de (1)

DBM (1)

- J -

- Q -

zipper (1)

dbm (1)

Jabberwocky (1)

quote (1)

débogage (1), (2), (3), (4), (5),
(6), (7), (8), (9), (10), (11),
(12), (13), (14), (15), (16),
(17), (18), (19), (20), (21)

JLD2 (1)

quotient (1)

déclaration (1), (2), (3), (4)

JLD2, documentation (1)

- R -

(1)ATTRIBUTION : toutes les licences Creative Commons obligent ceux qui utilisent vos œuvres à vous créditer de la manière dont vous le demandez, sans pour autant suggérer que vous approuvez leur utilisation ou leur donner votre aval ou votre soutien.
PAS D’UTILISATION COMMERCIALE : vous autorisez les autres à reproduire, à diffuser et (à moins que vous ne choisissiez ‘Pas de Modification’) à modifier votre œuvre, pour toute utilisation autre que commerciale, à moins qu’ils obtiennent votre autorisation au préalable.

(2)Dans la traduction française, les locutions latines sont en italique.

(3)Pour créer un compte GitHub, référez-vous à Comment créer un compte GitHub. Pour installer Git (MS Windows, macOS et GNU/Linux), consulter Installing Git — The easy way.

(4) Il s'avère qu'un marathon couvre environ 42 kilomètres.

(5)Le terme « intégrée » est équivalent dans ce contexte.

(6)Dans l'exemple, Julieta est une allusion au film de Pedro Almodovar.

(7)Le signe ÷ est un obèle.

(8)Questions qui reposent sur une division binaire.

(9)Depuis 1966, un prix Turing est annuellement décerné à une personne sélectionnée pour sa contribution de nature technique vis-à-vis de la communauté informatique. Les contributions doivent être d’une importance technique majeure et durable dans le domaine informatique.

(10) Pour une discussion plus complète et plus précise de la thèse de Turing, consultez le livre de Michael Sipser, Introduction to the Theory of Computation [7].

(11)Les truismes, les tautologies, les lapalissades sont de cet ordre.

(12)Ce mot a été inventé par Lewis Carroll dans le poème Jabberwocky  et semble désigner le tranchant d'une épée. Voir la traduction en français de ce poème sous ce lien.

(13)Un langage de programmation (voir Wikipédia).

(14)Une source d'amusement pour les informaticiens provient des instructions sur les bouteilles de shampoing : « Faire mousser, rincer, répéter », ce qui constitue une boucle infinie.

(15)C'est-à-dire les pictogrammes .

(16)Comme ce qui se pratique dans le shell bash, par exemple.

(17)∈ signifie « appartient à » ou « est un élément de »

(18) Des blagues potentiellement grivoises sur l'Internet sont parfois encodées en ROT13, un code César avec un décalage de 13. Si vous n'êtes pas facilement offensé, trouvez et décodez certaines d'entre elles.

(19)Les tests de programmes peuvent être utilisés pour montrer la présence de bogues,  mais jamais pour montrer leur absence !

(20)Le vers est extrait du poème « Clair de lune » de Paul Verlaine

(21)La traduction exacte devrait être pseudonymie. Cependant, alias (c'est-à-dire pseudo ou pseudonyme) et aliasing sont des termes consacrés. En informatique, s'agissant de personnes, il est également fréquent de trouver l'acronyme aka pour also known as.

(22)C'est-à-dire la fréquence d'apparition.

(23)Ce terme désigne des mots qui ont la même prononciation : « eau » et « haut » sont homophones.

(24)Ce néologisme est basé sur le terme mathématique N-uplet : Table UPLEt

(25)Anecdotiquement, SPQR signifie Senatus populusque romanus

(26)Dans le monde de Julia, l'agrégation est souvent appelée « slurp » et la dispersion « splat », en anglais du moins.

(27)Itérer : avec la signification de parcourir les éléments d'un conteneur (au sens large). Itérateur : objet capable de parcourir tous les éléments contenus dans un autre objet.

(28)La commande typeof(zip(s,t)) retourne Base.Iterators.Zip{Tuple{String,Array{Int64,1}}}

(29)Le numéro de téléphone indiqué dans le diagramme est le numéro du standard de Radio France (même si le numéro est public, ne l'appelez pas).

(30)En anglais, il s'agit d'un « bonus » (Grande-Bretagne) ou d'un « bingo » (États-Unis).

(31)Linguistiquement, il s'agit de la permutation de deux phonèmes dans la chaîne parlée (par exemple, aéroplane devient en français populaire aréoplane).

(32) C'est un raccourci que de déclarer : « les chaînes de caractères sont converties ». En effet, les chaînes sont persistantes. En réalité, une fonction comme lowercase retourne de nouvelles chaînes.

(33)Les paquets Profile.jl, ProfileView.jl et BenmarkTools.jl sont installables selon les méthodes indiquées dans l'appendice B.

(34)À cet égard, le contrôle de version dans les environnements de développement intégré s'avère particulièrement utile.

(35)La méthode du canard en plastique consiste à expliquer méticuleusement le code source qu'on a écrit à un collègue, à un simple passant ou même à un objet inanimé comme un canard en plastique (voir Wikipedia EN).

(36)pwd — print working directory — est une commande des systèmes , GNU/Linux et BSD.

(37)Ce répertoire est souvent noté $HOME.

(38)database manager

(39)mémoire tampon d'entrées/sorties

(40)Un shell est l'interface entre un utilisateur et le noyau.

(41)STandard OUTput

(42)comme le faisaient les astronomes et géographes grecs.

(43) Ironiquement, le fait de poser un problème sous une forme plus générale (ce qui demande un investissement) le rend parfois plus facile à résoudre du fait qu'il y a moins de cas particuliers et moins de possibilités d'erreur.

(44)Parfois appelé également multiméthode.

(45)J : Jack, Q : Queen, K : King

(46)Pour ♣ : \:clubs: TAB ; pour ♦ : \:diamonds: TAB ; pour ♥ : \:hearts: TAB ; pour ♠ : \:spades: TAB

(47)En français, nous devons éviter l'usage d'une fonction personnelle Main du fait de l'existence de la fonction générale Main.

(48)Citation de Richard M. Stallman.

(49)Le lecteur consultera la référence [10]

(50)Pour que la modification de fichier soit prise en compte, il est nécessaire de sortir de la session et d'y revenir. Ceci permet de relire .bashrc.

(51)Ceci peut se faire avec l'utilisation des commandes : sudo updatedb  suivi (en mode utilisateur) de locate .