为什么这个简单的构图不起作用?
Why doesn't this simple composition work?
我最近需要将 head
放在两个 monadic 操作之间。这是 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。这是为什么?
具体来说,link f
和 g
以及中间的 head
的最佳方式是什么?我最终使用了第三个(以 do
符号的形式),但我不太喜欢它,因为它应该是一个简单的单行2。
1剧透警告:第三个是唯一一个打印1
的;另外两个沉默了,都在runhaskell
和repl
下。
2 我确实意识到这些都是单行的,但是在唯一有效的行中,操作顺序感觉真的很混乱。
第一个和第二个完全一样,因为<$>
是左结合而head
是一个函数,而<$>
在函数monad中是.
.那么,
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
这显然没问题。
可能最好的写法是:
f >>= g . head
或更详细的形式:
f >>= (g . head)
所以我们基本上对 f
的值执行 fmap
(因此我们采用 IO
monad 中包装的值的 head
),然后然后我们传递给 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
[Int]
列表到 IO
操作(包含在 IO
中)。您可以将其视为 return (print 1)
:您没有 "evaluate" print 1
,但您 return
包裹在 IO
.
这里当然可以"absorb"外面的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
,但这在这里无关紧要)。
evalIO
也称为 join :: Monad m => m (m a) -> m a
。
我最近需要将 head
放在两个 monadic 操作之间。这是 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。这是为什么?
具体来说,link f
和 g
以及中间的 head
的最佳方式是什么?我最终使用了第三个(以 do
符号的形式),但我不太喜欢它,因为它应该是一个简单的单行2。
1剧透警告:第三个是唯一一个打印1
的;另外两个沉默了,都在runhaskell
和repl
下。
2 我确实意识到这些都是单行的,但是在唯一有效的行中,操作顺序感觉真的很混乱。
第一个和第二个完全一样,因为<$>
是左结合而head
是一个函数,而<$>
在函数monad中是.
.那么,
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
这显然没问题。
可能最好的写法是:
f >>= g . head
或更详细的形式:
f >>= (g . head)
所以我们基本上对 f
的值执行 fmap
(因此我们采用 IO
monad 中包装的值的 head
),然后然后我们传递给 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
[Int]
列表到 IO
操作(包含在 IO
中)。您可以将其视为 return (print 1)
:您没有 "evaluate" print 1
,但您 return
包裹在 IO
.
这里当然可以"absorb"外面的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
,但这在这里无关紧要)。
evalIO
也称为 join :: Monad m => m (m a) -> m a
。