我可能一直认为 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)
值:
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 资源的优先列表以获取数据,并使用第一个给我有用响应的列表。但是,我不想执行冗余查询(出于性能原因)。