Je suis nouveau sur Haskell mais je comprends comment utiliser Monad Transformers. Cependant, j’ai toujours des difficultés à saisir leur avantage sur la transmission des parameters aux appels de fonction.
Basé sur le wiki Monad Transformers Explained , nous avons essentiellement un object Config défini comme
data Config = Config Foo Bar Baz
et le faire circuler, au lieu d’écrire des fonctions avec cette signature
client_func :: Config -> IO ()
nous utilisons un transformateur ReaderT Monad et changeons la signature en
client_func :: ReaderT Config IO ()
tirer la config est juste un appel à ask
.
L’appel de fonction passe de client_func c
à runReaderT client_func c
Bien.
Mais pourquoi cela rend-il mon application plus simple?
1- Je soupçonne que les transformateurs Monad ont un intérêt lorsque vous assemblez beaucoup de fonctions / modules pour former une application. Mais c’est là que s’arrête ma compréhension. Quelqu’un pourrait-il s’il vous plaît faire la lumière?
2- Je n’ai trouvé aucune documentation sur la façon dont vous écrivez une grande application modulaire dans Haskell, où les modules exposent une certaine forme d’API et cachent leurs implémentations, ainsi que (partiellement) leurs propres états et environnements des autres modules. Des pointeurs s’il vous plaît?
(Edit: Real World Haskell déclare que “.. cette approche [Monad Transformers] … évolue vers des programmes plus importants”, mais il n’existe pas d’exemple clair démontrant cette affirmation)
EDIT Suivant Chris Taylor Réponse ci-dessous
Chris explique parfaitement pourquoi l’encapsulation de Config, State, etc … dans un Transformer Monad offre deux avantages:
getUserInput
) Writer
pour fournir une fonction de journalisation de niveau inférieur) Cela se fait au prix de la modification de la signature de toutes les fonctions afin qu’elles s’exécutent “dans” le Transformer Monad.
Donc, la question 1 est entièrement couverte. Merci Chris.
La question 2 est maintenant répondue dans ce post SO
Disons que nous écrivons un programme qui nécessite des informations de configuration sous la forme suivante:
data Config = C { logFile :: FileName }
Une façon d’écrire le programme consiste à passer explicitement la configuration entre les fonctions. Ce serait bien si nous n’avions qu’à le transmettre aux fonctions qui l’utilisent explicitement, mais malheureusement, nous ne sums pas sûrs qu’une fonction ait besoin d’appeler une autre fonction utilisant la configuration, nous sums donc obligés de la passer en tant que paramètre partout (en effet, ce sont les fonctions de bas niveau qui doivent utiliser la configuration, ce qui nous oblige à les transmettre également à toutes les fonctions de haut niveau).
Écrivons le programme comme ça, puis nous le réécrivons en utilisant le Reader
du Reader
et voyons quel avantage nous en retirons.
Nous nous retrouvons avec quelque chose comme ça:
readLog :: Config -> IO Ssortingng readLog (C logFile) = readFile logFile writeLog :: Config -> Ssortingng -> IO () writeLog (C logFile) message = do x <- readFile logFile writeFile logFile $ x ++ message getUserInput :: Config -> IO Ssortingng getUserInput config = do input <- getLine writeLog config $ "Input: " ++ input return input runProgram :: Config -> IO () runProgram config = do input <- getUserInput config putStrLn $ "You wrote: " ++ input
Notez que dans les fonctions de haut niveau, nous devons passer la config tout le temps.
Une alternative consiste à réécrire en utilisant le Reader
du Reader
. Cela complique un peu les fonctions de bas niveau:
type Program = ReaderT Config IO readLog :: Program Ssortingng readLog = do C logFile <- ask readFile logFile writeLog :: String -> Program () writeLog message = do C logFile <- ask x <- readFile logFile writeFile logFile $ x ++ message
Mais comme récompense, les fonctions de haut niveau sont plus simples, car nous n'avons jamais besoin de nous référer au fichier de configuration.
getUserInput :: Program Ssortingng getUserInput = do input <- getLine writeLog $ "Input: " ++ input return input runProgram :: Program () runProgram = do input <- getUserInput putStrLn $ "You wrote: " ++ input
Nous pourrions ré-écrire les signatures de type de getUserInput et runProgram pour être
getUserInput :: (MonadReader Config m, MonadIO m) => m Ssortingng runProgram :: (MonadReader Config m, MonadIO m) => m ()
ce qui nous donne beaucoup de souplesse pour plus tard, si nous décidons de changer le type de Program
sous-jacent pour une raison quelconque. Par exemple, si nous voulons append un état modifiable à notre programme, nous pourrions redéfinir
data ProgramState = PS Int Int Int type Program a = StateT ProgramState (ReaderT Config IO) a
et nous n'avons pas besoin de modifier getUserInput
ou runProgram
- ils continueront à fonctionner runProgram
.
NB Je n'ai pas tapé ce message, et encore moins essayé de l'exécuter. Il peut y avoir des erreurs!