为什么这个简单的构图不起作用?

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 fg 以及中间的 head 的最佳方式是什么?我最终使用了第三个(以 do 符号的形式),但我不太喜欢它,因为它应该是一个简单的单行2


1剧透警告:第三个是唯一一个打印1的;另外两个沉默了,都在runhaskellrepl下。

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 ~ Intb ~ IO ()m ~ (->) [c] .所以结果是:

 (<$>) g head :: (->) [c] (IO ())

或更简洁:

g <$> head :: [c] -> IO ()

第一个 (<$>) 函数将 g <$> head :: [c] -> IO ()IO [Int] 作为参数,因此这意味着 m ~ IOa ~ [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