【问题标题】:Why doesn't this simple composition work?为什么这个简单的组合不起作用?
【发布时间】:2019-02-27 18:04:52
【问题描述】:

我最近需要将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。这是为什么呢?

具体来说,将fghead 在中间链接的最佳方式是什么?我最终使用了第三个(以do 表示法的形式),但我不太喜欢它,因为它应该是一个微不足道的单行2


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

2 我确实意识到这些都是单行的,但是在唯一可行的情况下,操作顺序确实令人困惑。

【问题讨论】:

  • (&lt;$&gt;) 是左关联的,这意味着g &lt;$&gt; head &lt;$&gt; f(g &lt;$&gt; head) &lt;$&gt; f
  • @WillemVanOnsem 当然可以,但g &lt;$&gt; (head &lt;$&gt; f) 也不起作用。坦率地说,我有点困惑为什么上面的 any 不起作用。
  • 在上面你fmap 的值,所以g &lt;$&gt; head &lt;$&gt; f 基本上创建一个IO (IO [Int])),而不是IO ()
  • 啊,我读到这里笑了。如此明显,却又如此棘手!把它放在一个答案中,这样我就可以接受了:)我真的想知道是否有可能至少为此发出警告;毕竟这是一个无意丢弃的值。
  • @BartekBanachewicz 如果您希望此类错误出现错误,您可以将其编写为绑定:() &lt;- g . head &lt;$&gt; f。这会产生类型错误。

标签: haskell monads applicative


【解决方案1】:

可能最好的写法是:

f >>= g . head

或更详细的形式:

f >>= (g . head)

所以我们基本上对f 的值执行fmap(因此我们采用head 包装在IO monad 中的值),然后我们传递给g,例如:

(head <$> f) >>= g

在语义上是相同的。

但是现在如果我们使用g &lt;$&gt; head &lt;$&gt; f 会发生什么?我们先来分析一下类型:

f :: IO [Int]
g :: Int -> IO ()
(<$>) :: Functor m => (a -> b) -> m a -> m b

(我在这里使用m 以避免与f 函数混淆)

这个的规范形式是:

((<$>) ((<$>) g head) f)

第二个(&lt;$&gt;)g :: Int -&gt; IO ()head :: [c] -&gt; c 作为参数,这意味着a ~ Intb ~ IO ()m ~ (-&gt;) [c]。所以结果是:

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

或者不那么冗长:

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

第一个(&lt;$&gt;)函数因此将g &lt;$&gt; head :: [c] -&gt; IO ()IO [Int]作为参数,这意味着m ~ IOa ~ [Int]c ~ Intb ~ IO (),因此我们得到类型:

(<$>) (g <$> head) f :: IO (IO ())

因此我们不执行任何实际操作:我们将fmap [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

(这可以推广到各种Monads,但在这里无关紧要)。

evalIO 也称为join :: Monad m =&gt; m (m a) -&gt; m a

【讨论】:

  • 也称为join。 :)
  • @WillNess:是的,我在评论的时候正在处理这个问题;)
  • 我不愿称其为“展开”,因为这意味着您可以展开单个 IO f 以获取 f。我喜欢把它想象成外部的IO“吸收”内部的IO,让f安全地包裹在整个过程中。
  • @chepner:好点子,你认为这是一个更好的表述吗?
  • 我觉得不错。我不知道是否有更深层次的微妙之处可以说明是内部单子还是外部单子“幸存”了这个过程,或者两者都被新的单子取代了:)
【解决方案2】:

第一个和第二个完全一样,因为&lt;$&gt;是左关联的,head是一个函数,而&lt;$&gt;是函数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 ())

我们的returns 太多了。事实上,

    =  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

这显然没问题。

【讨论】:

    猜你喜欢
    • 2011-10-25
    • 2012-06-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-08
    相关资源
    最近更新 更多