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
eval (ExprVal x) = x
eval (ExprOp OpAdd e1 e2) = eval e1 + eval e2
eval (ExprOp OpMul e1 e2) = eval e1 * eval e2

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 typeOp’ with actual typeExpr
In the first argument ofExprOp’, namely ‘(ExprVal 1)’
      In the expression: ExprOp (ExprVal 1) (ExprVal 2) (ExprVal 3)
      In an equation for ‘notExpr’:
          notExpr = ExprOp (ExprVal 1) (ExprVal 2) (ExprVal 3)

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
valP = ExprVal <$> decimal

mulP :: Parser Expr
mulP = try (ExprOp OpMul <$> valP <*> (string "*" *> mulP)) <|> valP

addP :: Parser Expr
addP = try (ExprOp OpAdd <$> mulP <*> (string "+" *> addP)) <|> mulP

parseExpr :: String -> Maybe Expr
parseExpr = parseMaybe addP

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 ()
myblock1 = p_ "this is myblock1"

On peut également écrire des fonctions qui construisent du HTML.

myblock2 :: Text -> Html ()
myblock2 x = p_ ("this is myblock2, using " <> toHtml x)

Et comme l’EDSL est implémenté par une monade, on peut imbriquer des blocs, via la notation do.

mypage :: Html ()
mypage = do
    html_ $ do
        head_ $ title_ "mypage"
        body_ $ do
            h1_ "this is mypage"
            p_ "this is a paragraph"
            myblock1
            myblock2 "foobar"

On peut générer le fichier HTML correspondant :

main :: IO ()
main = renderToFile "lucid.html" mypage

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.