在 append 下保持 IO 惰性

Keeping IO lazy under append

我可能一直误以为 Haskell 比实际情况更懒惰,但我想知道是否有办法两全其美...

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

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

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

这两个值的类型都是 Num a => IO a。当 a 是:

时,IO a 是一个 Semigroup 实例
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}

正如我们所见,xy 都会产生各自的副作用,尽管 y 从来都不是必需的。

同样适用于Data.Monoid:

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

我想我明白为什么会发生这种情况,因为 SemigroupMonoid 实例都使用 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 资源的优先级列表,并使用第一个给我有用响应的资源。但是,我不想执行冗余查询(出于性能原因)。

Is there a way that I can retain the Semigroup or Monoid abstraction, while still get lazy IO?

有点,但也有缺点。我们的实例的潜在问题是 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(<*>)是根据ap实现的,所以第二个动作总是在[=25之前执行=] 已应用。

First-like 变体可以使用 ExceptT 或类似的,基本上任何具有 Left k >>= _ = Left k 样例的数据类型,以便我们可以在该点停止计算。尽管 ExceptT 用于例外情况,但它可能适用于您的用例。或者,Alternative 转换器之一(MaybeTExceptT)与 <|> 一起代替 <> 可能就足够了。


几乎完全惰性的IO类型也是可以的,但必须小心处理:

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 函数,但是 evaluateseqx

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

如果您要发布具有此功能的模块,我建议仅导出类型构造函数 LazyIOliftIO :: IO a -> LazyIO afinalizeLazyIO.

MaybeT monad transformer returns the first successful result, and does not execute the rest of the operations. In combination with the asum函数的Alternative实例,我们可以这样写:

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' 不会被执行。

我们也可以用一个模仿 Alternative:

Monoid 实例编写一个新类型的包装器
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

AlternativeMonoid之间的关系在this other SO question中有解释。