Premier billet d'une série au fil de ma lecture de [7].
Dans le chapitre premier, Simondon écrit:
Il s'intéresse donc à la génèse des objets techniques, ce qui l'amène à décrire un processus de concrétisation, faisant passer un objet abstrait à un objet concret, ou plutôt d'un objet plus abstrait (ou moins concret) à un objet moins abstrait (ou plus concret).
Simondon décrit l'objet technique abstrait comme composé d'éléments possédant chacun une fonction unique et intéragissant peu ou pas avec les autres:
Au contraire, l'objet technique concret est tel un moteur actuel où "chaque pièce importante est tellement rattachée aux autres par des échanges réciproques d'énergie qu'elle ne peut pas être autre qu'elle n'est." ([7] page 23)
De plus, l'objet abstrait présente des défauts qui seront corrigés dans les évolutions ultérieures de l'objet, dûs au fait qu'il n'y a pas de synergie entre les éléments assurant les différentes fonctions.
Enfin, le moteur concret d'aujourd'hui est le moteur abstrait de demain, puisque le moteur évolue par un processus de concrétisation.
Prenons maintenant un exemple pour voir comment ces idées s'appliquent aux objets techniques que sont les logiciels.
Imaginons que nous souhaitons définir une fonction prenant en entrée une liste de listes d'entiers, et affichant ces listes d'entiers ainsi que leur moyenne respective, triées par ordre croissant.
L'algorithme est simple: trier la liste des listes en entrée selon leur moyenne, puis afficher la liste triée.
En OCaml1, nous pourrons ainsi définir une fonction retournant la moyenne d'une liste d'entiers:
# let average = function [] -> assert false (* ne traitons pas les listes vides pour l'exemple *) | l -> let sum = List.fold_left (+) 0 l in (float sum) /. (float (List.length l)) ;; val average : int list -> float = <fun>
Puis nous définissons une fonction de tri de deux listes, d'après leur moyenne respective:
# let sort_list = let compare_list l1 l2 = compare (average l1) (average l2) in fun list -> List.sort compare_list list ;; val sort_list : int list list -> int list list = <fun>
Nous définissons egalement la fonction d'affichage d'une liste et sa moyenne:
# let print_list = let print_one list = let avg = average list in let s_list = List.map string_of_int list in let s = Printf.sprintf "[%s] => %f" (String.concat " ; " s_list) avg in print_endline s in fun list -> List.iter print_one list ;; val print_list : int list list -> unit = <fun>
Enfin, la fonction sort_and_print sera la fonction représentant notre objet technique. Elle prent une liste de listes d'entiers et affiche les listes et leurs moyennes triées par ordre croissant selon leurs moyennes. La fonction se borne à composer la fonction de tri et la fonction d'affichage:
# let sort_and_print list = print_list (sort_list list);; val sort_and_print : int list list -> unit = <fun>
# let data = [ [ 1 ; 2 ; 3 ; 4 ] ; [ 1 ; 3 ; 4 ; 2 ] ; [ 10 ; -2 ; 5 ; 12 ] ; [ -1 ; -2 ; 5 ] ; ];; val data : int list list = [[1; 2; 3; 4]; [1; 3; 4; 2]; [10; -2; 5; 12]; [-1; -2; 5]] # sort_and_print data;; [-1 ; -2 ; 5] => 0.666667 [1 ; 2 ; 3 ; 4] => 2.500000 [1 ; 3 ; 4 ; 2] => 2.500000 [10 ; -2 ; 5 ; 12] => 6.250000 - : unit = ()
La fonction sort_and_print, bien que correcte, présente un défaut: Elle calcule plus d'une fois la moyenne de chaque liste. En effet, les deux fonctions qui la composent (sort_list et print_list) appellent chacune la fonction average. Le tri de la liste effectue même le caclul de la moyenne à chaque comparaison de liste.
sort_and_print est donc un bon exemple d'objet technique abstrait, selon la définition de Simondon: Il est composé de deux "éléments définis par leur fonction complète et unique". sort_list et print_list prennent en effet une liste d'entiers et font tous les calculs nécessaires pour remplir leur fonction: trier et afficher.
Dans son ouvrage, Simondon s'appuie sur des exemples mécaniques et électriques, et les problèmes posés par les objets techniques abstraits sont par exemples l'échauffement, la pression, la précision d'un faisseau d'électrons. En ce qui concerne les logiciels, les problèmes posés par des fonctions abstraites au sens de Simondon seront plutôt à chercher dans un surplus de temps ou d'occupation mémoire pour effectuer les opérations.
C'est le cas pour notre objet technique: la fonction sort_and_print prend bien plus de temps que nécessaire à cause des appels répétés à la fonction de calcul de la moyenne.
Pour l'améliorer, nous pouvons effectuer le calcul des moyennes une fois pour toutes, puis utiliser le résultat à la fois dans la fonction de tri et dans la fonction d'affichage. La fonction average reste inchangée mais les fonctions de tri et d'affichage doivent être modifiées pour utiliser les moyennes préalablement calculées. Ainsi, ces deux fonctions ne prendront plus seulement une liste de listes d'entiers (de type int list list) mais une liste de listes de paires (moyenne, liste d'entiers) (donc de type (float * int list) list).
# let sort_list = let compare_list (avg1, l1) (avg2, l2) = compare avg1 avg2 in fun list -> List.sort compare_list list ;; val sort_list : ('a * 'b) list -> ('a * 'b) list = <fun> # let print_list = let print_one (avg, list) = let s_list = List.map string_of_int list in let s = Printf.sprintf "[%s] => %f" (String.concat " ; " s_list) avg in print_endline s in fun list -> List.iter print_one list ;; val print_list : (float * int list) list -> unit = <fun>
Notre fonction sort_and_print voit alors sa structure légèrement modifiée suite au réagencement des fonctions:
# let sort_and_print list = (* calcul de la moyenne de chaque liste, en gardant la liste originale *) let list_with_avg = List.map (fun l -> (average l, l)) list in (* trie et affichage en utilisant la nouvelle structure *) print_list (sort_list list_with_avg) ;; val sort_and_print : int list list -> unit = <fun>
Vérifions que le résultat reste inchangé:
# sort_and_print data;; [-1 ; -2 ; 5] => 0.666667 [1 ; 2 ; 3 ; 4] => 2.500000 [1 ; 3 ; 4 ; 2] => 2.500000 [10 ; -2 ; 5 ; 12] => 6.250000 - : unit = ()
La transformation que nous avons faite subir à notre fonction sort_and_print relève de ce que Simondon appelle le processus de concrétisation: Il s'agit de supprimer les défauts de la première version, non pas en cherchant uniquement à séparer la fonction de calcul des fonctions de tri et d'affichage, mais également en augmentant la synergie entre les fonctions.
En effet, les fonctions de tri et d'affichage se contentent maintenant de trier et d'afficher, les calculs des moyennes étant faits en amont. Les fonctions communiquent par une structure de donnée enrichie (contenant la liste et la moyenne), ce qui permet aux fonctions de fonctionner en synergie.
Cette façon de procéder est décrite par Simondon:
Evidemment, l'exemple de code montré ici est simple, mais l'approche reste valable à large échelle et pour des logiciels plus compliqués. Ce processus de concrétisation est souvent appelé "optimisation" en développement logiciel.
Par ailleurs, même si le développeur expérimenté verra d'emblée comment avoir un code plus concret comme dans l'exemple ci-dessus, il raisonnera tout de même en deux étapes: d'abord penser un objet abstrait, puis un objet concret qu'il implémente.
Lorsque le problème est plus ardu, l'historique du code reflètera les évolutions: des fonctions abstraites (au sens de Simondon), concrétisées au fur et à mesure que le problème est mieux compris et que l'objet technique est spécialisé.
Le processus de concrétisation décrit par Simondon s'applique bien au processus de développement d'un logiciel, comme nous venons de le voir. Reste à savoir jusqu'où il faut suivre ce processus. Dans un prochain billet, nous verrons notamment les deux types de perfectionnements, continu et discontinu. A suivre, donc...