Aller au contenu

Paradigmes

Dans cette séance, nous examinons différentes catégories de langages de programmation et illustrons les caractéristiques qui en découlent avec des exemples de code. Nous apprendrons la différence entre les langages impératifs et déclaratifs, la pertinence d’un système de types, l’encapsulation, et pourquoi la récursion est un élément fondamental de la programmation fonctionnelle.

Résumé de la séance

Les langages de programmation sont généralement inspirés par un ensemble de paradigmes. Tous les problèmes de programmation ne correspondent pas également à tous les paradigmes. Par conséquent, il est crucial de connaître les paradigmes de base, leurs possibilités et leurs limites afin de choisir le langage de programmation approprié.

Définition d’un paradigme

Un paradigme de programmation est une façon relativement abstraite de conceptualiser et de structurer l’implémentation d’un programme informatique. Un langage de programmation peut être classé comme supportant un ou plusieurs paradigmes.
-- Wikipedia

Dans cette unité de cours, nous examinerons deux principales catégories de paradigmes et leurs diverses sous-catégories.

  • Impératif : Comment atteindre un objectif, instructions concrètes étape par étape
  • Déclaratif : Ce qui est attendu, en termes d’entrées et de sorties

Paradigmes impératifs

  • Il s’agit de notre premier paradigme global, c’est-à-dire que les autres paradigmes en sont des formes spécialisées
  • Essentiellement, la programmation impérative consiste à dire à l’ordinateur comment faire quelque chose, étape par étape.
    • Comment : nous fournissons des instructions concrètes, indiquant comment résoudre un problème.
    • Étape par étape : les instructions sont listées les unes après les autres. Nous les traitons séquentiellement.

Langages impératifs primitifs

Les langages de programmation les plus anciens et les plus simples appartiennent à cette catégorie, et les instructions sont souvent orientées vers le matériel sous-jacent.

Assembleur

  • Exemple en assembleur :
section .data
    hello:     db 'Hello, World!',10    ; 'Hello, World!' plus a linefeed character
    helloLen:  equ $-hello             ; Length of the 'Hello world!' string

section .text
    global _start

_start:
    mov eax,4            ; The system call for write (sys_write)
    mov ebx,1            ; File descriptor 1 - standard output
    mov ecx,hello        ; Put the offset of hello in ecx
    mov edx,helloLen     ; helloLen is a constant, so we don't need to say
                         ;  mov edx,[helloLen] to get it's actual value
    int 80h              ; Call the kernel
    mov eax,1            ; The system call for exit (sys_exit)
    mov ebx,0            ; Exit with return "code" of 0 (no error)
    int 80h;

Niveau assez bas. Il faut littéralement connaître les abréviations eax, ebx, ... pour appeler un simple "système" out et afficher quelque chose à l’écran. C’est parce que vous écrivez l’équivalent lisible par l’humain du bytecode machine.

Basic

Basic nous fournit au moins une fonction print à appeler. Il existe toutefois d’autres concepts étranges, comme sauter dans le code source avec des instructions GOTO.

10 PRINT "Hello, World!"
20 GOTO 10

Vous pouvez expérimenter avec des programmes Basic sur "https://www.quitebasic.com"

Procédural

  • Une première constatation a été que les programmes longs et complexes sont difficiles à écrire, et encore plus difficiles à comprendre.
  • D’où l’idée d’encapsuler la logique répétitive dans des fonctions.
    • Le code est toujours exécuté ligne par ligne, mais
    • L’exécution du code saute à l’intérieur et à l’extérieur des fonctions.

ANSI C

Un langage bas niveau répandu qui supporte les fonctions est le langage de programmation C :

#include <stdio.h>

// Function declaration
int sumToN(int n);

int main() {
    int n = 100;
    int sum = sumToN(n);

    printf("The sum of numbers from 1 to %d is %d\n", n, sum);
    return 0;
}

// Function definition
int sumToN(int n) {
    int sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    return sum;
}

C est un langage compilé, donc pour l’exécuter, nous devons le convertir en code machine :

  • Compiler avec :
    $ gcc sum.c
  • Cela crée un nouveau fichier a.out, un binaire en code machine. Nous pouvons simplement l’exécuter :
    sum.c
Pourquoi le compilateur est-il nécessaire ? L’assembleur n’avait pas besoin de compilateur.

