53 votes

Injection de dépendances en Haskell : résoudre la tâche de façon idiomatique

Quelle est la solution idiomatique en Haskell pour l'injection de dépendances ?

Par exemple, supposons que vous ayez une interface frobby et vous deviez transmettre une instance conforme à la norme frobby (il peut y avoir plusieurs variétés de ces instances, par exemple, foo et bar ).

Les opérations typiques sont :

  • des fonctions qui prennent une certaine valeur X et retourner une valeur Y . Par exemple, il peut s'agir d'un accesseur de base de données, qui prend une requête SQL et un connecteur et renvoie un ensemble de données. Vous pourriez avoir besoin d'implémenter postgres, mysql, et un système de test fictif.

  • des fonctions qui prennent une certaine valeur Z et renvoyer une fermeture relative à Z spécialisé pour un foo o bar style, choisi au moment de l'exécution.

Une personne a résolu le problème comme suit :

http://mikehadlow.blogspot.com/2011/05/dependency-injection-haskell-style.html

Mais je ne sais pas si c'est la façon canonique de gérer cette tâche.

109voto

ertes Points 3012

Je pense que la réponse appropriée ici est, et je vais probablement recevoir quelques downvotes juste pour avoir dit cela : oubliez le terme injection de dépendances . Oubliez-le. C'est un mot à la mode dans le monde de l'OO, mais rien de plus.

Résolvons le vrai problème. Gardez à l'esprit que vous résolvez un problème, et que ce problème est la tâche de programmation en question. Ne faites pas de votre problème "l'implémentation de l'injection de dépendances".

Nous allons prendre l'exemple d'un enregistreur, car il s'agit d'une fonctionnalité de base que de nombreux programmes souhaitent avoir, et il existe de nombreux types d'enregistreurs différents : Un qui enregistre vers stderr, un qui enregistre vers un fichier, une base de données, et un qui ne fait simplement rien. Pour les unifier tous, il faut un type :

type Logger m = String -> m ()

Vous pouvez également choisir un type plus sophistiqué pour économiser quelques frappes de clavier :

class PrettyPrint a where
    pretty :: a -> String

type Logger m = forall a. (PrettyPrint a) => a -> m ()

Définissons maintenant quelques loggers en utilisant cette dernière variante :

noLogger :: (Monad m) => Logger m
noLogger _ = return ()

stderrLogger :: (MonadIO m) => Logger m
stderrLogger x = liftIO . hPutStrLn stderr $ pretty x

fileLogger :: (MonadIO m) => FilePath -> Logger m
fileLogger logF x =
    liftIO . withFile logF AppendMode $ \h ->
        hPutStrLn h (pretty x)

acidLogger :: (MonadIO m) => AcidState MyDB -> Logger m
acidLogger db x = update' db . AddLogLine $ pretty x

Vous pouvez voir comment cela construit un graphique des dépendances. Le site acidLogger dépend d'une connexion à une base de données pour le MyDB la mise en page de la base de données. Le passage d'arguments aux fonctions est à peu près le moyen le plus naturel d'exprimer des dépendances dans un programme. Après tout, une fonction est simplement une valeur qui dépend d'une autre valeur. C'est également vrai pour les actions. Si votre action dépend d'un enregistreur, elle est naturellement une fonction d'enregistreurs :

printFile :: (MonadIO m) => Logger m -> FilePath -> m ()
printFile log fp = do
    log ("Printing file: " ++ fp)
    liftIO (readFile fp >>= putStr)
    log "Done printing."

Vous voyez comme c'est facile ? À un moment donné, vous vous rendez compte à quel point votre vie sera plus facile si vous oubliez toutes les absurdités que OO vous a enseignées.

12voto

Gabriel Gonzalez Points 23530

Utilice pipes . Je ne dirai pas que c'est idiomatique car la bibliothèque est encore relativement nouvelle, mais je pense que cela résout exactement votre problème.

Par exemple, disons que vous voulez envelopper une interface avec une base de données :

import Control.Proxy

-- This is just some pseudo-code.  I'm being lazy here
type QueryString = String
type Result = String
query :: QueryString -> IO Result

database :: (Proxy p) => QueryString -> Server p QueryString Result IO r
database = runIdentityK $ foreverK $ \queryString -> do
    result <- lift $ query queryString
    respond result

Nous pouvons alors modéliser une interface vers la base de données :

user :: (Proxy p) => () -> Client p QueryString Result IO r
user () = forever $ do
    lift $ putStrLn "Enter a query"
    queryString <- lift getLine
    result <- request queryString
    lift $ putStrLn $ "Result: " ++ result

Vous les connectez comme ça :

runProxy $ database >-> user

Cela permettra ensuite à l'utilisateur d'interagir avec la base de données à partir de l'invite.

Nous pouvons alors remplacer la base de données par une base de données factice :

mockDatabase :: (Proxy p) => QueryString -> Server p QueryString Result IO r
mockDatabase = runIdentityK $ foreverK $ \query -> respond "42"

Maintenant, nous pouvons remplacer la base de données par la base factice très facilement :

runProxy $ mockDatabase >-> user

Ou nous pouvons changer le client de la base de données. Par exemple, si nous avons remarqué qu'une session client particulière a déclenché un bogue bizarre, nous pouvons le reproduire comme suit :

