Forum Programmation.autre Haskell : simplifier 2 "case" imbriqués

Posté par . Licence CC by-sa
Tags :
1
12
mai
2016

Bonjour,
je viens de coder un petit truc vite fait en Haskell pour remplir des noms de ville à partir de codes postaux dans un fichier CSV. Ca marche, mais j'aimerais simplifier ma fonction "main" qui contient 2 "case" imbriqués, à cause de la fonction parseCSVFromFile qui a pour type :

parseCSVFromFile :: FilePath -> IO (Either ParseError CSV)

Je fais donc la gestion d'erreur à la main, mais vu que Either est une monade, j'aimerais que ça soit fait automatiquement. Et le fait que le Either soit encapsulé dans un IO complique un peu les choses. J'ai essayé d'utiliser liftM, mais je n'ai pas réussi.

Voici le code en question :

module Main (
    main
) where

import Text.CSV
import qualified Data.Map as Map

fromCSV :: CSV -> Map.Map String String
fromCSV = Map.fromList . map (\l -> (l!!2, l!!1)) . init . tail

fillCity :: Map.Map String String -> Record -> Record
fillCity _ l | length l /= 13 = l
fillCity m l = [cp] ++ [Map.findWithDefault "" cp m] ++ drop 2 l
    where cp = head l

f :: CSV -> CSV -> String
f cps = printCSV . map (fillCity $ fromCSV cps)

