Introduction aux DSL, en Haskell
Voir aussi : video youtube - video peertube - code source
Cet article introduit les DSL (ou langages dédiés, en français) et en quoi Haskell est intéressant pour les mettre en œuvre.
Qu’est-ce-qu’un DSL ?
Un DSL (domain-specific language) est un langage dédié à un domaine d’application particulier, contrairement à un langage de programmation généraliste. Un DSL permet de “travailler” dans le domaine métier qu’il modélise.
Les DSL sont un outil classique. Par exemple, HTML est un langage dédié à la description de documents hypertexte. Autre exemple, SQL décrit des requêtes pour une base de données relationnelles.
Un EDSL (embedded domain-specific language) est un DSL embarqué dans un langage de programmation généraliste. Ceci permet de profiter à la fois de la modélisation du domaine métier (via le DSL) et de capacité de programmation plus puissante (via le langage d’accueil).
Un EDSL peut-être embarqué de façon superficielle (c’est-à-dire avec sa syntaxe propre, potentiellement différente de celle du langage d’accueil), ou de façon profonde (c’est-à-dire directement via la syntaxe du langage d’accueil).
Ci-dessous quelques exemples illustrant comment mettre en œuvre des EDSL avec Haskell.
Exemple 1 : expressions arithmétiques
On veut représenter des expressions arithmétiques avec des valeurs et des opérateurs. Les valeurs peuvent être des entiers et les opérateurs des additions ou des multiplications.
EDSL “profond”
Avec le système de type de Haskell, on peut définir un EDSL directement avec des types algébriques. Par exemple, un type Expr
pour les expressions et un type Op
pour les opérateurs.
data Op
= OpAdd
| OpMul
deriving (Show)
data Expr
= ExprVal Int
| ExprOp Op Expr Expr
deriving (Show)
On a donc ici un EDSL “profond”, qu’on peut manipuler directement avec du code Haskell classique, par exemple pour évaluer une expression.
eval :: Expr -> Int
ExprVal x) = x
eval (ExprOp OpAdd e1 e2) = eval e1 + eval e2
eval (ExprOp OpMul e1 e2) = eval e1 * eval e2 eval (
On peut utiliser tout cela dans l’interpréteur ghci
.
*Main> expr1 = ExprOp OpMul (ExprVal 2) (ExprVal 21)
*Main> eval expr1
42
L’EDSL ainsi défini est vérifié via le système de type de Haskell.
*Main> notExpr = ExprOp (ExprVal 1) (ExprVal 2) (ExprVal 3)
<interactive>:1:19: error:
Couldn't match expected type ‘Op’ with actual type ‘Expr’
• In the first argument of ‘ExprOp’, namely ‘(ExprVal 1)’
• In the expression: ExprOp (ExprVal 1) (ExprVal 2) (ExprVal 3)
In an equation for ‘notExpr’:
= ExprOp (ExprVal 1) (ExprVal 2) (ExprVal 3) notExpr
EDSL “superficiel”
Haskell propose l’extension TemplateHaskell
, qui est très pratique pour personnaliser la syntaxe d’un EDSL (mais dépasse le cadre de cet article).
Une solution plus basique consiste à parser un texte d’entrée. Par exemple, avec Megaparsec
, on peut définir un parseur pour une syntaxe infixe classique d’expressions arithmétiques :
valP :: Parser Expr
= ExprVal <$> decimal
valP
mulP :: Parser Expr
= try (ExprOp OpMul <$> valP <*> (string "*" *> mulP)) <|> valP
mulP
addP :: Parser Expr
= try (ExprOp OpAdd <$> mulP <*> (string "+" *> addP)) <|> mulP
addP
parseExpr :: String -> Maybe Expr
= parseMaybe addP parseExpr
On peut alors écrire des expressions sous forme de texte et les parser pour les transformer en données utilisables avec Haskell.
*Main> parseExpr "2*21"
Just (ExprOp OpMul (ExprVal 2) (ExprVal 21))
*Main> eval <$> parseExpr "2*21"
Just 42
Exemple 2 : le format JSON avec Aeson
Aeson
est une bibliothèque classique en Haskell pour importer ou exporter des données JSON. Au cœur de cette bibliothèque, le type Value
permet de représenter le format JSON (cf Data.Aeson.Types)
data Value = Object !Object
| Array !Array
| String !Text
| Number !Scientific
| Bool !Bool
| Null
deriving (Eq, Read, Typeable, Data, Generic)
On peut donc utiliser ce type comme un EDSL “profond”, par exemple pour générer du JSON.
*Main> import Data.HashMap.Strict
*Main Data.HashMap.Strict> :set -XOverloadedStrings
*Main Data.HashMap.Strict> encode $ Object (fromList [("age",Number 42.0),("name",String "John Doe")])
"{\"age\":42,\"name\":\"John Doe\"}"
Cependant, comme expliqué dans un article précédent, Haskell permet de convertir un type algébrique vers le type Value
(notamment, via l’extension DeriveGeneric
) et ainsi de profiter des fonctions de aeson
.
Person <----> Value <----> JSON
Par exemple, avec un type Person
:
data Person = Person
name :: String
{ age :: Int
,deriving (Generic, Show)
}
instance FromJSON Person
instance ToJSON Person
On peut alors, définir une valeur de type Person
, la transformer en Value
ou l’exporter au format JSON.
*Main> person1 = Person "John Doe" 42
*Main> toJSON person1
Object (fromList [("age",Number 42.0),("name",String "John Doe")])
*Main> encode person1
"{\"age\":42,\"name\":\"John Doe\"}"
Exemple 3 : le format HTML avec Lucid
Lucid est une bibliothèque Haskell qui implémente un EDSL pour le HTML sous forme de monade. Plus exactement, le type de base HtmlT
est un transformateur de monade défini par :
newtype HtmlT m a =
HtmlT {runHtmlT :: m (HashMap Text Text -> Builder,a)
Le détail de ce type n’est pas très important; on notera juste qu’il est suivi d’instances, notamment de Functor
, Applicative
et Monad
:
instance Functor m => Functor (HtmlT m) where
...
instance Applicative m => Applicative (HtmlT m) where
...
instance Monad m => Monad (HtmlT m) where
...
Lucid
redéfinit également les éléments du langage HTML. Par exemple, p_ "foobar"
représente la balise <p>foobar</p>
. On peut donc créer des “blocs de HTML”, directement en Haskell et vérifiés par le système de types.
myblock1 :: Html ()
= p_ "this is myblock1" myblock1
On peut également écrire des fonctions qui construisent du HTML.
myblock2 :: Text -> Html ()
= p_ ("this is myblock2, using " <> toHtml x) myblock2 x
Et comme l’EDSL est implémenté par une monade, on peut imbriquer des blocs, via la notation do
.
mypage :: Html ()
= do
mypage $ do
html_ $ title_ "mypage"
head_ $ do
body_ "this is mypage"
h1_ "this is a paragraph"
p_
myblock1"foobar" myblock2
On peut générer le fichier HTML correspondant :
main :: IO ()
= renderToFile "lucid.html" mypage main
Ce qui donne :
<html>
<head>
<title>mypage</title>
</head>
<body>
<h1>this is mypage</h1>
<p>this is a paragraph</p>
<p>this is myblock1</p>
<p>this is myblock2, using foobar</p>
</body>
</html>
Conclusion
Les langages dédiés permettent de représenter et de manipuler des domaines particuliers, de façon indépendante ou embarqué au sein d’un langage de programmation.
Haskell est un langage intéressant pour embarquer des langages dédiés notamment parce que son système de types permet de valider les données et parce que ses fonctionnalités comme les monades permettent de structurer les données efficacement.