Le code assembleur est littéralement l’équivalent lisible par l’humain des instructions machine (bytecode). L’exécution d’un compilateur consiste à traduire chaque instruction individuelle en bytecode.
C, en revanche, n’est pas une version directe un-à-un du bytecode. Il est plus facile à comprendre pour nous, humains, mais pour que l’ordinateur le comprenne, nous devons quand même le convertir en bytecode.
Cela présente aussi quelques avantages : * Optimisations de performance. * Détection des erreurs bas niveau, par exemple erreurs de syntaxe ou variables non référencées.

Note : C présente encore de nombreuses subtilités, par exemple rien n’empêche de dépasser les limites d’un tableau :

#include <stdio.h>

int main() {
    int numbers[] = {1, 2, 3, 4, 5};   // Array of 5 integers
    int sum = 0;
    int size = sizeof(numbers) / sizeof(numbers[0]);  // Compute array length

    // Loop over the array
    for (int i = 0; i < size; i++) {
        sum += numbers[i];   // Access each element
    }

    printf("The sum of the array elements is %d\n", sum);
    return 0;
}

Voyons ce qui se passe :

$ gcc array.c
$ ./a.out
The sum of the array elements is 17
Que s’est-il passé ?

Le compilateur C ne vérifie pas que les dimensions des tableaux sont respectées. Notre boucle vient de dépasser les limites du tableau et a ajouté n’importe quelle valeur présente dans la RAM à cet endroit.
C ne nous protège pas contre les erreurs de programmation stupides, nous obtenons simplement des résultats incohérents.

Bash

  • Bash est un autre exemple utile de langage procédural.
  • Contrairement à C, il ne nécessite pas de compilateur, seulement un interpréteur.
  • Vous avez déjà utilisé l’interpréteur Bash : c’est votre ligne de commande !
  • Bash est très pratique, car il combine la puissance d’accès au système de fichiers avec la programmation déclarative.
    • Créer des répertoires et fichiers
    • Supprimer, renommer des répertoires et fichiers
    • Expressions régulières
    • Tableaux
    • Fonctions
    • ... et la possibilité d’appeler n’importe quel programme que vous avez installé (récupérer des pages web, extraire des informations, envoyer des messages à des webAPIs, ...)

Voici un exemple de programme qui crée 100 dossiers, chacun contenant un fichier numéroté identique à l’intérieur :

for i in {1..100}; do mkdir $i; touch $i/$i; done

Orienté Objet

  • Avec C, nous avons déjà vu comment la fonctionnalité peut être encapsulée.
  • Mais souvent, on ne veut pas seulement modulariser la logique, on veut modulariser les données.
  • Les langages orientés objet ajoutent le concept de classes et de masquage de l’information :
    • Une classe définit

Définition macro OO ici. Secrets de la classe / encapsulation, orientation sur des objets du monde réel. Combinaison de l’état et du comportement.

Typage strict, encapsulation stricte

Idéalement, en utilisant un langage de programmation orienté objet, nous devons respecter les règles OO.

  • Les classes définissent strictement quelles entrées et sorties elles acceptent
    • Idéalement, un compilateur nous avertit avant l’exécution si nous violons les types.
  • Les objets disposent d’un mécanisme approprié pour protéger leurs secrets
    • Nous ne pouvons pas accéder directement aux secrets de la classe, nous devons passer par les mécanismes d’accès prévus.
    • Idéalement, un compilateur nous avertit lorsque nous essayons d’accéder à quelque chose de restreint.

Java possède des mécanismes pour faire respecter l’encapsulation, mais par défaut, ils ne sont pas activés.

Voici un exemple d’une classe Java représentant une Voiture avec un accès ouvert à toutes les variables :

public class Car {

    public int remainingFuel = 50;
    public int mileage = 200;
    public int horsepower = 80;
    public int speed = 0;

    public void accelerate() {
        speed = speed + 10;
        System.out.println("Vroooooom");
    }

    public void slowDown() {
        speed = speed - 10;
        System.out.println("Screeeeech");
    }

    public void fuelUp() {
        remainingFuel = 100;
        System.out.println("Glug, glug, glug");
    }
}

