我可能一直误以为Haskell比现在更懒,但我想知道是否有办法让两个世界都做到最好......
Data.Monoid
和Data.Semigroup
定义了First
的两种变体形式。 monoidal版本模拟最左边的非空值,而半群版本只是模拟最左边的值。
这适用于纯值,但请考虑不纯的值:
x = putStrLn "x" >> return 42
y = putStrLn "y" >> return 1337
这两个值都具有Num a => IO a
类型。当IO a
为<{1}}时,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}
我想我理解为什么会发生这种情况,因为Semigroup
和Monoid
个实例都使用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资源的优先级列表,并使用第一个给我一个有用的响应。但是,我不想执行冗余查询(出于性能原因)。
答案 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
变换器(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
&#39; s seq
的{{1}}函数:
x
如果您要发布包含此功能的模块,我建议您只导出类型构造函数finalizeLazyIO :: LazyIO a -> IO a
finalizeLazyIO k = do
x <- runLazyIO k
x `seq` return x
,LazyIO
和liftIO :: 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
解释Monoid
与type Data struct {
People []Person `xml:"person"`
}
type Person struct {
Name string `xml:"name"`
Gender string `xml:"gender"`
Somethings []string `xml:"somethings>thing"`
}
之间的关系。