J'ai vu des gens parler de Free Monad with Interpreter , en particulier dans le contexte de l'accès aux données. Quel est ce schéma? Quand pourrais-je vouloir l'utiliser? Comment cela fonctionne-t-il et comment pourrais-je le mettre en œuvre?
Je comprends (à partir de messages tels que this ) qu'il s'agit de séparer le modèle de l'accès aux données. En quoi diffère-t-il du modèle de référentiel bien connu? Ils semblent avoir la même motivation.
Le modèle réel est en réalité beaucoup plus général que le simple accès aux données. C'est un moyen léger de créer un langage spécifique au domaine qui vous donne un AST, puis d'avoir un ou plusieurs interprètes pour "exécuter" le AST comme vous le souhaitez.
La partie monade gratuite est juste un moyen pratique d'obtenir un AST que vous pouvez assembler à l'aide des fonctionnalités de monade standard de Haskell (comme la notation do) sans avoir à écrire beaucoup de code personnalisé. Cela garantit également que votre DSL est composable: vous pouvez le définir en parties puis assembler les parties de manière structurée, vous permettant de profiter des abstractions normales de Haskell comme les fonctions.
L'utilisation d'une monade gratuite vous donne la structure d'une DSL composable; il vous suffit de spécifier les pièces. Vous écrivez simplement un type de données qui englobe toutes les actions de votre DSL. Ces actions pourraient faire n'importe quoi, pas seulement l'accès aux données. Cependant, si vous spécifiez tous vos accès aux données en tant qu'actions, vous obtiendrez un AST qui spécifie toutes les requêtes et commandes du magasin de données. Vous pouvez alors interpréter cela comme bon vous semble: exécutez-le contre une base de données en direct, exécutez-la sur une maquette, enregistrez simplement les commandes de débogage ou même essayez d'optimiser les requêtes.
Regardons un exemple très simple pour, disons, un magasin de valeurs clés. Pour l'instant, nous allons simplement traiter les clés et les valeurs comme des chaînes, mais vous pouvez ajouter des types avec un peu d'effort.
data DSL next = Get String (String -> next)
| Set String String next
| End
Le paramètre next
nous permet de combiner des actions. Nous pouvons l'utiliser pour écrire un programme qui obtient "foo" et définit "bar" avec cette valeur:
p1 = Get "foo" $ \ foo -> Set "bar" foo End
Malheureusement, cela ne suffit pas pour une DSL significative. Puisque nous avons utilisé next
pour la composition, le type de p1
est de la même longueur que notre programme (ie 3 commandes):
p1 :: DSL (DSL (DSL next))
Dans cet exemple particulier, utiliser next
comme ceci semble un peu étrange, mais c'est important si nous voulons que nos actions aient des variables de type différentes. Nous pourrions vouloir un get
et set
, par exemple.
Notez comment le champ next
est différent pour chaque action. Cela indique que nous pouvons l'utiliser pour faire de DSL
un foncteur:
instance Functor DSL where
fmap f (Get name k) = Get name (f . k)
fmap f (Set name value next) = Set name value (f next)
fmap f End = End
En fait, c'est la manière niquement valide d'en faire un Functor, nous pouvons donc utiliser deriving
pour créer automatiquement l'instance en activant l'extension DeriveFunctor
.
L'étape suivante est le type Free
lui-même. C'est ce que nous utilisons pour représenter notre AST structure, construit au-dessus du type DSL
. Vous pouvez le voir comme une liste à la - type level, où "cons" imbrique simplement un foncteur comme DSL
:
-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a = Cons a (List a) | Nil
Nous pouvons donc utiliser Free DSL next
pour donner aux programmes de différentes tailles les mêmes types:
p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
Qui a le type beaucoup plus agréable:
p2 :: Free DSL a
Cependant, l'expression réelle avec tous ses constructeurs est encore très difficile à utiliser! C'est là que la partie monade entre en jeu. Comme le nom de "monade libre" l'indique, Free
est une monade - tant que f
(dans ce cas DSL
) est un foncteur:
instance Functor f => Monad (Free f) where
return = Return
Free a >>= f = Free (fmap (>>= f) a)
Return a >>= f = f a
Maintenant, nous allons quelque part: nous pouvons utiliser la notation do
pour rendre nos expressions DSL plus agréables. La seule question est de savoir quoi mettre pour next
? Eh bien, l'idée est d'utiliser la structure Free
pour la composition, nous allons donc simplement mettre Return
pour chaque champ suivant et laisser la do-notation faire toute la plomberie:
p3 = do foo <- Free (Get "foo" Return)
Free (Set "bar" foo (Return ()))
Free End
C'est mieux, mais c'est quand même un peu gênant. Nous avons Free
et Return
partout. Heureusement, il existe un modèle que nous pouvons exploiter: la façon dont nous "levons" une action DSL dans Free
est toujours la même - nous l'enveloppons dans Free
et appliquons Return
pour next
:
liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)
Maintenant, en utilisant cela, nous pouvons écrire des versions Nice de chacune de nos commandes et avoir une DSL complète:
get key = liftFree (Get key id)
set key value = liftFree (Set key value ())
end = liftFree End
En utilisant ceci, voici comment nous pouvons écrire notre programme:
p4 :: Free DSL a
p4 = do foo <- get "foo"
set "bar" foo
end
L'astuce intéressante est que tandis que p4
ressemble à un petit programme impératif, c'est en fait une expression qui a la valeur
Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))
Ainsi, la partie monade libre du modèle nous a donné une DSL qui produit des arbres de syntaxe avec une syntaxe Nice. Nous pouvons également écrire des sous-arbres composables en n'utilisant pas End
; par exemple, nous pourrions avoir follow
qui prend une clé, obtient sa valeur et l'utilise ensuite comme clé elle-même:
follow :: String -> Free DSL String
follow key = do key' <- get key
get key'
Maintenant, follow
peut être utilisé dans nos programmes comme get
ou set
:
p5 = do foo <- follow "foo"
set "bar" foo
end
Nous obtenons donc une belle composition et une abstraction pour notre DSL également.
Maintenant que nous avons un arbre, nous arrivons à la deuxième moitié du modèle: l'interpréteur. Nous pouvons interpréter l'arbre comme nous le souhaitons simplement en faisant correspondre les motifs sur celui-ci. Cela nous permettrait d'écrire du code sur un vrai magasin de données dans IO
, ainsi que d'autres choses. Voici un exemple par rapport à un magasin de données hypothétique:
runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
do res <- getKey key
runIO $ k res
runIO (Free (Set key value next)) =
do setKey key value
runIO next
runIO (Free End) = close
runIO (Return _) = return ()
Cela évaluera avec plaisir n'importe quel fragment DSL
, même s'il ne se termine pas par end
. Heureusement, nous pouvons créer une version "sûre" de la fonction qui n'accepte que les programmes fermés par end
en définissant la signature du type d'entrée sur (forall a. Free DSL a) -> IO ()
. Alors que l'ancienne signature accepte un Free DSL a
pour n'importe lequela
(comme Free DSL String
, Free DSL Int
et ainsi de suite), cette version n'accepte qu'un Free DSL a
qui fonctionne pour every possible a
— que nous ne pouvons créer qu'avec end
. Cela garantit que nous n'oublierons pas de fermer la connexion lorsque nous aurons terminé.
safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO
(Nous ne pouvons pas simplement commencer par donner à runIO
ce type car cela ne fonctionnera pas correctement pour notre appel récursif. Cependant, nous pourrions déplacer la définition de runIO
dans un where
bloquer dans safeRunIO
et obtenir le même effet sans exposer les deux versions de la fonction.)
L'exécution de notre code dans IO
n'est pas la seule chose que nous pourrions faire. Pour les tests, nous pourrions vouloir l'exécuter sur un State Map
au lieu. Écrire ce code est un bon exercice.
Voici donc le modèle d'interprétation monade + gratuit. Nous faisons une DSL, profitant de la structure de monade gratuite pour faire toute la plomberie. Nous pouvons utiliser la do-notation et les fonctions de monade standard avec notre DSL. Ensuite, pour vraiment l'utiliser, nous devons l'interpréter d'une manière ou d'une autre; comme l'arbre n'est finalement qu'une structure de données, nous pouvons l'interpréter comme nous le souhaitons à différentes fins.
Lorsque nous l'utilisons pour gérer les accès à un magasin de données externe, il est en effet similaire au modèle de référentiel. Il sert d'intermédiaire entre notre magasin de données et notre code, séparant les deux. À certains égards, cependant, il est plus spécifique: le "référentiel" est toujours un DSL avec un AST explicite que nous pouvons ensuite utiliser comme bon nous semble).
Cependant, le modèle lui-même est plus général que cela. Il peut être utilisé pour beaucoup de choses qui n'impliquent pas nécessairement des bases de données externes ou du stockage. Il est logique partout où vous voulez un contrôle fin des effets ou de plusieurs cibles pour une DSL.
Une monade libre est fondamentalement une monade qui construit une structure de données dans la même "forme" que le calcul plutôt que de faire quelque chose de plus compliqué. ( Il existe des exemples en ligne. ) Cette structure de données est ensuite transmise à un morceau de code qui la consomme et exécute les opérations. * Je ne suis pas entièrement familier avec le modèle de référentiel, mais à partir de ce que j'ai l il semble que ce soit une architecture de niveau supérieur, et un interprète monade + gratuit pourrait être utilisé pour l'implémenter. D'un autre côté, l'interpréteur gratuit monad + pourrait également être utilisé pour implémenter des choses entièrement différentes, comme les analyseurs.
* Il convient de noter que ce modèle n'est pas exclusif aux monades et peut en fait produire un code plus efficace avec des applications gratuites ou flèches gratuites . ( Les analyseurs en sont un autre exemple. )