Pas typé strictement, encapsulation non stricte

  • Python est un bon exemple de langage qui gère les choses avec beaucoup plus de souplesse que Java.
  • Python supporte les types et les classes, donc on pourrait le considérer comme un langage OO à part entière.

Considérons cet exemple :

  • Nous avons une classe Car
  • La classe Car possède un champ fuel (int)
  • fuel est privé (notation __)
class Car:

    # Constructor for car, here we initialize fuel with the provided int
    def __init__(self, fuel: int):
        self.__fuel = fuel  # "private" by convention

    # Driving consumes some fuel
    def drive(self, distance: int):
        """Consumes 1 unit of fuel per unit of distance"""
        if self.__fuel >= distance:
            self.__fuel -= distance
            print(f"Drove {distance} units, fuel left: {self.__fuel}")
        else:
            print("Not enough fuel!")

    # Getter to look up remaining fuel
    def get_fuel(self) -> int:
        return self.__fuel

Et pendant un certain temps, cela semble être un langage de programmation très raisonnable :

  • Nous pouvons créer des objets Car.
  • Nous pouvons utiliser la méthode get_fuel pour vérifier combien de carburant reste.
  • Nous pouvons conduire, en consommant du carburant. La méthode ne nous permettra pas de conduire s’il n’y a pas assez de carburant restant.
from car import Car

## Here we initialize a Car object with 10 units of fuel.
my_car = Car(10)

print("Fuel (via method):", my_car.get_fuel())

# Normal drive:
my_car.drive(3)

# Let's check how much fuel is remaining:
print("Fuel (via method):", my_car.get_fuel())

Cependant, il s’avère que ce langage n’est pas sûr du tout :

  • Nous pouvons contourner nous-mêmes le champ fuel et y mettre n’importe quoi.
  • Les types sont purement décoratifs, rien ne nous empêche d’écrire autre chose.
  • Comme il n’y a pas de compilateur, nous ne découvrons nos erreurs de programmation (la plupart du temps involontaires) qu’une fois qu’il est trop tard (à l’exécution du programme).
## So far everything was normal, but we can totally abuse the provided code and deviate from string OO programming.
# 1) We can "steal" fuel from the car, without driving:
# (fuel is a private field, but python lets as tamper anyway)
my_car._Car__fuel = 1  # whoops, we just dumped fuel.
print("Fuel after external tampering:", my_car.get_fuel())

# 2) Even worse, although "fuel" is supposed to be an int, we can write a string inside:
my_car._Car__fuel = "infinite"  # no runtime type check
print("Fuel is now:", my_car._Car__fuel)

# This breaks logically but Python won't stop you
# Now fuel is no longer an int but a string, what will happen if we attempt to drive ?
my_car.drive(2)  # would throw a TypeError at runtime => The program will crash AT RUNTIME !

Le code ci-dessus n’est pas sécurisé

Un développeur peut toujours accéder au champ "privé" balance, par exemple en utilisant my_car._Car__fuel = "infinite". Il n’y a même pas de vérification de type !

Pourquoi cela ?

  • Le double underscore est purement décoratif, exprimant le mince espoir que tout le monde respecte les internes des classes.
  • Le double underscore ne fournit absolument aucune protection au niveau du langage contre quelqu’un qui manipule les internes de la classe. Il est surtout décoratif.

Mon avis

J’espère ne jamais me retrouver à bord d’un avion dont le constructeur aurait décidé d’implémenter des composants essentiels en Python.

Paradigmes déclaratifs

Les langages déclaratifs sont un autre terme générique, avec de nombreux partisans. Essentiellement, les langages déclaratifs s’intéressent moins au comment atteindre un résultat et se concentrent plutôt sur ce qui est souhaité.

Graphique

Il est très probable que vous ayez déjà travaillé avec un langage déclaratif : HTML

En HTML, vous décrivez uniquement ce que vous attendez de voir. Vous ne vous souciez pas de la façon dont le navigateur traduit votre description en résultat graphique à un niveau technique.

<html>
<body>
<h1>I want a big fat heading.</h1>
<h2>And then I want some subheading</h2>
<p>... and then I want some text.</p><br/>
<p>and then a cute image:
<p>
    <img src="https://upload.wikimedia.org/wikipedia/commons/6/6e/Golde33443.jpg"/>
</body>
</html>
Une idée pour un autre langage graphique ?

