2016-12-13 1 views
1

Il est une bibliothèque qui fournit un type de données F et une fonction de typeComment terminer un calcul qui s'exécute dans la monade `IO`?

ffoldlIO :: (b -> a -> IO b) -> b -> F a -> IO b 

La fonction est similaire à

foldlIO :: (b -> a -> IO b) -> b -> [a] -> IO b 
foldlIO f a = \xs -> foldr (\x r (!a') -> f a' x >>= r) return xs a 

Je me demande si foldlIO (et donc ffoldlIO) peut fonctionner dans un court -circuit de mode.

Considérons cet exemple:

example1 :: IO Int 
example1 = foldlIO (\a x -> if a < 4 then return (a + x) else return a) 0 [1..5] 

Ici foldlIO parcourt toute la liste, mais si l'on jette une exception pour arrêter le calcul et l'attraper? Quelque chose comme ceci:

data Terminate = Terminate 
    deriving (Show) 

instance Exception Terminate 

example2 :: IO Int 
example2 = do 
    ra <- newIORef 0 
    let step a x 
     | a' < 4 = return a' 
     | otherwise = writeIORef ra a' >> throwIO Terminate 
     where a' = a + x 
    foldlIO step 0 [1..] `catch` \(_ :: Terminate) -> readIORef ra 

Est-ce fiable? Y at-il un meilleur moyen de terminer un calcul qui s'exécute dans la monade IO (et aucune autre monade) ou suis-je pas censé faire cela du tout?

Répondre

3

Par exemple, vous pouvez utiliser ContT transformateur monade comme ceci:

example3 :: IO Int 
example3 = flip runContT return . callCC $ \exit -> do 
    let step a x 
      | a' < 4 = return a' 
      | otherwise = exit a' 
      where a' = a + x 
    foldM step 0 [1..] 

Vous pouvez également définir votre propre version de foldM avec posibility de résiliation.

termFoldM :: (Monad m, Foldable t) => 
    ((b -> ContT b m c) -> b -> a -> ContT b m b) -> b -> t a -> m b 
termFoldM f a t = flip runContT return . callCC $ \exit -> foldM (f exit) a xs 

example4 :: IO Int 
example4 = termFoldM step 0 [1..] 
    where 
    step exit a x 
     | a' < 4 = return a' 
     | otherwise = exit a' 
     where a' = a + x 

Mais de cette façon (avec ContT) a un problème. Vous ne pouvez pas facilement faire des actions IO. Par exemple, ce code ne sera pas compilé, car la fonction step doit renvoyer la valeur du type ContT Int IO Int et non IO Int.

let step a x 
     | a' < 4 = putStrLn ("'a = " ++ show a') >> return a' 
     | otherwise = exit a' 
     where a' = a + x 

Heureusement, vous pouvez résoudre ce par la fonction lift, comme ceci:

let step a x 
     | a' < 4 = lift (putStrLn ("'a = " ++ show a')) >> return a' 
     | otherwise = exit a' 
     where a' = a + x 
+1

C'est plus ou moins ce que j'ai écrit aussi. 'ContT' est (in) célèbre pour être dur pour les débutants, donc plus de commentaires aideraient aussi. Je suggère aussi d'ajouter 'lift (putStrLn" foo ") >> ...' quelque part pour montrer comment faire pour IO dans 'ContT _ IO _'. – chi

+0

Ce calcul s'exécute dans 'ContT Int IO Int' au lieu de' IO' plaine comme j'ai demandé. Je vais modifier la question pour la rendre moins confuse, désolé. – user3237465

+0

"vous pouvez définir votre propre version de' foldM' "- Je ne peux pas: la bibliothèque fournit seulement' ffoldlIO' et c'est beaucoup trop compliqué pour implémenter mon propre 'foldM'. Les listes dans la question sont juste pour un exemple de ce que je veux réaliser. – user3237465

1

Ma première réponse n'a pas été correcte. Donc, je vais essayer de m'améliorer. Je pense que l'utilisation d'exceptions pour se terminer dans une monade d'E/S n'est pas un hack mais elle n'a pas l'air propre. Je propose de définir l'instance MonadCont IO comme ceci:

data Terminate = forall a . Terminate a deriving (Typeable) 
instance Show Terminate where show = const "Terminate" 
instance Exception Terminate 

instance MonadCont IO where 
    callCC f = f exit `catch` (\(Terminate x) -> return . unsafeCoerce $ x) 
     where exit = throwIO . Terminate 

Ensuite, vous pouvez réécrire votre exemple plus propre.

example :: IO Int 
example = callCC $ \exit -> do 
    let step a x 
      | a' < 4 = return a' 
      | otherwise = exit a' 
      where a' = a + x 
    foldlIO step 0 [1..] 

Variante avec IOREf.

data Terminate = Terminate deriving (Show, Typeable) 
instance Exception Terminate 

instance MonadCont IO where 
    callCC f = do 
     ref <- newIORef undefined 
     let exit a = writeIORef ref a >> throwIO Terminate 
     f exit `catch` (\Terminate -> readIORef ref) 
+0

C'est un joli tour, merci. Cependant, je ne veux pas introduire d'instances orphelines et j'ai déjà une [machinerie] (https://github.com/effectfully/prefolds) qui permet d'écrire des plis de court-circuit, donc j'ai juste besoin d'interpréter de tels plis dans la monade 'IO', et je ne suis pas encore sûr de l'approche' Terminate'. Avez-vous peut-être une référence qui confirme que cette approche est OK? Un papier qui le mentionne, ou une bibliothèque couramment utilisée ou quelque chose comme ça. – user3237465

+1

@ user3237465 Non, je ne le fais pas. Mais j'ai connu un problème potentiel avec cette approche - quelqu'un peut attraper toutes les exceptions et ceci peut casser votre programme. Heureusement, c'est peu probable. Donc, c'est une mauvaise idée d'utiliser cette approche partout, mais dans certains cas, cela peut être possible. – freestyle

+0

C'est une bonne observation. Ensuite, je vais juste stocker des accumulateurs dans un 'IORef' et une valeur finale sera toujours dans le' IORef', peu importe si une exception a été levée et peu importe si elle a été capturée par quelqu'un. – user3237465