juin
2009
Bonsoir,
Il y a peu, on m’a demandé d’écrire un programme qui récupère un flux Atom XML et qui ne récupère que le titre et l’url des éléments (qui en l’occurence sont des billets aggrégés sur Planet OCaml).
J’ai hésité entre OCaml, C++ et Haskell. Plutôt tenté par du fonctionnel, mon choix s’est vite porté sur Haskell grâce au nombre impressionnant de paquets présents sur Hackage.
J’ai donc opté pour le paquet feed pour la gestion d’Atom et download pour la récupération du XML distant, qui se situe ici).
Alors, voyons voir… Premièrement, on importe les modules dont on a besoin, évidemment.
import Text.Atom.Feed
import Text.Feed.Import
import Text.Feed.Types
Ensuite, on définit l’adresse du fichier atom.xml… Toujours rien de bien sorcier.
Puis on se lance dans main !
putStrLn "*** Recent blog posts ***"
Right src <- openURIString url
Ici, on affiche simplement un message dans la console, puis on récupère le contenu du fichier dans la chaîne de caractères src.
Right est l’un des constructeurs du type Either, ne vous en préoccupez pas trop.
Ensuite, on s’occupe à proprement parler du flux Atom…
Just est un constructeur du type Maybe, qui permet de gérer les erreurs (Just mavaleur si pas eu d’erreur, Nothing si une erreur). AtomFeed est lui un constructeur pour le type Feed précisant que c’est un flux Atom, et non RSS1 ou RSS2 par exemple (qui sont eux aussi gérés par ce même paquet). La fonction parseFeedString prend donc une chaîne (ici src) et retourne un flux Atom… C’est là, en utilisant is, que l’on va pouvoir récupérer les différents éléments du XML au format Atom.
Bon, et si on récupérait les différentes entrées de notre flux (ici ce sont des billets de blog) ?
Ceci nous retourne donc une liste d’Entry. Et c’est parti, on va récupérer les informations qu’il nous faut, puis les afficher !
mapM_ (\(x,y) -> putStrLn $ (stringize x) ++ ": " ++ y) infos
Oui, oui, ça se complique un peu.
Tout d’abord, qu’est-ce que infos ? map transforme chaque élément en l’image de l’élément donné par son premier argument, qui est donc une fonction. Ici, on va transformer chaque Entry en un couple (titre, id) associé à notre entrée, où id se trouve être l’URL originale du billet.
Bon infos est donc la liste des couples (titre, url). Ah ? Ce n’étant pas tellement cette ligne qui vous faisait peur mais la suivante ?
Bon, sans rentrer en détail dans les monades (la page Monad du HaskellWiki le faisant mieux que moi, et surtout donnant d’excellents liens pour comprendre de quoi il s’agit, où c’est utilisé, etc), on se trouve dans la monade IO. mapM_ se retrouve donc avec le type :
Notre [a], c’est infos, donc notre liste de couples…
(a -> IO b) est effectivement le type de notre fonction anonyme, que je me permets de vous montrer à part ici :
A un couple (titre,url), elle associe un appel à putStrLn, qui retourne IO (). Donc le type b est en fait (). mapM_ ne faut qu’effectuer des actions dans une monade donnée, en ignorant le résultat de chacune au lieu de les placer des une liste comme le fait son homologue mapM
Ah oui, j’oubliais, stringize, qui est définie juste après fonction main.
stringize (TextString s) = s
stringize _ = error "shoud not be called on something else than TextString"
Elle me permet juste de passer d’une valeur construite avec TextString, donc de type TextContent, à la valeur qui est en fait englobée par ce type, de type String.
Voilà donc le code complet :
import Text.Atom.Feed
import Text.Feed.Import
import Text.Feed.Types
url = "http://planet.ocamlcore.org/atom.xml"
main = do
putStrLn "*** Recent blog posts ***"
Right src <- openURIString url
let Just (AtomFeed is) = parseFeedString src
let entries = feedEntries is
let infos = map (\e -> (entryTitle e, entryId e)) entries
mapM_ (\(x,y) -> putStrLn $ (stringize x) ++ ": " ++ y) infos
stringize :: TextContent -> String
stringize (TextString s) = s
stringize _ = "stringize Error"
La compilation :
$ ghc -package download -o cwn cwn.hs
Et un exemple d’execution :
$ ./cwn
*** Recent blog posts ***
ocaml-text: http://forge.ocamlcore.org/projects/ocaml-text/
0.1.3 sources now in subversion: http://forge.ocamlcore.org/forum/forum.php?forum_id=355
Sudoku in ocamljs, part 2: RPC over HTTP: tag:blogger.com,1999:blog-1445545651031573301.post-3490486535879812384
Caml Weekly News, 28 Apr 2009: http://alan.petitepomme.net/cwn/2009.04.28.html
Bouncing Ball in OCaml with OCamlSDL: http://blog.mestan.fr/?p=31
Sudoku in ocamljs, part 1: DOM programming: tag:blogger.com,1999:blog-1445545651031573301.post-4574121943207730951
Using OCaml’s module functors to provide monadic contexts for Batteries: http://blog.mestan.fr/?p=30
Lastfm no longer free as in free beer (and some bits about xml in OCaml): http://blog.rastageeks.org/spip.php?article34
Last lecture: http://dutherenverseauborddelatable.wordpress.com/?p=571
Liquidsoap now supports AAC+ encoding.: http://blog.rastageeks.org/spip.php?article33
Alors, pas si « académique » que ça le fonctionnel, non ?
Tout ça en 18 lignes, lignes vides comprises, 15 non comprises.
Enjoy.
PS : on pourrait raccourcir et rendre le code moins compréhensible en utilisant des opérateurs et moins de valeurs intermédiaires, mais ce n’est pas l’objectif ici.
Désolé de décevoir tes attentes mais je n’ai pas vraiment l’intention de faire l’équivalent en OCaml. Si je le faisais ça serait en étendant mon module Lex pour lui permettre de parser du xml. Sauter à la balise fermante, capturer le contenu d’une balise, c’est à peu près les seules fonctions qui me manquent pour parser du xml en LL(1).
Oui, j’ai oublié de mentionner que les cas d’erreurs (non présence du fichier là où je le veux, mauvais parsing, etc) provoquent l’arrêt du programme. Ca n’aurait pas de sens d’essayer de faire autre chose si quelque chose échoue durant l’exécution. Une exception, par exemple, est assez explicite ici pour nous dire ce qui n’a pas marché.
Pour faire un équivalent en OCaml, ça sera un peu plus long/tricky, étant donné qu’il n’y a si je ne m’abuse pas de bibliothèques qui mâchent autant le travail.
Encore une bonne initiative.
C’est tellement compact qu’un programmeur impératif pourrait croire que tu ne traite pas le cas où openURIString et parseFeedString échouent (ces cas sont traités silencieusement, il y a un filtrage partiel qui s’il échoue met fin au programme en libérant les ressources).
Tu as eu la bonne idée de fournir la ligne de commande pour la compilation. C’est un détail qui a son importance, j’y penserai à l’avenir.