Haskell: монадический захват? - PullRequest
12 голосов
/ 16 июля 2009

У меня есть некоторые функции, написанные на C, которые я вызываю из Haskell. Эти функции возвращают IO (CInt). Иногда я хочу запустить все функции независимо от того, что из них возвращает, и это легко. Для примера кода это общая идея того, что происходит в настоящее время:

Prelude> let f x = print x >> return x
Prelude> mapM_ f [0..5]
0
1
2
3
4
5
Prelude>

Я получаю желаемые побочные эффекты, и мне плевать на результаты. Но теперь мне нужно остановить выполнение сразу после первого элемента, который не возвращает желаемый результат. Скажем, возвращаемое значение 4 или выше требует остановки выполнения - тогда я хочу сделать следующее:

Prelude> takeWhile (<4) $ mapM f [0..5]

Что дает мне эту ошибку:

<interactive>:1:22:
    Couldn't match expected type `[b]' against inferred type `IO a'
    In the first argument of `mapM', namely `f'
    In the second argument of `($)', namely `mapM f ([0 .. 5])'
    In the expression: takeWhile (< 4) $ mapM f ([0 .. 5])

И это имеет смысл для меня - результат все еще содержится в монаде ввода-вывода, и я не могу просто сравнить два значения, содержащиеся в монаде ввода-вывода. Я знаю, что именно в этом и заключается цель монад - объединение результатов воедино и отбрасывание операций при выполнении определенного условия - но есть ли в этом случае простой способ «обернуть» монаду ввода-вывода, чтобы прекратить выполнение цепочки при условии по моему выбору, без написания экземпляра MonadPlus?

Могу ли я просто "снять" значения с f для целей takeWhile?

Это решение для функторов? Функторы еще не «нажали» на меня, но у меня сложилось впечатление, что это может быть хорошая ситуация для их использования.

<ч /> Обновление:

@ sth имеет самый близкий ответ на то, что я хочу - на самом деле, это почти то, к чему я стремился, но я все еще хотел бы посмотреть, есть ли стандартное решение , которое не соответствует явно рекурсивный - это ведь Haskell! Оглядываясь назад на то, как я сформулировал свой вопрос, теперь я понимаю, что не совсем ясно о желаемом поведении.

Функция f, которую я использовал выше для примера, была просто примером. Реальные функции написаны на C и используются исключительно для их побочных эффектов. Я не могу использовать предложение Тома о mapM_ f (takeWhile (<4) [0..5]), потому что я не знаю, приведет ли какой-либо ввод к успеху или неудаче, пока не будет выполнен.

Меня на самом деле не волнует возвращаемый список - я просто хочу вызывать функции C, пока либо список не будет исчерпан, либо первая функция C не вернет код ошибки.

В псевдокоде в стиле C мое поведение будет следующим:

do {
    result = function_with_side_effects(input_list[index++]);
} while (result == success && index < max_index);

Итак, снова ответ @ sth выполняет именно то поведение, которое я хочу, за исключением того, что результаты могут (должны?) Быть отброшены. dropWhileM_ функция будет эквивалентна для моих целей. Почему в Control.Monad нет такой функции или takeWhileM_? Я вижу, что было подобное обсуждение в списке рассылки , но, похоже, ничего не вышло.

Ответы [ 5 ]

15 голосов
/ 16 июля 2009

Вы можете определить последовательность как

sequence xs = foldr (liftM2 (:)) (return []) xs

Проблема с liftM2, которую вы видели, заключается в том, что у вас нет возможности остановить m2, что может быть launchTheMissiles!

liftM2 :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c
liftM2 f m1 m2 = do
    x1 <- m1
    x2 <- m2
    return (f x1 x2)

Использование guard, как показано ниже, выглядит привлекательно:

sequenceUntil p xs = foldr (myLiftM2 p (:)) (return []) xs
  where myLiftM2 p f m1 m2 = do
            x1 <- m1
            guard $ p x1
            x2 <- m2
            return (f x1 x2)

