Сохраняя 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
,