Tout est fonction : retour sur le cours SICP de David Beazley
(ezzeriesa.notion.site)Retour sur le cours SICP de David Beazley : une semaine d’expérience
Je partage ici mon expérience de participation au cours SICP de David Beazley à la fin de l’année 2022. Il existe de nombreuses ressources gratuites, mais le cours de Dave s’est révélé très efficace en choisissant des sujets précis et en les expliquant en profondeur.
Point de départ
Le cours SICP était donné en langage Scheme, et on y expliquait le modèle fondamental de substitution en implémentant un interpréteur Scheme simple en Python.
Bases du langage Scheme
- Primitives : valeurs de base (par ex. des entiers)
- Opérateurs : opérations de base comme
+,-,*,/, utilisées en notation préfixée - define : définition de variables
> (define x 2)
> (+ x 3) ; résultat : 5
- if : conditionnelle
- lambda : définition de fonctions anonymes
> ((lambda (x) (* x x)) 3) ; résultat : 9
Interpréteur Scheme en Python
Implémentation d’un interpréteur simple qui évalue du code Scheme en utilisant Python. Les opérations de base sont définies comme des fonctions Python.
definitions = {
"+": lambda x, y: x + y,
"*": lambda x, y: x * y,
}
Exemple :
> evaluate(("+", 2, 3)) # résultat : 5
L’implémentation couvre aussi define, lambda et le traitement de la conditionnelle if.
Modèle de substitution (Substitution Model)
Le modèle de substitution est une manière simple d’interpréter un programme : on évalue le programme en remplaçant les variables par leurs valeurs. Mais ce modèle échoue dès qu’on introduit une affectation (assignment).
État (State)
L’affectation (assignment) est un bon exemple de situation où le modèle de substitution se brise. Par exemple, lorsqu’on modélise le solde d’un compte bancaire, on met à jour la variable avec set!.
(define balance 100)
(define (withdraw amount)
(set! balance (- balance amount))
balance)
Dans ce cas, le modèle de substitution ne permet pas de distinguer l’état du solde avant et après l’opération.
Le modèle d’environnement (Environment) devient alors nécessaire. Les variables y sont définies dans un environnement, et chaque procédure possède son propre environnement.
Flux (Streams)
Les streams constituent une autre manière de modéliser l’état. Grâce à l’évaluation paresseuse (lazy evaluation), ils permettent aussi de modéliser des valeurs futures.
Boucle infinie et ordre d’évaluation
Différence d’ordre d’évaluation : la plupart des langages utilisent l’évaluation en ordre applicatif (applicative-order evaluation), qui évalue d’abord les arguments.
> (square (+ 1 2)) ; résultat : 9
Mais l’évaluation en ordre normal (normal-order evaluation) retarde l’évaluation des arguments jusqu’au moment où ils deviennent réellement nécessaires. Cela permet d’éviter certaines boucles infinies.
> (define (p) (p))
> (define (test x y) (if (= x 0) 0 y))
> (test 0 (p)) ; renvoie 0 en ordre normal, boucle infinie en ordre applicatif
Calcul lambda et numéraux de Church
L’encodage de Church permet de représenter les nombres comme des procédures. C’est un concept important de la programmation fonctionnelle.
(define (zero f) (lambda (x) x))
(define (increment n) (lambda (f) (lambda (x) (f ((n f) x)))))
zeroest une fonction qui renvoie simplement son argument (fonctionidentity).incrementapplique un appel de fonction supplémentaire.
Exemple
> ((zero (lambda (x) (+ x 1))) 0) ; résultat : 0
> (((increment zero) (lambda (x) (+ x 1))) 0) ; résultat : 1
Itération vs récursion
Scheme utilise la récursion plutôt que des boucles for pour effectuer les tâches répétitives.
Exemple de récursion : factorielle
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
Cet appel récursif peut consommer beaucoup de mémoire en utilisant la pile.
Optimisation des appels terminaux (Tail-call optimization)
Scheme réduit l’usage mémoire grâce à l’optimisation des appels terminaux, ce qui lui permet de fonctionner comme un processus itératif.
(define (factorial n)
(define (iter product counter)
(if (> counter n)
product
(iter (* product counter) (+ counter 1))))
(iter 1 1))
Conclusion
Le cours de David Beazley aborde en profondeur, à partir d’une sélection ciblée, les concepts majeurs de SICP. Il aide en particulier à mieux comprendre différents paradigmes de programmation, notamment la programmation fonctionnelle, le calcul lambda et l’ordre d’évaluation.
Citation de Knuth
Si vous n’étudiez que la théorie, cela signifie qu’il est temps de vous concentrer sur la pratique ; et si vous ne faites que de la pratique, cela signifie qu’il est temps de vous concentrer sur la théorie.
1 commentaires
Commentaires sur Hacker News
À titre d’avertissement, se plonger profondément dans SICP/Lisp/Scheme peut changer votre façon de penser la programmation, et ce genre de stimulation intellectuelle est toujours bienvenu.
Mais appliquer ces idées en bloc à une base de code orientée objet produit généralement l’effet inverse ou suscite la résistance de l’équipe.
Par exemple, après Lisp, on peut avoir envie de remplacer toutes les boucles
forpar desforEach, ou de tout transformer en chaînesmap/reduce, mais si le langage n’embrasse pas pleinement la programmation fonctionnelle, cela peut nuire à la fois à la lisibilité et aux performances.Au final, se rappeler que la mémoire mutable et le CPU exécutent le code aide à garder les pieds sur terre ; aujourd’hui, je trouve qu’au quotidien, la conception orientée données et l’« empathie mécanique » adaptée aux réalités du hardware sont plus pratiques que des abstractions comme les nombres de Church.
Moins il y a d’effets de bord, plus le comportement est prévisible, et l’orienté objet est souvent utilisé même là où de simples fonctions suffiraient.
Dans le développement de jeux, avec beaucoup de calculs et de code à risque, l’orienté objet convient plutôt bien ; mais dans le développement web, la programmation fonctionnelle est bien plus naturelle, et dans le SaaS, des langages comme Elixir permettent d’écrire du code plus fiable, avec moins de bugs et plus facile à tester.
En plus, des choses comme
forEachetmap/reduceexistaient déjà dans les collections Smalltalk, et ont aussi été reprises dans Object Pascal et C++, même si ce n’était pas une syntaxe de première classe.Comme la mémoire sous-jacente est mutable, même les langages de la famille ML disposent de mécanismes pour gérer la mutation quand c’est vraiment nécessaire.
goto, maisgotoest encore moins populaire que Lisp.Ce qui est encore plus étrange, c’est que sur les CPU modernes, les caches sont indispensables aux performances, et pourtant aucun langage ne traite encore la hiérarchie mémoire comme un citoyen de première classe.
Ce qui s’en rapproche le plus semble être du C à la Linux, avec une mer de traits de soulignement.
Cela fournit une bonne introduction à l’encodage de l’état avec des fonctions pures.
En pratique, il existe beaucoup plus d’encodages purement fonctionnels pour toutes sortes de données : arbres, entiers, types somme/produit, images, monades, etc.
L’encodage peut être un peu déroutant, mais il est aussi élégant et compact.
Par exemple, en JavaScript, une implémentation fonctionnelle de la monade Maybe donne ceci :
Nothing = nothing => just => nothingJust = v => nothing => just => just(v)pure = Justbind = mx => f => mx(mx)(f)evalMaybe = maybe => maybe("Nothing")(v => "Just " + v)console.log(evalMaybe(bind(Nothing)(n => pure(n + 1)))) // Nothingconsole.log(evalMaybe(bind(Just(42))(n => pure(n + 1)))) // Just 43data Maybe a = Nothing | Just afoldMaybe :: (Unit -> r) -> (a -> r) -> Maybe a -> rLes deux fonctions d’ordre supérieur passées à
foldMaybecorrespondent respectivement àNothingetJust.Cela dit, du côté de
Nothing, on ajoute un paramètreUnitpour être un peu plus précis.En théorie des types, c’est assez élégant, mais en programmation réelle, ce n’est pas franchement idéal.
J’avais déjà regardé le vrai cours SICP, c’est-à-dire les enregistrements de 1986 du MIT OCW.
On le félicite souvent pour sa forte densité d’information, mais en réalité pas mal de temps est perdu avec les questions-réponses des étudiants, les moments où l’enseignant attire l’attention sur une tentative de présentation « multimédia » dans la salle, ou encore le fait que le plan global du cours n’était pas entièrement calé à l’avance et ne permettait donc pas d’anticiper certaines questions.
Le temps passé à écrire au tableau devient aussi considérable une fois cumulé.
Bien sûr, l’ordre des supports peut toujours être discuté et réorganisé, et je prévois un jour de réaliser moi-même une série de vidéos expliquant ce contenu d’une manière qui corresponde à ma sensibilité.
Je suis content de voir que ce cours semble conserver ses racines tout en utilisant un langage plus moderne comme Python, et je pense que, comme Python est un langage multiparadigme pratique, les gens ne reconnaissent pas assez son expressivité au travers des idiomes fonctionnels, même si ce n’est pas une pureté totale.
À mon avis, Python pourrait, et devrait, être retiré de ce cours.
Python prend très mal en charge la programmation fonctionnelle.
Les listes ne sont pas fondées sur cons, les lambdas sont très limitées, le pattern matching est atroce et n’est pas basé sur des expressions, et les espaces de noms sont aussi étranges.
Python est difficilement un langage moderne : il est resté dans les années 1990, et il n’a grandi, malheureusement au détriment de meilleurs langages, que parce qu’il dispose d’une C-API correcte.
Comme il n’y a pas d’optimisation des appels terminaux, certains exercices de code nécessitent des solutions complètement différentes de celles en Scheme.
Si du code traduit à l’identique échoue, l’enseignant doit-il dire aux étudiants de simplement le croire parce que cela ne fonctionne pas ainsi à cause du langage choisi, ou bien faut-il considérer chaque cas comme un problème d’externalisation complète de la pile et le résoudre de cette façon ?
Forcer SICP à rentrer dans Python me semble assez idiot.
Le site est désormais hors ligne, mais les cours sont consultables sur archive.org.
https://en.m.wikipedia.org/wiki/ArsDigita#ArsDigita_Foundati...
https://archive.org/details/arsdigita_01_sicp/
Ils vendaient une clé USB contenant tout le programme ; ce serait vraiment bien si quelqu’un mettait l’ISO en ligne.
https://web.archive.org/web/20190222145553/aduni.org/drives/
Des choses qui s’expriment simplement en Scheme deviennent des exercices complexes dans d’autres langages.
Au lieu de se concentrer sur les concepts sous-jacents, les étudiants se retrouvent à se focaliser sur des détails du langage d’implémentation dont ils n’auraient pas à se soucier en Scheme.
La première fois que j’ai vu
cons/car/cdrimplémentés avec des lambdas, ça m’a paru magique.Mais au final, j’y vois surtout une démonstration du fait que le runtime du langage implémente un dictionnaire clé/valeur, et qu’on peut s’appuyer sur cette implémentation pour construire d’autres structures de données.
En Elixir, on peut extraire autant d’éléments qu’on veut depuis le début.
Une closure sans comportement n’est qu’un pointeur vers les variables capturées, et une closure avec deux pointeurs est une paire dont on peut obtenir
caretcdr.Le runtime doit permettre d’utiliser ce qui est pointé même en dehors de la définition, ce qui nécessite de l’analyse d’échappement, du garbage collection, etc., mais pas de dictionnaire.
Récemment, je suis tombé sur l’idée selon laquelle, en démonstration de théorèmes, pour prouver quelque chose comme
0 != 1, l’encodage de Church ne suffit pas et il faut des types de données inductifs.J’ai mis quelques éléments en vrac à ce sujet ici, avec aussi une critique distincte de SICP : https://intellec7.notion.site/Drinking-SICP-hatorade-and-why...
J’aimerais mieux comprendre les limites du point de vue selon lequel « tout n’est que fonction ».
0 ≠ 1.On peut déduire du théorème
f = g -> f x = g xun fait d’inégalité sur le membre de droite, puis en prendre la contraposée.Il me semble exact de dire qu’on ne peut pas prouver directement une inégalité entre nombres de Church sans autre fait sur les inégalités.
En revanche, avec les types de données inductifs, le système de preuve peut « observer » directement l’égalité ou l’inégalité en retirant récursivement l’application du constructeur le plus externe de deux instances concrètes du même type inductif.
Je me demande si l’on peut y parvenir avec l’encodage de Scott ou l’encodage de Church.
Le livre lui-même est déjà discuté ici : https://news.ycombinator.com/item?id=42157558
Je me demande s’il y a une raison pour laquelle le lien mène à une discussion plus bas dans la page plutôt qu’au début de celle-ci.
Peut-être pourrait-on fusionner cet article avec la discussion existante.
David Beazley est une figure assez légendaire dans le monde Python, et même si ce cours m’a d’abord semblé être une idée surprenante, après y avoir réfléchi deux secondes, ça m’a paru être une combinaison parfaite, donc je me suis inscrit au cours suivant.
Le point essentiel, c’est que ce type de format ressemble à ce que pourrait devenir la formation continue des ingénieurs logiciels.
Il y a une coquille dans le code de la section « the substitution model ».
("+", ("fib", ("-", "n", 2)), ("fib", ("-", "n", 1))),Ce qui est défini, c’est
fibonacci, etfibne l’est pas ; les deux appels àfibdevraient donc clairement êtrefibonacci.Le code du dépôt GitHub réel est correct : https://github.com/savarin/pyscheme/blob/0f47292c8e5112425b5...
Je pense que SICP est excellent.
Cela dit, plus j’apprends et plus j’étudie les maths, plus je me rapproche de la conclusion que les relations constituent un concept primitif plus fondamental.
Toute fonction peut être exprimée comme une relation de forme restreinte, mais l’inverse ne tient pas sans ajouter tout un appareillage supplémentaire.
Les bases de données relationnelles et SQL sont certes les exemples les plus connus et les plus réussis de programmation relationnelle, mais je pense que ce domaine reste encore largement inexploré.
En ce moment, mon intérêt porte moins sur la conception de langages de programmation que sur l’enseignement des bases des mathématiques à de très jeunes enfants.
Curieusement, enseigner un prédicat comme « est grand » comme une relation unaire, et « est plus grand que » comme une relation binaire, est bien plus facile que d’essayer de saisir le même concept sous forme de fonction.
Je n’aime pas trop la perspective « tout est fonction », car elle est trop simplificatrice et souvent peu utile.
Par exemple, il existe des fonctions qui ne tiennent pas dans le cache, la RAM, le disque, etc., des fonctions dont le Big O explose, comme les
JOINà N voies ou la recherche/le matching, et aussi des fonctions avec effets de bord, y compris non idempotentes.Presque personne ne pense aux attaques par canal auxiliaire sur les fonctions.
Il existe aussi des fonctions non déterministes qui dépendent de dates, d’heures, de durées, etc., et les fonctions peuvent échouer en cours de route, voire ne pas échouer proprement.
Il est également difficile de supposer qu’elles ne consomment pas de ressources qui affectent d’autres « fonctions » utilisant un pool de ressources partagé.
Les arguments d’une fonction peuvent être arbitrairement grands ou complexes, mais dans le monde réel il y a des limites ; il faut alors des pointeurs, puis à nouveau des références distantes comme le Web ou le disque.
Dites-moi quand m’arrêter, je pourrais continuer longtemps.
Dans ce cas, le modèle du lambda-calcul est le fondement de l’approche « tout n’est que fonction » ; il est très simple, tout en étant plus facile à manipuler et à raisonner que les machines de Turing, entre autres, ce qui en fait un bon modèle de calcul.
C’est pourquoi il se trouve à la base de la plupart des systèmes de logique informatique et de preuve.
Si une fonction a besoin d’une ressource, faites en sorte qu’elle demande cette ressource ; si elle dépend de la date/de l’heure, faites en sorte qu’elle dépende de la date/de l’heure ; si elle renvoie une valeur non déterministe, faites en sorte qu’elle renvoie une valeur non déterministe.
L’une des raisons pour lesquelles l’approche de la programmation fonctionnelle brille, c’est qu’elle oblige à traiter ces choses sérieusement.
Si vous voulez utiliser une ressource partagée implicitement, vous devez la modéliser, et une « fonction » qui dépend d’une ressource partagée implicite se distingue explicitement d’une vraie fonction.
Cela relève plutôt de la procédure.
Une fonction est une procédure, mais toutes les procédures ne sont pas des fonctions.