LaTeX ! Vous décrivez uniquement le contenu du document, LaTeX s’occupe du reste !

Voici un exemple de code LaTeX public en action, générant un document.

Données

Récupérer des données depuis une base de données nécessite souvent beaucoup d’optimisation. En tant qu’utilisateur, vous vous souciez rarement de la façon exacte dont les données sont récupérées, tant que cela reste ultra-rapide.

Les langages de requête de bases de données comme Sequel (SQL) servent justement à cela : vous décrivez ce que vous voulez, pas comment l’obtenir efficacement.

Par exemple, pour une table de base de données contenant des professeurs :

Prénom Nom Champs
Alan Turing Theoretical CS, AI
John McCarthy AI, Lisp
Donald Knuth Algorithms, TeX
Edsger Dijkstra Algorithms, Software Eng
Grace Hopper Programming Languages
Tim Berners-Lee Web, Networking
Ada Lovelace Programming, Computation
Marvin Minsky AI, Robotics
Claude Shannon Information Theory
Leslie Lamport Distributed Systems
John Backus Programming Languages, Fortran
Richard Stallman Free Software, OS
Ken Thompson Operating Systems
Dennis Ritchie Programming Languages, C
Barbara Liskov Distributed Systems, OOP
Vinton Cerf Networking, Internet
Donald Becker OS, Networking
Andrew Tanenbaum OS, Networking
Bjarne Stroustrup Programming Languages, C++
Shafi Goldwasser Cryptography

Nous pourrions récupérer tous les professeurs ayant un "m" dans leur prénom :

SELECT *
FROM professors
WHERE first_name LIKE '%m%';
Pourquoi l’instruction SQL est-elle déclarative ?

Parce que nous ne spécifions pas COMMENT la base de données doit être recherchée. Nous décrivons seulement ce que nous voulons.

Fonctionnel

"[...] une approche fonctionnelle consiste à composer le problème comme un ensemble de fonctions à exécuter. Vous définissez soigneusement l’entrée de chaque fonction, et ce que chaque fonction

retourne - MSDN"

  • est une approche déclarative : le programmeur définit quoi exécuter, mais pas dans quel ordre.
  • Le programme n’est pas exécuté ligne par ligne, mais appel de fonction par appel de fonction.
    • pas de boucles comme structures de contrôle.
    • usage intensif de la récursion.

Récursion (récapitulatif)

Dans les langages fonctionnels purs, vous ne pouvez pas programmer de manière impérative :

  • Vous ne pouvez pas utiliser des incréments basiques comme i = i+1
  • À la place, vous devez utiliser la récursion.

Voici un petit récapitulatif de la façon dont les boucles peuvent être transformées en récursions :

Les solutions itératives (basées sur des boucles) et récursives sont également puissantes, c’est-à-dire que chaque solution itérative peut être transformée en solution récursive et vice-versa.

Calcul itératif de la factorielle :

  private static int facultyLoop(int n) {
    int faculty = 1;
    int i = 1;
    while (i <= n) {
        faculty = faculty * i;
        i++;
    }
    return faculty;
}

Calcul récursif de la factorielle :

private static int facultyRecursion(int i) {
    return i <= 1 ? 1 : i * facultyRecursion(i - 1);
}
La programmation fonctionnelle est-elle meilleure que la programmation impérative ?

Le choix entre résoudre un problème de manière itérative ou récursive dépend entièrement du problème. Certains problèmes sont décrits plus élégamment comme une fonction récursive et deviennent trop verbeux lorsqu’ils sont codés de manière itérative. Pour d’autres, c’est l’inverse. Il n’y a pas de gagnant clair, tout dépend de la nature du problème que vous résolvez. Cependant, pour une véritable programmation fonctionnelle, un langage OO comme Java n’est pas le meilleur choix. Chaque appel récursif ajoute une fonction à la pile, alors que d’autres langages comme Haskell disposent de meilleurs mécanismes pour gérer les effets secondaires de la récursion.

Haskell

Haskell est considéré comme un langage fonctionnel pur (pas de boucles).

Voici un petit exemple de la façon de réaliser une tâche mathématique simple en utilisant le langage de programmation Haskell :

  • Chaque bloc de deux lignes définit d’abord une fonction (types des entrées et sorties)
  • La ligne suivante définit ensuite comment le résultat est calculé, en fonction des entrées.
