在 append 下保持 IO 惰性
Keeping IO lazy under append
我可能一直误以为 Haskell 比实际情况更懒惰,但我想知道是否有办法两全其美...
Data.Monoid
和 Data.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}
正如我们所见,x
和 y
都会产生各自的副作用,尽管 y
从来都不是必需的。
同样适用于Data.Monoid
:
Prelude Data.Monoid> fmap (First . Just) x <> fmap (First . Just) y
x
y
First {getFirst = Just 42}
我想我明白为什么会发生这种情况,因为 Semigroup
和 Monoid
实例都使用 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
转换器之一(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
和其他惰性 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
函数,但是 evaluate
或 seq
的 x
:
finalizeLazyIO :: LazyIO a -> IO a
finalizeLazyIO k = do
x <- runLazyIO k
x `seq` return x
如果您要发布具有此功能的模块,我建议仅导出类型构造函数 LazyIO
、liftIO :: IO a -> LazyIO a
和 finalizeLazyIO
.
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
Alternative
和Monoid
之间的关系在this other SO question中有解释。
我可能一直误以为 Haskell 比实际情况更懒惰,但我想知道是否有办法两全其美...
Data.Monoid
和 Data.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}
正如我们所见,x
和 y
都会产生各自的副作用,尽管 y
从来都不是必需的。
同样适用于Data.Monoid
:
Prelude Data.Monoid> fmap (First . Just) x <> fmap (First . Just) y
x
y
First {getFirst = Just 42}
我想我明白为什么会发生这种情况,因为 Semigroup
和 Monoid
实例都使用 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
转换器之一(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
和其他惰性 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
函数,但是 evaluate
或 seq
的 x
:
finalizeLazyIO :: LazyIO a -> IO a
finalizeLazyIO k = do
x <- runLazyIO k
x `seq` return x
如果您要发布具有此功能的模块,我建议仅导出类型构造函数 LazyIO
、liftIO :: IO a -> LazyIO a
和 finalizeLazyIO
.
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
Alternative
和Monoid
之间的关系在this other SO question中有解释。