5

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

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

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

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

这两个值都有类型Num a => IO aIO 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}

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

4

2 回答 2

3

monad 转换器的Alternative实例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

决赛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

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

于 2017-11-05T20:13:42.280 回答
1

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

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

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

我们在这里受到 的摆布(<*>),通常第二个参数y至少在 WHNF 中。例如,在Maybe's implementation 中,第一行将正常工作,第二行将error

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

IO's(<*>)是根据 来实现的ap,所以第二个动作总是在应用之前执行<>

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


几乎完全惰性的 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其他惰性 IOPrelude功能),但必须小心使用。此时操作顺序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得到了interleaved,因为

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 ()}

您可以编写一个s 或's的finalizeLazyIO函数:evaluateseqx

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

如果您要发布具有此功能的模块,我建议仅导出类型 constructorLazyIO和.liftIO :: IO a -> LazyIO afinalizeLazyIO

于 2017-11-05T14:48:06.737 回答