-- Function to get all proper divisors of n
divisors :: Int -> [Int]
divisors n = [x | x <- [1 .. n `div` 2], n `mod` x == 0]

-- Function to check if n is perfect
sumDivisors :: Int -> Bool
sumDivisors n = sum (divisors n) == n

-- Result consumes the number as input
isPerfect :: Int -> Bool
isPerfect n = sumDivisors n

Pour exécuter le programme, nous avons besoin de l’interpréteur correspondant (il est aussi possible de compiler Haskell, mais afficher les résultats devient alors un peu plus compliqué).

$ ghci perfect.hs
GHCi, version 9.12.2: https://www.haskell.org/ghc/  :? for help
[1 of 2] Compiling Main             ( perfect.hs, interpreted )
Ok, one module loaded.

L’interpréteur a maintenant chargé notre fichier Haskell, nous pouvons simplement appeler les fonctions.

ghci> result
True

Les langages fonctionnels sont bien adaptés aux fonctions, c’est-à-dire aux problèmes mathématiques. Cependant, pour des problèmes du monde réel, ils ont tendance à causer beaucoup de maux de tête. Un programme qui se contente d’afficher des résultats (sans utiliser l’interpréteur) nécessite déjà des concepts de programmation avancés.

Piloté par les événements

Les langages pilotés par les événements suivent des règles simples du type « si ceci se produit, alors déclencher cette action ».

Les langages graphiques, ou ceux qui supportent les interfaces utilisateur, ont souvent un composant piloté par les événements pour réagir aux interactions de l’utilisateur.

JavaScript en est un exemple (gestion des clics de souris, frappes au clavier, etc.).

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Button Alert Example</title>
</head>
<body>

<!-- Event example: Click is an event, the action to invoke is the showAlert function. -->
<button onclick="showAlert()">Click Me</button>

<script>
    function showAlert() {
        alert("Button was clicked!");
    }
</script>

</body>
</html>

Récapitulatif

Paradigme Focus Utilisations courantes Langages
Impératif Instructions étape par étape Scripts simples, bas niveau Assembleur, Basic
Procédural Organisation en fonctions Applications simples, enseignement C, Python, Bash
POO Modélisation d’entités réelles Grandes applications, interfaces graphiques C++, Java
Concurrent Plusieurs tâches simultanées Systèmes temps réel, tâches parallèles Go, Erlang
Déclaratif Décrire le résultat, pas les étapes SQL, configurations, UI SQL, HTML, CSS
Fonctionnel Fonctions pures, immuabilité Problèmes mathématiques Clojure, Haskell
Piloté par événements Gestion des événements Interfaces graphiques, applications web JavaScript, C# (pour UI)

Arbre Impératif vs Déclaratif

graph TD
    A[Programming Paradigms]
%% Root categories
    A --> B[Imperative]
    A --> C[Declarative]
%% Imperative descendants
    B --> B1[Procedural]
    B1 --> B1a[Assembly]
    B1 --> B1b[C]
    B --> B2[Object-Oriented]
    B2 --> B2b[Java]
    B2 --> B2c[C++]
    B --> B3[Concurrent/Parallel]
    B3 --> B3a[Go]
    B3 --> B3b[Erlang]
%% Declarative descendants
    C --> C1[Functional]
    C1 --> C1a[Haskell]
    C1 --> C1b[Lisp]
    C --> C2[Graphic]
    C2 --> C2a[HTML]

Langages multi-paradigmes

Certains langages de programmation présentent des chevauchements significatifs de paradigmes. On les appelle aussi "langages multi-paradigmes".

Incompatibilités de paradigmes

Nous avons déjà vu un exemple fort d’incompatibilité de paradigmes : coder en OO avec Python.

  • Le fait qu’une chose puisse être implémentée dans un langage donné ne signifie pas qu’elle devrait l’être, ni que c’est un bon style de programmation.
  • En fait, il est parfaitement possible de forcer des constructions de programmation dans un langage pour lequel elles n’ont pas été conçues.
    • Dans le meilleur des cas, le code devient trop complexe.
    • Dans le pire des cas, le code ne fait pas du tout ce que l’on pourrait attendre...

Utiliser le bon outil pour le bon usage