Приведенный выше код не будет работать в вашем приложении, поскольку монада ввода-вывода не является экземпляром MonadPlus .

Так что держите его за руку еще немного

module Main where

import Control.Monad

printx :: Int -> IO Int
printx x = do
    print x
    return x

sequenceUntil :: (Monad m) => (a -> Bool) -> [m a] -> m [a]
sequenceUntil p xs = foldr (myLiftM2 (:) []) (return []) xs
  where myLiftM2 f z m1 m2 = do
            x1 <- m1
            if p x1 then do x2 <- m2
                            return $ f x1 x2
                    else return z

main :: IO ()
main = do
  let as :: [IO Int]
      as = map printx [1..10]
  ys <- sequenceUntil (< 4) as
  print ys

Несмотря на то, что as представляет собой список действий от 1 до 10, вывод будет

1
2
3
4
[1,2,3]

Отбрасывание результатов тогда тривиально:

sequenceUntil_ :: (Monad m) => (a -> Bool) -> [m a] -> m ()
sequenceUntil_ p xs = sequenceUntil p xs >> return ()

main :: IO ()
main = do
  let as :: [IO Int]
      as = map printx [1..]
  sequenceUntil_ (< 4) as

Обратите внимание на использование [1..], которое показывает, что новый комбинатор поддерживает лень .


Вы можете предпочесть spanM:

spanM :: (Monad m) => (a -> Bool) -> [m a] -> m ([a], [m a])
spanM _ [] = return ([], [])
spanM p (a:as) = do
  x <- a
  if p x then do (xs,bs) <- spanM p as
                 return (x:xs, bs)
         else return ([x], as)

Обратите внимание, что он немного отличается от span тем, что в элементе результатов содержится элемент с ошибкой. Вторая пара - это оставшиеся действия. Например:

*Main> (xs,bs) <- spanM (< 4) as
1
2
3
4
*Main> xs  
[1,2,3,4]
*Main> sequence bs
5
6
7
8
9
10
[5,6,7,8,9,10]

Еще одна альтернатива:

untilM :: Monad m => (a -> Bool) -> [m a] -> m ()
untilM p (x:xs) = do
  y <- x
  unless (p y) $ untilM p xs

Обратите внимание, что смысл предиката дополняется:

*Main> untilM (>= 4) as
1
2
3
4
14 голосов
/ 16 июля 2009

Я не думаю, что в стандартной библиотеке есть что-то вроде takeWhileM, но вы можете написать это самостоятельно, чтобы выполнить столько операций ввода-вывода, сколько необходимо:

takeWhileM :: (Monad m) => (a -> Bool) -> [m a] -> m [a]
takeWhileM _ [] = return []
takeWhileM p (a:as) =
   do v <- a
      if p v
         then do vs <- takeWhileM p as
                 return (v:vs)
         else return []

Предоставленный список оценивается только до тех пор, пока не будет найден элемент, который не соответствует предикату:

*Main> takeWhileM (<4) (map f [1..5])
1
2
3
4
[1,2,3]
10 голосов
/ 16 июля 2009

Редактировать: Теперь я вижу то, что вы ищете.

gbacon опубликовал замечательную sequenceWhile функцию, которая является почти "примитивом", который вам нужен.

На самом деле, поскольку вас интересуют только побочные эффекты, sequenceWhile_ должно быть достаточно. Вот определение (снова, вдохновленный gbacon, проголосуйте за него!):

sequenceWhile_ :: (Monad m) => (a -> Bool) -> [m a] -> m ()
sequenceWhile_ p xs = foldr (\mx my -> mx >>= \x -> when (p x) my)
                            (return ()) xs

Вы называете это так:

Prelude Control.Monad> sequenceWhile (<4) $ map f [1..]

Оригинальный ответ:

Вы не можете просто «снять» значения из IO Монады для использования с takeWile, но вы можете «поднять» takeWhile для использования в Монаде!

Функция liftM переведет функцию (a -> b) в функцию (m a -> m b), где m - монада.