reproduce :: (Proxy p) => () -> Client p QueryString Result IO ()
reproduce () = do
    request "SELECT * FROM WHATEVER"
    request "CREATE TABLE BUGGED"
    request "I DON'T REALLY KNOW SQL"

... puis le brancher comme ça :

runProxy $ database >-> reproduce

pipes vous permet de diviser les comportements de flux ou interactifs en composants modulaires afin de pouvoir les mélanger comme bon vous semble, ce qui est l'essence même de l'injection de dépendances.

Pour en savoir plus sur pipes il suffit de lire le tutoriel à Control.Proxy.Tutorial .

5voto

Michał Bendowski Points 938

Pour poursuivre sur la réponse d'ertes, je pense que la signature souhaitée pour printFile es printFile :: (MonadIO m, MonadLogger m) => FilePath -> m () que j'ai lu comme "Je vais imprimer le fichier donné. Pour ce faire, j'ai besoin d'effectuer quelques entrées/sorties et quelques journalisations."

Je ne suis pas un expert, mais voici ma tentative de solution. Je vous serais reconnaissant de me faire part de vos commentaires et de vos suggestions pour l'améliorer.

{-# LANGUAGE FlexibleInstances #-}

module DependencyInjection where

import Prelude hiding (log)
import Control.Monad.IO.Class
import Control.Monad.Identity
import System.IO
import Control.Monad.State

-- |Any function that can turn a string into an action is considered a Logger.
type Logger m = String -> m ()

-- |Logger that does nothing, for testing.
noLogger :: (Monad m) => Logger m
noLogger _ = return ()

-- |Logger that prints to STDERR.
stderrLogger :: (MonadIO m) => Logger m
stderrLogger x = liftIO $ hPutStrLn stderr x

-- |Logger that appends messages to a given file.
fileLogger :: (MonadIO m) => FilePath -> Logger m
fileLogger filePath value = liftIO logToFile
  where
      logToFile :: IO ()
      logToFile = withFile filePath AppendMode $ flip hPutStrLn value

-- |Programs have to provide a way to the get the logger to use.
class (Monad m) => MonadLogger m where
    getLogger :: m (Logger m)

-- |Logs a given string using the logger obtained from the environment.
log :: (MonadLogger m) => String -> m ()
log value = do logger <- getLogger
               logger value

-- |Example function that we want to run in different contexts, like
--  skip logging during testing.
printFile :: (MonadIO m, MonadLogger m) => FilePath -> m ()
printFile fp = do
    log ("Printing file: " ++ fp)
    liftIO (readFile fp >>= putStr)
    log "Done printing."

-- |Let's say this is the real program: it keeps the log file name using StateT.
type RealProgram = StateT String IO

-- |To get the logger, build the right fileLogger.
instance MonadLogger RealProgram where
    getLogger = do filePath <- get
                   return $ fileLogger filePath

-- |And this is how you run printFile "for real".
realMain :: IO ()
realMain = evalStateT (printFile "file-to-print.txt") "log.out"

-- |This is a fake program for testing: it will not do any logging.
type FakeProgramForTesting = IO

-- |Use noLogger.
instance MonadLogger FakeProgramForTesting where
    getLogger = return noLogger

-- |The program doesn't do any logging, but still does IO.
fakeMain :: IO ()
fakeMain = printFile "file-to-print.txt"

3voto

JJJ Points 1481

Une autre option consiste à utiliser types de données existentiellement quantifiées . Prenons XMonad à titre d'exemple. Il y a un ( frobby ) pour les mises en page - LayoutClass typeclass :

-- | Every layout must be an instance of 'LayoutClass', which defines
-- the basic layout operations along with a sensible default for each.
-- 
-- ...
-- 
class Show (layout a) => LayoutClass layout a where

    ...

et le type de données existentielles Mise en page :

-- | An existential type that can hold any object that is in 'Read'
--   and 'LayoutClass'.
data Layout a = forall l. (LayoutClass l a, Read (l a)) => Layout (l a)

qui peut envelopper n'importe quel ( foo o bar ) instance de LayoutClass interface. Elle est elle-même une mise en page :

instance LayoutClass Layout Window where
    runLayout (Workspace i (Layout l) ms) r = fmap (fmap Layout) `fmap` runLayout (Workspace i l ms) r
    doLayout (Layout l) r s  = fmap (fmap Layout) `fmap` doLayout l r s
    emptyLayout (Layout l) r = fmap (fmap Layout) `fmap` emptyLayout l r
    handleMessage (Layout l) = fmap (fmap Layout) . handleMessage l
    description (Layout l)   = description l

Il est désormais possible d'utiliser Layout de manière générique avec seulement LayoutClass méthodes d'interface. Une mise en page appropriée qui implémente LayoutClass sera sélectionnée au moment de l'exécution, il y en a un grand nombre dans l'onglet XMonad.Layout et en xmonad-contrib . Et, bien sûr, il est possible de passer d'une mise en page à l'autre de manière dynamique :

-- | Set the layout of the currently viewed workspace
setLayout :: Layout Window -> X ()
setLayout l = do
    ss@(W.StackSet { W.current = c@(W.Screen { W.workspace = ws })}) <- gets windowset
    handleMessage (W.layout ws) (SomeMessage ReleaseResources)
    windows $ const $ ss {W.current = c { W.workspace = ws { W.layout = l } } }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X