Quand votre code devient trop compliqué et que vous avez constamment l’impression que le langage lui-même vous limite dans ce que vous voulez faire, il y a de fortes chances que vous soyez confronté à une incompatibilité de paradigme.

OO en Python

  • Lorsque vous êtes habitué aux principes OO stricts, passer à un écosystème moins OO peut entraîner des incompatibilités de paradigme : le développeur essaie d’imposer un paradigme à un langage qui n’a pas été conçu principalement pour cela.
  • Cela arrive à la plupart des développeurs Java venant d’un background Java.
  • Java permet facilement des principes OO comme le masquage de l’information, les champs privés, etc.
  • Comme vu précédemment, en Python, les champs peuvent être déclarés « pseudo » privés, mais cela ne change pas réellement leur comportement et ils restent accessibles.

Compilé versus interprété

  • Tout au long de la séance, nous avons vu différents langages compilés et différents langages interprétés.
  • Notez que cette distinction ne s’aligne pas avec la dichotomie Déclaratif / Impératif.
    • Certains langages supportent même les deux : le code Haskell peut être interprété et compilé.
    • Souvent, les langages compilés ont un système de types plus strict, simplement parce que l’on peut imposer la sécurité des types à la compilation et éliminer toute une catégorie d’erreurs de programmation.

Langages compilés

  • Système de types plus strict
  • Optimisations avant l’exécution
  • Le bytecode est habituellement lié à une plateforme spécifique
Pourquoi habituellement ?

Parce que l’on peut avoir les deux : un compilateur et un interpréteur. Java en est un exemple : le code est compilé, mais doit encore être interprété par une JVM.

Interprétés

  • Souvent des scripts
  • Souvent une syntaxe plus simple
  • Souvent pas de système de types strict (ou pas de types du tout)

Langages ésotériques

En guise de conclusion, les paradigmes ne sont pas toujours pragmatiques. La communauté des langages de programmation a parfois tendance à inventer des paradigmes absurdes, juste pour démontrer des concepts et introduire un nouveau langage.

Voici quelques-uns de mes préférés :

Whitespace

  • Whitespace n’a que deux caractères :
    • Espace
    • Tabulation
  • Avec cela, vous avez des représentations de 0 et de 1, donc vous pouvez écrire tout ce que vous voulez en binaire.
  • Voici un programme "Hello, World!" en Whitespace :

Crédit image : Wikipedia

Vous ne voyez rien ? Moi non plus, c’est le but :)

Une astuce académique

Si votre professeur ne précise pas dans quel langage vous devez faire votre devoir, rendez une feuille vide en précisant que vous l’avez implémenté en Whitespace et que vous avez imprimé la solution pour leur commodité.

Ook!

Ook! est un autre langage sophistiqué. Techniquement, ce n’est qu’une variante du langage Brainfuck (un langage conçu pour être le plus impossible à utiliser possible).

  • Ook! n’a que quelques commandes, et elles se ressemblent toutes, mais tout est possible.
  • Voici un programme Hello, World! écrit en Ook! :
Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook! Ook? Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook! Ook! Ook? Ook! Ook? Ook.
Ook! Ook. Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook! Ook? Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook?
Ook! Ook! Ook? Ook! Ook? Ook. Ook. Ook. Ook! Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook. Ook! Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook! Ook. Ook. Ook? Ook. Ook? Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook? Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook? Ook! Ook! Ook? Ook! Ook? Ook. Ook! Ook.
Ook. Ook? Ook. Ook? Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook! Ook? Ook? Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook? Ook! Ook! Ook? Ook! Ook? Ook. Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook.
Ook? Ook. Ook? Ook. Ook? Ook. Ook? Ook. Ook! Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook.
Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook!
Ook! Ook. Ook. Ook? Ook. Ook? Ook. Ook. Ook! Ook.

Piet

Piet combine la programmation avec l’art moderne. Le code est une toile, et l’exécution du programme correspond à déplacer un pointeur horizontalement ou verticalement à travers la toile. Le pointeur continue dans la même direction jusqu’à ce qu’il rencontre un changement de couleur :

Voici un programme Hello, World! en Piet :

Littérature

Inspiration et lectures supplémentaires pour les esprits curieux :

Voici le lien pour passer à l’unité de laboratoire : Lab 02