【问题标题】:Keeping IO lazy under append在 append 下保持 IO 惰性
【发布时间】:2018-04-17 15:19:26
【问题描述】:

我可能一直认为 Haskell 比它更懒惰,但我想知道是否有一种方法可以两全其美......

Data.MonoidData.Semigroup 定义了First 的两个变体。 monoidal 版本对最左边的非空值进行建模,而 semigroup 版本只是对最左边的值进行建模。

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

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

这两个值的类型都是Num a => IO aIO aSemigroup 实例,而 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}

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

【问题讨论】:

  • 您需要 semigroup/monoid 抽象,还是 First 抽象(也就是提前退出 IO 函数)?
  • @Zeta 理想情况下,我更愿意保留半群/幺半群抽象,但我愿意接受建议。
  • 您可以尝试为 Semigroup a =&gt; Semigroup (IO a) 编写自己的实例,以满足您正在寻找的行为。
  • @danidiaz 由于IO 包装在FirstIO (launchMissiles *&gt; return Nothing)IO 包装在FirstIO (return Nothing) 中不同,我会说不,它们不相等。 &lt;&gt; 之后的位从不考虑评估。

标签: haskell lazy-evaluation monoids semigroup


【解决方案1】:

MaybeT monad 转换器的 Alternative 实例返回第一个成功的结果,并且不执行其余操作。结合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' 不会被执行。

我们也可以用一个模仿AlternativeMonoid 实例编写一个新类型包装器:

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 中进行了说明。

【讨论】:

  • 完美,基于newtypeMonoid 实例(和Semigroup)正是我想要的!谢谢!
【解决方案2】:

有没有一种方法可以保留 Semigroup 或 Monoid 抽象,同时仍然获得惰性 IO?

有点,但也有缺点。我们实例的根本问题是Applicative 的通用实例看起来像

instance Semigroup a => Semigroup (SomeApplicative a) where
    x <> y = (<>) <$> x <*> y

我们在这里受到(&lt;*&gt;) 的摆布,通常第二个参数y 至少在WHNF 中。例如在Maybe 的实现中,第一行可以正常工作,第二行可以error

liftA2 (<>) Just (First 10) <> Just (error "never shown")
liftA2 (<>) Just (First 10) <> error "fire!"

IO(&lt;*&gt;) 是根据ap 实现的,所以第二个动作总是&lt;&gt; 应用之前执行。

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


几乎完全惰性的 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 函数,该函数可以是evaluates 或seqx

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

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

【讨论】:

    猜你喜欢
    • 2016-04-26
    • 1970-01-01
    • 2023-04-03
    • 1970-01-01
    • 1970-01-01
    • 2013-02-01
    • 2012-01-03
    • 1970-01-01
    • 2014-06-25
    相关资源
    最近更新 更多