我最近需要将head
放在两个单子操作之间。这是SSCCE:
module Main where
f :: IO [Int]
f = return [1..5]
g :: Int -> IO ()
g = print
main = do
putStrLn "g <$> head <$> f"
g <$> head <$> f
putStrLn "g . head <$> f"
g . head <$> f
putStrLn "head <$> f >>= g"
head <$> f >>= g
该程序格式正确,可在不发出警告的情况下进行编译。但是,以上3个版本中只有一个版本有效。 1 。为什么会这样?
特别是,将f
和g
与中间的head
链接在一起的最佳方法是什么?我最终使用了第3个(以do
表示法的形式),但我真的不喜欢它,因为它应该是微不足道的单线 2 。
1 剧透警报:第三个是唯一打印1
的警报;在runhaskell
和repl
下,其他两个都保持沉默。
2 我确实意识到这些都是单线的,但是操作顺序在唯一可行的操作中确实让人感到困惑。
答案 0 :(得分:4)
第一个和第二个完全相同,因为在函数monad中,head
是左关联的,而<$>
是一个函数,而.
是 g . head <$> f
= fmap (print . head) (return [1..5] :: IO [Int])
= do { x <- (return [1..5] :: IO [Int])
; return ( print (head x) ) }
= do { let x = [1..5]
; return ( print (head x) ) } :: IO _whatever
=
return ( print 1 ) :: IO (IO ())
。然后,
return
我们那里有太多 = fmap (print . head) (return [1..5] :: IO [Int])
= return (print (head [1..5]))
= return (print 1)
个。实际上,
(head <$> f) >>= g
= (fmap head $ return [1..5]) >>= print
= (return (head [1..5])) >>= print
= (return 1) >>= print
是一个较短的推导。
第三个是
array
这显然还可以。
答案 1 :(得分:4)
也许最好的写法是:
f >>= g . head
或更详细的形式:
f >>= (g . head)
因此,我们基本上对fmap
的值执行f
(因此,我们将head
单子中包装的值的IO
取值),然后传递然后转到g
,例如:
(head <$> f) >>= g
在语义上是相同的。
但是现在如果我们使用g <$> head <$> f
会发生什么?让我们首先分析类型:
f :: IO [Int]
g :: Int -> IO ()
(<$>) :: Functor m => (a -> b) -> m a -> m b
(我在这里使用m
是为了避免与f
函数混淆)
这的规范形式是:
((<$>) ((<$>) g head) f)
第二个(<$>)
采用g :: Int -> IO ()
和head :: [c] -> c
作为参数,这意味着a ~ Int
,b ~ IO ()
和m ~ (->) [c]
。结果是:
(<$>) g head :: (->) [c] (IO ())
或更简单:
g <$> head :: [c] -> IO ()
第一个(<$>)
函数因此采用了g <$> head :: [c] -> IO ()
和IO [Int]
作为参数,这意味着m ~ IO
,a ~ [Int]
,c ~ Int
, b ~ IO ()
,因此我们得到以下类型:
(<$>) (g <$> head) f :: IO (IO ())
因此,我们没有执行任何实际操作:我们将fmap
的{{1}}列表添加到[Int]
动作(包装在IO
中)中。您可能会看到它是IO
:您没有“评估” return (print 1)
,而是print 1
包裹在return
中。
您当然可以在此处“吸收”外部IO
,然后使用内部IO
,例如:
IO
或更短:
evalIO :: IO (IO f) -> IO f
evalIO res = do
f <- res
f
(可以将其概括为各种evalIO :: IO (IO f) -> IO f
evalIO res = res >>= id
,但这与这里无关)。
Monad
也称为join :: Monad m => m (m a) -> m a
。