Forum Programmation.autre Paradigme fonctionnel : juste un habillage ?

Posté par (page perso) .
Tags : aucun
3
26
oct.
2012

Bonjour à tous,

Je suis en train de m'essayer au fonctionnel, en ce moment ocaml. Mais ma question n'est pas sur le langage mais sur la manière de construire du code fonctionnel. Je soumet ici un bout de code que j'ai fait pour tester qui permet de formater du texte en spécifiant la largeur maximale de chaque ligne :

(** Une simple fonction de sortie *)
let printer text =
  print_endline text;;

(** Formateur de texte qui va réécrire le texte donné sur un largeur maximale de length *)
let format f length =
  let buffer = Buffer.create length

  (** Affiche la ligne courante sur la sortie, et relance une ligne vide *)
  in let finish () =
          f (Buffer.contents buffer);
          Buffer.reset buffer;

  (** Ajoute le texte donné en paramètre au texte en cours. Si la 
    * longueur dépasse, l'affiche sur une nouvelle ligne *)
  in let join text =
    let rec assemble extracts =

      match extracts with
      | [] -> ()
      | head::tail ->
          if (Buffer.length buffer) + (String.length head) > length then
            finish();
          Buffer.add_string buffer head;
          if tail != [] then
           Buffer.add_string buffer " ";
          assemble tail
    in assemble (Str.split_delim (Str.regexp "[ \t]") text)

  in join, finish;;

let concat, terminate = format printer 20 in
  concat "Ceci";
  concat " est un";
  concat "e chaine assez ";
  concat "longue, voir";
  concat "e même trop lo";
  concat "ngue ! Tellement longue que l'on peut se demander si elle va tenir !";
  concat " sur une seule ligne";
  terminate();;

Le code fonctionne bien et produit le résultat escompté :

Ceci est une chaine
assez longue, voire
même trop longue !
Tellement longue que
l'on peut se
demander si elle va
tenir ! sur une
seule ligne

Pourtant, en relisant le code, je suis en train de m'interroger : je suis parti dans l'idée de faire du code fonctionnel, et je l'ai construit comme tel, mais avec le recul, j'ai l'impression d'avoir écrit une classe objet.

Si je reprend le code :

Voilà qui ressemble à un constructeur de classe avec ses paramètres :

let format f length =

Ça c'est une variable privée :

  let buffer = Buffer.create length

Enfin je déclare les méthodes de ma classe pour que l'on puisse les utiliser à l'extérieur, j'instancie ma classe, et exécute ses fonctions. :

  in let finish () =
  
  in let join text =
  
  in join, finish;;

let concat, terminate = format printer 20 in

Je sais bien que fonctionnel/objet ne sont que des représentations d'un même code, mais je ne m'attendais pas à avoir une telle similitude dans la présentation du code. Je vois bien que j'ai utilisé des concepts propre au fonctionnel (pattern matching, code récursif, fermeture) mais je m'attendais à quelque chose de différent.

D'où ma question : est-ce que mon interrogation est naïve, et je ne fait que constater des évidences, ou bien est-ce moi qui suis trop imbibé de code objet et n'ai pas réussi à m'en détacher ? J'accepte que l'on me réponde que le code est trop simpliste pour pouvoir mettre en évidence une différence, mais je voulais justement tester un cas réel et éloigné des constructions mathématiques que l'on retrouve habituellement en exemple…

Sinon je trouve le langage vraiment agréable, très simple dans la syntaxe, dans lequel on retrouve vite ses marques. Je vais continuer à le tester encore un petit moment…

  • # concepts propre au fonctionnel?

    Posté par . Évalué à 2.

    Je faisais du code récursif en Pascal alors la récursion comme concept propre au fonctionnel..
    De même le pattern matching est très similaire au case of/select..
    La fermeture, OK, ça c'est quelque-chose qui vient du fonctionnel effectivement.

    J'ai lu sur Scala que c'était un mélange fonctionnel/objet donc les deux me paraissent non-exclusif en effet.

    Pour le reste, le fonctionnel pur ça serait plutôt en Haskell qu'en OCaml.

  • # Une solution en F#

    Posté par (page perso) . Évalué à 3.

    Je n'ai pas de compilateur OCaml sous la main, mais voici une proposition en F#. La traduction en OCaml devrait être simple.

    open System
    
    let split (s : String) =
     let rec loop i j xs =
      if i = 0 then
       if j > 0 then
        s.[0 .. j - 1] :: xs
       else
        xs
      else
       if s.[i - 1] = ' ' then
        loop (i - 1) (i - 1) (s.[i .. j - 1] :: xs)
       else
        loop (i - 1) j xs
     let n = String.length s
     loop n n []
    
    let format width xs =
     let rec loop rs line rems =
      match rems with
      | [] -> List.rev (line :: rs)
      | hd :: tl ->
       match line with
       | "" ->
        loop rs hd tl // problème si un mot est plus long que width
       | _ ->
        let line' = line + " " + hd
        if String.length line' <= width then
         loop rs line' tl
        else
         loop (line :: rs) hd tl
     loop [] "" xs
    
    let s =
     [
      "Ceci"
      " est un"
      "e chaine assez "
      "longue, voir"
      "e même trop lo"
      "ngue ! Tellement longue que l'on peut se demander si elle va tenir !"
      " sur une seule ligne"
     ]
     |> String.concat ""
    
    Console.WriteLine("Original string: " + s)
    Console.WriteLine()
    
    let xs = split s
    
    Console.WriteLine("Split string:")
    for x in xs do
     Console.WriteLine("[" + x + "]")
    Console.WriteLine()
    
    let lines = format 20 xs
    
    Console.WriteLine("After formatting:")
    for line in lines do
     Console.WriteLine(line)
    
    

    La fonction split découpe la chaîne passée en paramètre et renvoie une liste de chaînes. La fonction format prend une liste de chaines et renvoie une liste de chaînes (une par ligne) en s'assurant qu'aucune ligne ne dépasse la largeur maxi spécifiée (sauf si un élément de la liste d'entrée est lui-même plus long que ladite largeur maxi).

  • # Ben, non, pas vraiment…

    Posté par . Évalué à 2.

    C'est rigolo comme approche. Effectivement, le comportement d'un code objet est parfaitement émulé.
    Ce code est très impératif. La fermeture utilisée pour instancier une classe, généralement on s'en sert pour aider le compilo à ne pas évaluer une expression deux fois, ou partager une fonction partiellement appliquée (par exemple pour ne compiler qu'une fois une expression régulière) et l'appliquer ensuite sur plein de chaines).
    Le pattern matching concrètement c'est pas différent d'un filtrage avec des instanceof en java, c'est juste beaucoup beaucoup plus sucré. La syntaxe primitive est le match foo with | bar => 3 | baz => 1 | …

    Une approche fonctionnelle pourrait consister à construire les lignes récursivement en piochant dans une liste et en décomptant les caractères.

    Ça c'est une variable privée :
    let buffer = Buffer.create length

    Oui et non, buffer n'est pas une variable, c'est une déclaration. Seulement la valeur attachée à buffer, elle, est une variable, et n'a rien de fonctionnel. Ça viole la transparence référentielle.

    fonctionnel/objet ne sont que des représentations d'un même code

    Ça dépend comment on défini "même code", parce que par exemple, pour la factorielle entre
    acc = 1; while(n) acc *= n--;
    et
    (\f.\x.f(x x) \x.f(x x)) (\f.\n.if n == 0 then 1 else n * f (n-1)) n
    il y a un monde.
    Dans certains langages de programmation fonctionnel, c'est vraiment la seconde version qui est exécutée. Il y a le graphe de l'expression qui est construit dans le tas et l'évaluation se fait en réduisant ce graphe.

    D'autre part j'ai tendance à penser que LA caractéristique des langages fonctionnels, aussi importante que la récursivité est la pureté. L'idée c'est d'écrire des maths dans un programme et que l'ordinateur les mouline.

    est-ce que mon interrogation est naïve, et je ne fait que constater des évidences, ou bien est-ce moi qui suis trop imbibé de code objet et n'ai pas réussi à m'en détacher ?

    OCaml est un langage multi-paradigme. Avec Haskell par exemple il est impossible d'écrire ce genre de chose sans encapsulation.

    Please do not feed the trolls

    • [^] # Re: Ben, non, pas vraiment…

      Posté par . Évalué à 3.

      format :: Int -> String -> [String]
      format n s = let format' [] = []
                       format' wds = let (d,fin) = aux n wds
                                     in (unwords d) : format' fin
                   in  format' $words s
                      where 
                        -- aux retourne un couple (premiere ligne, reste du texte)
                        aux c [] = ([],[])
                        aux c (w:wds) = let lw = length w 
                                        in if c - lw - 1 < -1
                                           then  ([],w:wds)
                                           else let (d,fin) = aux (c - lw - 1) wds
                                                in (w:d,fin)
      
      main = putStr $ unlines  $ format 20 $ concat [   
        "Ceci",
        " est un",
        "e chaine assez ",
        "longue, voir",
        "e même trop lo",
        "ngue ! Tellement longue que l'on peut se demander si elle va tenir !",
        " sur une seule ligne"]
      
      

      J'ai un peu galéré pour compter les espaces… et évité les trucs très propre à haskell. Il y a très forcement une solution bien plus élégante. Mais ce code est pur, et donc est possible de faire format10 = format 10 et d'utiliser cette fonction de manière concurrente.

      Please do not feed the trolls

      • [^] # Re: Ben, non, pas vraiment…

        Posté par . Évalué à 4.

        Une autre différence assez fondamentale (propre à haskell) est que cette fonction fonctionne sur les flux infinis, au moment où la première ligne "ceci est une chaine" est imprimée sur l'écran, concat ["e même trop lo","ngue ! … n'a toujours pas été évalué.

        Please do not feed the trolls

  • # Naïvité

    Posté par (page perso) . Évalué à 3.

    D'où ma question : est-ce que mon interrogation est naïve,

    Ton interrogation est effectivement très naïve: tu remarques que tu as une structure du données, une procédure pour l'initialiser et des traitements sur cette structure. Cela veut juste dire que tu as introduit (sans le dire) un type de données, cela n'a rien à voir avec la programmation orientée objet. Ce qui caractérise la programmation objet c'est la relation d'héritage et les constructions qu'elle permet. Tu ne parles pas d'héritage, donc tu ne parles pas de programmation objet.

    Je sais bien que fonctionnel/objet ne sont que des représentations d'un même code,

    C'est faux, tu ne peux pas facilement traduire tout programme écrit pour le langage objet beta dans un langage fonctionnel alpha, et vice-versa.

    J'accepte que l'on me réponde que le code est trop simpliste pour pouvoir mettre en évidence une différence, mais je voulais justement tester un cas réel et éloigné des constructions mathématiques que l'on retrouve habituellement en exemple…

    Je ne comprends pas trop ce qu'il y a de plus réel dans ton exemple que dans d'autres… Si les exemples de ton livre de programmation sont mauvais, le livre est mauvais: il ne t'apprends pas à programmer. Le langage Caml est un des meilleurs livres de programmation qui soit, tu peux le télécharger sur le site de l'INRIA:

    http://caml.inria.fr/pub/distrib/books/llc.pdf

    (Attention le sujet est Caml light pas OCaml, donc il y a de petites différences, la plus importante sont les parsers de flots qui sont un composant externe dans OCaml).

    Je vois bien que j'ai utilisé des concepts propre au fonctionnel (pattern matching, code récursif, fermeture) mais je m'attendais à quelque chose de différent.

    Attention, même si c'est un point fort de OCaml le pattern matching n'a rien à voir avec le paradigme de programmation fonctionnelle.

    Quelques remarques en vrac sur ton code:

    1. Tu devrais le réécrire pour n'appeler qu'une fois Str.regexp.
    2. concat a; concat b se réécrit List.iter concat [a; b]: la séparation code et données est plus claire.
    3. Pour ton code la regexp n'apporte vraiment rien, tu devrais essayer de traduire ton code en transformant ton texte en char Stream.t.

    Courage avec OCaml, n'hésite pas à poser tes questions!

  • # Merci

    Posté par (page perso) . Évalué à 2.

    Merci à tous pour vos réponses.

    Tout d'abord, je reconnais que le code n'est pas pur dans le sens où son exécution produit des effets de bord dans la fermeture. Mais cela est causé par la manière dont je compte m'en servir : la liste d'appel à concat sera remplacé par un gestionnaire SAX. Je doit donc être capable de traiter les informations au fur et à mesure de leur arrivée.

    Sinon il est vrai que mettre en place une structure de données n'a pas grand chose à voir avec de l'objet, mais j'aurai aimé ne pas utiliser la fermeture comme une structure.

    Histoire de me prendre la tête, j'ai essayé une solution sans utiliser de fermeture, et l'autre solution trouvée aurait été d'utiliser un yield (comme en python) pour interrompre le traitement, ce qui ne semble pas possible en OCaml. (J'ai essayé de le simuler en retournant une fonction partielle dans un code récursif sans succès).

    Bon, je vais continuer d'expérimenter tout ça, finalement mon exemple me semble suffisamment riche pour que je puisse essayer différentes approches

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.