我可能一直认为 Haskell 比它更懒惰,但我想知道是否有一种方法可以两全其美......Data.Monoid
和 Data.Semigroup
定义 First
的两个变体. monoidal 版本对最左边的非空值进行建模,而 semigroup 版本只是对最左边的值进行建模。
这适用于纯值值,但考虑不纯值:
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)
值(value)观: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
不打印。那么我的问题是:
我可以两全其美吗?有没有一种方法可以保留 Semigroup 或 Monoid 抽象,同时仍然获得惰性 IO?
例如,是否有某种
LazyIO
我可以包装的容器First
值,以便我得到我想要的惰性 IO?我所追求的实际情况是,我想查询 IO 资源的优先列表以获取数据,并使用第一个给我有用响应的列表。但是,我不想执行冗余查询(出于性能原因)。
最佳答案
Alternative
MaybeT
的实例monad 转换器返回第一个成功的结果,并且不执行其余的操作。结合 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
在 this other SO question 中有解释.
关于haskell - 在 append 下保持 IO 惰性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47120384/