在追加下保持 IO 懒惰

Mar*_*ann 5 haskell lazy-evaluation monoids semigroup

我可能一直错误地认为 Haskell 比现在更懒惰,但我想知道是否有办法两全其美……

Data.Monoid并Data.Semigroup定义 的两个变体First。幺半群版本对最左边的非空值建模,而半群版本只是对最左边的值建模。

这适用于纯值,但考虑不纯值:

x = putStrLn "x" >> return 42
y = putStrLn "y" >> return 1337
Run Code Online (Sandbox Code Playgroud)

这两个值都具有类型Num a => IO a。IO a是一个Semigroup实例,当a是:

instance Semigroup a => Semigroup (IO a)
  -- Defined in `Data.Orphans'
Run Code Online (Sandbox Code Playgroud)

这意味着可以组合两个IO (First a)值:

Prelude Data.Semigroup Data.Orphans> fmap First x <> fmap First y
x
y
First {getFirst = 42}
Run Code Online (Sandbox Code Playgroud)

我们可以看到,虽然双方x并y产生各自的副作用,即使y是从来没有要求。

这同样适用于Data.Monoid:

Prelude Data.Monoid> fmap (First . Just) x <> fmap (First . Just) y
x
y
First {getFirst = Just 42}
Run Code Online (Sandbox Code Playgroud)

我想我明白为什么会发生这种情况,因为Semigroup和Monoid实例都使用liftA2,这似乎最终基于IO bind,据我所知,这是严格的。

First但是,如果我省去了抽象,我可以得到更懒惰的评估:

first x _ = x

mfirst x y = do
  x' <- x
  case x' of
    (Just _) -> return x'
    Nothing -> y
Run Code Online (Sandbox Code Playgroud)

使用这两个忽略y:

Prelude> first x y
x
42
Prelude> mfirst (fmap Just x) (fmap Just y)
x
Just 42
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,y都不会打印。

那么我的问题是:

我可以两全其美吗?有没有办法可以保留半群或 Monoid 抽象,同时仍然获得懒惰的 IO?

例如,是否有某种LazyIO容器可以将First值包装在其中,以便获得我想要的惰性 IO?

我所追求的实际场景是,我想查询数据的 IO 资源的优先列表,并使用第一个给出有用响应的列表。但是,我不想执行冗余查询(出于性能原因)。

dan*_*iaz 3

Alternativemonad 转换器的实例返回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
Run Code Online (Sandbox Code Playgroud)

最终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
Run Code Online (Sandbox Code Playgroud)

Alternative和之间的关系在另一个 SO 问题Monoid中进行了解释。