Monad Transformers vs Passer des parameters aux fonctions

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:

  1. Il empêche une fonction de niveau supérieur de devoir conserver dans sa signature de type tous les parameters requirejs par les (sous) fonctions qu’elle appelle mais n’est pas nécessaire pour son propre usage (voir la fonction getUserInput )
  2. et par conséquent, rendre les fonctions de niveau supérieur plus résistantes à un changement du contenu de la Monad Transformer (par exemple, si vous souhaitez y append un 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.

Option 1. Passage de configuration explicite

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.

Option 2. Lecteur monade

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 

Prenant plus loin

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!