保持IO懒惰的追加

时间:2017-11-05 10:13:19

标签: haskell lazy-evaluation monoids semigroup

我可能一直误以为Haskell比现在更懒,但我想知道是否有办法让两个世界都做到最好......

Data.MonoidData.Semigroup定义了First的两种变体形式。 monoidal版本模拟最左边的非空值,而半群版本只是模拟最左边的值。

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

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

这两个值都具有Num a => IO a类型。当IO a为<{1}}时,Semigroupa个实例。

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 绑定,据我所知,这是严格的。

但是,如果我放弃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资源的优先级列表,并使用第一个给我一个有用的响应。但是,我不想执行冗余查询(出于性能原因)。

2 个答案:

答案 0 :(得分:1)

  

有没有办法可以保留Semigroup或Monoid抽象,同时仍然会得到懒惰的IO?

有点,但有缺点。我们实例的udnerlying问题是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&#39; (<*>)是按ap实施的,因此第二个操作将始终<>之前执行适用。

First类变体可能与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 ()}

您可以编写finalizeLazyIOevaluate&#39; s seq的{​​{1}}函数:

x

如果您要发布包含此功能的模块,我建议您只导出类型构造函数finalizeLazyIO :: LazyIO a -> IO a finalizeLazyIO k = do x <- runLazyIO k x `seq` return x LazyIOliftIO :: IO a -> LazyIO a

答案 1 :(得分: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'不会被执行。

我们还可以编写一个带有Monoid实例的newtype包装器,该实例模仿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解释Monoidtype Data struct { People []Person `xml:"person"` } type Person struct { Name string `xml:"name"` Gender string `xml:"gender"` Somethings []string `xml:"somethings>thing"` } 之间的关系。