Сохраняя IO ленивым при добавлении

Возможно, у меня сложилось ложное впечатление, что Haskell ленивее, чем он есть, но мне интересно, есть ли способ получить лучшее из обоих миров...

Data.Monoid а также Data.Semigroup определить две вариации First, Моноидальная версия моделирует крайнее левое непустое значение, тогда как версия полугруппы просто моделирует крайнее левое значение.

Это прекрасно работает для чистых значений, но учитывайте нечистые значения:

x = putStrLn "x" >> return 42
y = putStrLn "y" >> return 1337

Оба эти значения имеют тип Num a => IO a, IO a это Semigroup случай, когда a является:

instance Semigroup a => Semigroup (IO a)
  -- Defined in `Data.Orphans'

Это означает, что можно объединить два IO (First a) ценности:

Prelude Data.Semigroup Data.Orphans> fmap First x <> fmap First y
x
y
First {getFirst = 42}

Как мы видим, оба x а также y производить свои соответствующие побочные эффекты, даже если y никогда не требуется.

То же самое относится и к Data.Monoid:

Prelude Data.Monoid> fmap (First . Just) x <> fmap (First . Just) y
x
y
First {getFirst = Just 42}

Я думаю, я понимаю, почему это происходит, учитывая, что оба Semigroup а также Monoid случаи использования liftA2 который, кажется, в конечном счете основывается на IO bind, что строго, насколько я понимаю.

Если я обойдусь без First однако, я могу получить более ленивую оценку:

first x _ = x

mfirst x y = do
  x' <- x
  case x' of
    (Just _) -> return x'
    Nothing -> y

Используя оба из этих игнорирует y:

Prelude> first x y
x
42
Prelude> mfirst (fmap Just x) (fmap Just y)
x
Just 42

В обоих этих случаях y не печатается

Тогда мой вопрос:

Могу ли я получить лучшее из обоих миров? Есть ли способ, которым я могу сохранить абстракцию полугруппы или моноида, в то же время получая ленивый ввод-вывод?

Есть ли, например, какая-то LazyIO контейнер, который я могу обернуть First значения в, так что я получаю ленивый IO я хотел бы иметь?

Реальный сценарий, который мне нужен, заключается в том, что я хотел бы запросить приоритетный список ресурсов ввода-вывода для данных и использовать первый, который дает мне полезный ответ. Однако я не хочу выполнять лишние запросы (по соображениям производительности).

2 ответа

Решение

Alternative экземпляр для MaybeT Монадный преобразователь возвращает первый успешный результат и не выполняет остальные операции. В сочетании с asum функция, мы можем написать что-то вроде:

import Data.Foldable (asum)
import Control.Applicative
import Control.Monad.Trans.Maybe

action :: Char -> IO Char
action c = putChar c *> return c

main :: IO ()
main = do
    result <- runMaybeT $ asum $ [ empty
                                 , MaybeT $ action 'x' *> return Nothing
                                 , liftIO $ action 'v'
                                 , liftIO $ action 'z'
                                 ]
    print result

где финал action 'z' не будет выполнен

Мы также можем написать новый тип оболочки с Monoid экземпляр, который имитирует Alternative:

newtype FirstIO a = FirstIO (MaybeT IO a)

firstIO :: IO (Maybe a) -> FirstIO a
firstIO ioma = FirstIO (MaybeT ioma)

getFirstIO :: FirstIO a -> IO (Maybe a)
getFirstIO (FirstIO (MaybeT ioma)) = ioma

instance Monoid (FirstIO a) where
    mempty = FirstIO empty
    FirstIO m1 `mappend` FirstIO m2 = FirstIO $ m1 <|> m2

Отношение между Alternative а также Monoid объясняется в этом другом вопросе ТАК.

Есть ли способ, которым я могу сохранить абстракцию полугруппы или моноида, в то же время получая ленивый ввод-вывод?

Несколько, но есть и недостатки. Главная проблема для наших экземпляров состоит в том, что общий экземпляр для Applicative будет выглядеть

instance Semigroup a => Semigroup (SomeApplicative a) where
    x <> y = (<>) <$> x <*> y

Мы здесь во власти (<*>) и обычно второй аргумент y будет хотя бы в WHNF. Например в Maybe Реализация первой строки будет работать нормально, а вторая строка error:

liftA2 (<>) Just (First 10) <> Just (error "never shown")
liftA2 (<>) Just (First 10) <> error "fire!"

IO "s (<*>) реализуется с точки зрения ap поэтому второе действие всегда будет выполняться раньше <> применены.

First Подобный вариант возможен с ExceptT или аналогичные, по существу, любой тип данных, который имеет Left k >>= _ = Left k как случай, чтобы мы могли остановить вычисление в этой точке. Хотя ExceptT предназначен для исключений, он может хорошо работать для вашего варианта использования. В качестве альтернативы, один из Alternative трансформаторы (MaybeT, ExceptT) вместе с <|> вместо <> может быть достаточно.


Возможен почти полностью ленивый тип ввода-вывода, но с ним нужно обращаться осторожно:

import Control.Applicative (liftA2)
import System.IO.Unsafe (unsafeInterleaveIO)  

newtype LazyIO a = LazyIO { runLazyIO :: IO a }

instance Functor LazyIO where
  fmap f = LazyIO . fmap f . runLazyIO

instance Applicative LazyIO where
  pure    = LazyIO . pure
  f <*> x = LazyIO $ do
              f' <- unsafeInterleaveIO (runLazyIO f)
              x' <- unsafeInterleaveIO (runLazyIO x)
              return $ f' x'

instance Monad LazyIO where
  return  = pure
  f >>= k = LazyIO $ runLazyIO f >>= runLazyIO . k

instance Semigroup a => Semigroup (LazyIO a) where
  (<>) = liftA2 (<>)

instance Monoid a => Monoid (LazyIO a) where
  mempty  = pure mempty
  mappend = liftA2 mappend

unsafeInterleaveIO включит поведение, которое вы хотите (и используется в getContents и другие ленивые IO Prelude функции), но его следует использовать с осторожностью. Получатель чего-то IO операции полностью отключены в этот момент. Только когда мы проверяем значения, мы запустим оригинал IO:

ghci> :module +Data.Monoid Control.Monad
ghci> let example = fmap (First . Just) . LazyIO . putStrLn $ "example"
ghci> runLazyIO $ fmap mconcat $ replicateM 100 example
First {getFirst = example
Just ()}

Обратите внимание, что мы получили только наши example один раз в выходной, но в совершенно случайном месте, так как putStrLn "example" а также print result получил чередование, так как

print (First x) = putStrLn (show (First x))
                = putStrLn ("First {getFirst = " ++ show x ++ "}")

а также show x наконец поставит IO необходимо получить x В бою. Действие будет вызвано только один раз, если мы будем использовать результат несколько раз:

ghci> :module +Data.Monoid Control.Monad
ghci> let example = fmap (First . Just) . LazyIO . putStrLn $ "example"
ghci> result <- runLazyIO $ fmap mconcat $ replicateM 100 example
ghci> result
First {getFirst = example
Just ()}
ghci> result
First {getFirst = Just ()}

Вы могли бы написать finalizeLazyIO функция, которая либо evaluate с или seq "s x хоть:

finalizeLazyIO :: LazyIO a -> IO a
finalizeLazyIO k = do
  x <- runLazyIO k
  x `seq` return x

Если бы вы опубликовали модуль с этими функциями, я бы рекомендовал экспортировать только конструктор типов LazyIO, liftIO :: IO a -> LazyIO a а также finalizeLazyIO,

Другие вопросы по тегам