Introduction à DeriveGeneric en Haskell, exemple avec le format JSON
Voir aussi : video youtube - video peertube - code source
En Haskell, DeriveGeneric
est une fonctionnalité qui analyse la définition
d’un type. Ceci permet de générer automatiquement des instances de classe de
type. Plus généralement, le principe des
Generics
en Haskell est assez proche de ce qu’on appelle souvent réflexion dans
d’autres langages. Les Generics sont un outil très intéressant mais cet article
traite uniquement d’un cas d’utilisation : importer/exporter un type Haskell
depuis/vers le format JSON.
Le format JSON
Le format JSON permet de représenter des données sous une forme inspirée des objets JavaScript. C’est un format plus léger que XML, et qui souvent utilisé pour récupèrer des données depuis des API web.
Par exemple, la commande suivante demande à l’API Github des informations sur
le dépôt haskell/aeson
.
curl https://api.github.com/repos/haskell/aeson
En réponse, le serveur envoie les données correspondantes, au format JSON.
{"id": 1280014,
"name": "aeson",
"private": false,
"owner": {
"login": "haskell",
"id": 450574,
...
,
}"description": "A fast Haskell JSON library",
...
}
Les types enregistrements
Pour rappel, Haskell permet de définir des types enregistrements, c’est-à-dire des types algébriques avec des champs nommés et typés. C’est donc un format clé-valeur similaire à un objet en JSON.
Par exemple, on peut définir un type Repo
pour réprésenter un dépôt Github
(partiellement).
data Repo = Repo
id :: Int
{ name :: Text
, private :: Bool
, description :: Text
, owner :: Owner
, }
Instanciation automatique
La bibliothèque Haskell Aeson permet d’importer et d’exporter un type depuis/vers le format JSON.
Par exemple, on peut écrire le fichier Repo.hs
suivant.
{-# LANGUAGE DeriveGeneric #-}
module Repo where
import Data.Aeson
import Data.Text
import GHC.Generics
import Owner
data Repo = Repo
id :: Int
{ name :: Text
, private :: Bool
, description :: Text
, owner :: Owner
,deriving (Generic, Show)
}
instance FromJSON Repo
instance ToJSON Repo
Dans ce code, on a essentiellement reprit le type Repo
précédent. Cependant
on l’a fait dériver de Generic
et on a demandé d’instancier les classes
FromJSON
et ToJSON
. Grâce à DeriveGeneric
et à Aeson
, ces instances
sont écrites automatiquement, en reprenant les champs du type Repo
pour le
format JSON.
On peut alors écrire un programme principal qui décode l’exemple de données JSON précédent.
main :: IO ()
= do
main <- eitherDecodeFileStrict "data.json"
repoE case repoE of
Left err -> print err
Right repo -> print (repo :: Repo)
Le programme importe bien les données JSON dans un type Repo
en Haskell.
Repo {id = 1280014, name = "aeson", private = False, ...
Instanciation personnalisée
Ainsi, en ajoutant un deriving
et deux instance
, on peut avoir un type
enregistrement compatible avec JSON mais Aeson fournit également d’autres
fonctionnalités.
Par exemple, si on veut utiliser des champs différents, entre Haskell et JSON,
on peut personnaliser les instances. Le fichier Owner.hs
suivant définit un
type Owner
.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Owner where
import Data.Aeson
import Data.Text
import GHC.Generics
data Owner = Owner
mylogin :: Text
{ myid :: Int
,deriving (Generic, Show) }
Ici, le type Owner
définit des champs mylogin
et myid
. Si on veut
importer des données JSON où ces champs s’appellent différemment, par exemple
login
et id
, on peut écrire explicitement notre instance de FromJSON
.
instance FromJSON Owner where
= withObject "Owner" $ \v -> Owner
parseJSON <$> v .: "login"
<*> v .: "id"
De même, pour exporter le type Owner
vers un JSON dont les champs sont nommés
différemment :
instance ToJSON Owner where
Owner l i) =
toEncoding ("login" .= l
pairs ( <> "id" .= i
)
Par exemple, avec le programme suivant :
main :: IO ()
= C.putStrLn $ encode Owner { mylogin="haskell", myid=450574 } main
On obtient :
"login":"haskell","id":450574} {
Conclusion
Les Generics
de Haskell permettent de réaliser des fonctionnalités
intéressantes, similaires à ce que propose la réflexion mais à la compilation.
Par exemple, avec l’extension DeriveGeneric
et la bibliothèque Aeson
, on
importer/exporter en JSON automatiquement, et, si besoin, personnaliser cet
import/export.