(В качестве примечания вы можете найти такую ​​функцию, выполнив поиск по ее типу в Hoogle , в данном случае по запросу: Monad m => (a -> b) -> (m a -> m b))

С помощью liftM вы можете сделать это:

Prelude> :m + Control.Monad
Prelude Control.Monad> let f x = print x >> return x
Prelude Control.Monad> liftM (takeWhile (<4)) $ mapM f [0..5]
0
1
2
3
4
5
[0,1,2,3]

Теперь это может быть не то, что вы хотели. mapM применяет функцию f ко всему списку по порядку, прежде чем возвращать список. Этот результирующий список затем передается в поднятую функцию takeWhile.

Если вы хотите прекратить печать после третьего элемента, вам придется прекратить вызывать печать. Это значит, не применять f к такому элементу. Итак, вы получите что-то простое, например:

Prelude> mapM_ f (takeWhile (<4) [0..5])

Кстати, если вы удивляетесь , почему mapM сначала напечатает все перед возвратом списка. Это можно увидеть, заменив функции их определениями:

mapM f [0..1]
=
sequence (map f [0..1])
=
sequence (f 0 : map f [1..1])
=
sequence (f 0 : f 1 : [])
=
sequence ((print 0 >> return 0) : f 1 : [])
= 
sequence ((print 0 >> return 0) : (print 1 >> return 1) : [])
=
do x  <- (print 0 >> return 0)
   xs <- (sequence ((print 1 >> return 1) : []))
   return (x:xs)
=
do x  <- (print 0 >> return 0)
   xs <- (do y  <- (print 1 >> return 1)
             ys <- sequence ([])
             return (y:ys))
   return (x:xs)
=
do x  <- (print 0 >> return 0)
   xs <- (do y  <- (print 1 >> return 1)
             ys <- return []
             return (y:ys))
   return (x:xs)
=
do x  <- (print 0 >> return 0)
   xs <- (do y <- (print 1 >> return 1)
             return (y:[]))
   return (x:xs)
=
do x  <- (print 0 >> return 0)
   xs <- (print 1 >> return (1:[]))
   return (x:xs)
=
do x <- (print 0 >> return 0)
   print 1
   return (x:1:[])
=
do print 0
   print 1
   return (0:1:[])

Этот процесс замены функций их определениями называется эквациональным рассуждением .

Если я не допустил ошибок, теперь вы можете (надеюсь) увидеть, что mapM (с использованием sequence) сначала печатает все, а , а затем возвращает список.

6 голосов
/ 16 июля 2009

Вы можете использовать один из пакета "Список" .

import Control.Monad.ListT (ListT)
import Data.List.Class (execute, fromList, joinM, takeWhile)
import Prelude hiding (takeWhile)

f x = print x >> return x
main =
  execute . takeWhile (< 4) .
  joinM $ fmap f (fromList [0..5] :: ListT IO Int)
  • fromList [0..5] создает монадический список, содержащий 0..5, который не выполняет монадических действий
  • fmap f к этому списку приводит к ListT IO (IO Int), который все еще не выполняет никаких монадических действий, только содержит те.
  • joinM превращает это в ListT IO Int. каждое содержащееся действие будет выполнено, когда элемент будет использован, и его результатом будет значение в списке.
  • takeWhile обобщается для любого List. И [], и "Monad m => ListT m" являются экземплярами List.
  • execute потребляет монадический список, выполняя все его действия.
  • Если вас интересуют результаты, вы можете использовать "toList :: List m => m a -> ItemM m [a]" ("ItemM (ListT IO)" - IO). так что в этом случае это "toList :: ListT IO a -> IO [a]". Более того, вы можете продолжать использовать функции более высокого порядка, такие как scanl и т. Д., Для обработки монадического списка во время его исполнения.
3 голосов
/ 15 марта 2012

В последнее время вы можете использовать взлом MonadList , который включает удобные функции , такие как takeWhileM, dropWhileM, deleteByM и многие другие.

...