main :: IO ()
main = do
    r <- parseCSVFromFile "laposte_hexasmal.csv"
    case r of
        Left err -> error (show err)
        Right cps -> do
            r2 <- parseCSVFromFile "listing.csv"
            case r2 of
                Left err -> error (show err)
                Right listing -> putStrLn . f cps $ listing
  • # deux solutions :

    Posté par . Évalué à 2. Dernière modification le 12/05/16 à 15:49.

    Deux solutions :

    Tu peux fusionner les deux case, ça change légèrement la sémantique vis à vis des exceptions :

    main :: IO ()
    main = do
        r <- parseCSVFromFile "laposte_hexasmal.csv"
        r2 <- parseCSVFromFile "listing.csv"
        case (r,r2) of
            (Right cps,Right listing) -> putStrLn . f cps $ listing 
            (Left err, _) -> error $ show err
            (_, Left err) -> error $ show err

    Avec la monade Either

    main :: IO ()
    main =  calcul >>= either (error . show)  putStrLn --  either est un destructeur de Either
    
    -- calcul :: IO (Either Text.Parsec.Error.ParseError String)
    calcul = do
        r <-  parseCSVFromFile "laposte_hexasmal.csv" -- tu fais tes IO
        r2 <-  parseCSVFromFile "listing.csv"         -- là aussi
        return $ do cps <- r              -- les résultats de tes IO sont monadiques
                    listing <- r2         -- tu peux t'en servir pour monader
                    return $ f cps listing

    Sauf erreur de ma part, tu peux pas utiliser de tranformeurs car la monade IO est nécessairement au fond de la pile de monade. Un transformeur mT ne peut que produire une monade IO, là il faudrait une monade IOT qui produit un calcul dans Either
    si ça existait il serait possible de sortir de la monade IO, (tu aurais une fonction de type (IO (m a) -> m a), c'est pas bon.

    • [^] # Re: deux solutions :

      Posté par . Évalué à 2.

      Honte à moi, j'ai pas vu la jolie solution que tu suggérais pourtant :

      main :: IO ()
      main =  (liftM2 . liftM2) f (parseCSVFromFile "laposte_hexasmal.csv") (parseCSVFromFile "listing.csv")
                  >>= either (error . show)  putStrLn

      Pas du tout besoin de transformeurs, c'est même pas monadique en fait, c'est juste applicatif.
      (tes valeurs sont sont encapsulées deux fois, dans (Either ErrorBidule), puis dans IO. Ta fonction f à deux parametre peut être liftée une fois pour travailler sur Either ErrorBidule, et une seconde fois pour travailler sur IO.

      • [^] # Re: deux solutions :

        Posté par . Évalué à 1.

        Merci pour ta réponse, je ne pense pas que j'aurais réussi à trouver ça. Je n'ai pas trop l'habitude d'utiliser les liftM2, liftA2, etc. il faudrait que je révise.

        tu as créé ton compte juste pour me répondre ? sympa ;)

        • [^] # Re: deux solutions :

          Posté par . Évalué à 2.

          Haha, oui et non ^
          C'est quelque chose que je voulais régulièrement faire depuis un moment. Voilà, c'est fait :-)

      • [^] # Re: deux solutions :

        Posté par (page perso) . Évalué à 3. Dernière modification le 13/05/16 à 21:10.

        Ouch… Désolé d'arriver après la bataille, mais le code haskell dont le but est d'être le plus concis possible me rend fou, c'est illisible, et ça donne une mauvaise réputation à Haskell.

        Voilà ce que j'écrirais (et je trouve ça 100 fois plus lisible, et c'est beaucoup plus facile à faire évoluer, je vais expliquer pourquoi):

        import Text.Parsec.Error (ParseError)
        import Text.CSV (parseCSVFromFile, CSV)
        
        
        liftParsed :: IO (Either ParseError a) -> IO a
        liftParsed = (=<<) $ either (error . show) return
        
        
        computation :: CSV -> CSV -> String
        computation = undefined -- Ton code ici
        
        
        main :: IO ()
        main = do
            cps <- liftParsed $ parseCSVFromFile "laposte_hexasmal.csv"
            listing <- liftParsed $ parseCSVFromFile "listing.csv"
            putStrLn $ computation cps listing

        Pourquoi c'est plus lisible (ÀMHA)

        La magie est cachée dans liftParsed. L'implémentation de liftParsed est un peu complexe et difficile a lire. Mais tu n'as pas besoin de t'en préoccuper, la seule chose que tu as à lire c'est le type liftParsed :: IO (Either ParseError a) -> IO a.

        Deuxième chose que j'ai modifié c'est le fameux putStrLn . f cps $ listing. Je l'ai remplacé par putStrLn $ f cps listing. « Pourquoi ? » vas-tu me demander. Ce qui important c'est de pouvoir nommer les choses, le $ remplace les parenthèses. Ce qui veux dire que ton expression serait: (putStrLn . f cps) listing, la mienne putStrLn (f cps listing). Les deux fonctionnent, mais pour un lecteur, pour pouvoir comprendre le code, les humains essaient de comprendre ce que les expressions entre parenthèses veulent dire. Une manière de rendre le code plus lisible, serait de donner un nom aux choses entre parenthèse (ce n'est pas toujours nécessaire).

        Dans mon cas je peux écrire:

        let output = f a b in
        putStrLn output

        Dans ton cas, j'ai vraiment du mal a nommer la variable:

        let ???? = putStrLn . f cps in
        ???? listing

        Pourquoi c'est plus facile à faire évoluer?

        Prenons le code de foobarbaz:

        main =  (liftM2 . liftM2) f (parseCSVFromFile "laposte_hexasmal.csv") (parseCSVFromFile "listing.csv")
                    >>= either (error . show)  putStrLn

        Imagine demain tu dois parser 3 fichiers. Voilà comment tu vas le faire évoluer:

        main =  (liftM3 . liftM3) f (parseCSVFromFile "laposte_hexasmal.csv") (parseCSVFromFile "listing.csv") (parseCSVFromFile "foo.bar")
                >>= either (error . show) putStrLn

        Je te laisse imaginer le diff avec Git. Et le maximum est liftM5, ça veux dire que tu ne peux pas faire un calcul sur plus de 5 fichiers.

        D'un autre coté avec mon code, voilà le diff avec ma solution:

         main = do
             cps <- liftParsed $ parseCSVFromFile "laposte_hexasmal.csv"
             listing <- liftParsed $ parseCSVFromFile "listing.csv"
        -    putStrLn $ computation cps listing
        +    other <- liftParsed $ parseCSVFromFile "other.csv"
        +    putStrLn $ computation cps listing other

        Edit: J'ai oublié de préciser que le code soumis ici (qui n'est pas celui de foobarbazz) est sous licence WTFPL.

Suivre le flux des commentaires

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