【问题标题】:Join two IO actions using the same input in Haskell在 Haskell 中使用相同的输入连接两个 IO 操作
【发布时间】:2010-12-29 04:50:03
【问题描述】:

我有两个功能:

emptyDirectory, copyStubFileTo :: FilePath -> IO ()

我想将它们组合成如下方式:

forM_ ["1", "2"] $\n -> do
  emptyDirectory n
  copyStubFileTo n

Haskell 中是否有其他标准方法可以简化这种组合? 我的意思是加入两个 IO 操作并为它们提供相同的输入。

【问题讨论】:

    标签: haskell


    【解决方案1】:
    liftA2 (>>) emptyDirectory copyStubFileTo
    

    【讨论】:

    • mapM_ (liftA2 (>>) emptyDirectory copyStubFileTo) ["1", "2"]
    • 很难理解如何匹配 '(Monad m, Applicative f) => f (m a)' 和 'FilePath -> IO ()'。
    • @aXqd,这使用了函数的 Applicative 实例,您可以将其视为 Reader monad。这两个函数都需要一个环境(FilePath 参数),所以f (m a) 被实例化为(FilePath ->) (IO ()) 两个函数。然后使用(>>)(FilePath ->) 环境中组合两个IO () 动作,从而产生(FilePath ->) (IO ()) 的最终类型,通常写为FilePath -> IO ()。这与 Pelotom 的答案相同。
    • @aXqd:这是对符号的滥用,类似于(+ 2) 等运算符部分。真正的语法看起来像((->) Int Bool),上面的Applicative 实例是((->) FilePath)
    • @aXqd 它已经过时了,因为Monad 的任何实例也应该是Applicative 的实例,在这种情况下ap 是多余的。不幸的是,出于向后兼容性的原因,Monad 不是 Applicative 的子类,因为它应该是
    【解决方案2】:

    简短的回答是不,没有标准的方法。稍长一点的答案是你可以自己写一个>&> 组合器:

    (>&>) :: Monad m => (a -> m b) -> (a -> m c) -> a -> m c
    (f >&> g) x = f x >> g x
    

    正如您从类型中看到的那样,这完全符合您的要求。使用这个,你有emptyDirectory >&> copyStubFileTo :: FilePath -> IO (),所以

    mapM_ (emptyDirectory >&> copyStubFileTo) ["1", "2"]
    

    不幸的是,Hoogle 没有为类型签名提供任何信息,因此我认为可以安全地假设它不存在。


    现在,这不是我对>&> 的原始实现,因为我最初是根据类似的非单子组合器(&) :: (a -> b) -> (a -> c) -> a -> (b,c) 来看待它的,我发现我自己重新实现了一些规律性。如果你非单子地处理这个然后泛化,你最终会得到我认为是有用的组合器的集合(我一直在重新发明),这些组合器似乎不存在任何标准(即使我觉得至少其中一些)应该)。如果您想要一些更通用的组合器,它们就在这里;这些似乎都不存在于 Hoogle 上。 (选择适当的优先级留给感兴趣的读者作为练习。)

    首先,您需要(&) :: (a -> b) -> (a -> c) -> a -> (b,c),这是您想要的非单子版本。你不能合并bc,因为它们是任意类型,所以我们返回一个元组。这种类型只有一个合理的函数:(f & g) x = (f x, g x)。如果我们通过pointfree 提供这个实现,我们会得到更好的东西:

    (&) :: Monad m => m a -> m b -> m (a,b)
    (&) = liftM2 (,)
    

    这适用于函数,因为(r ->) 是一个单子(阅读器单子); (&) 抓住了做两件事并收集两个结果的概念,对于 (r ->),“做某事”是评估一个函数。

    但是,有了这个,您将拥有emptyDirectory & copyStubFileTo :: FilePath -> (IO (), IO ())。哎呀。因此,我们想要将 monad 从元组中取出,因此我们需要一个函数 tupleM :: Monad m => (m a, m b) -> m (a,b)。自己写这个,它使用上面的&函数:

    tupleM :: Monad m => (m a, m b) -> m (a,b)
    tupleM = uncurry (&)
    

    如果您查看类型,这实际上是有道理的,尽管它可能需要一些阅读(对我来说确实如此)。

    现在,我们可以为一元函数定义一个版本的(&)

    (<&>) :: Monad m => (a -> m b) -> (a -> m c) -> a -> m (b, c)
    f <&> g = tupleM . (f & g)
    

    我们现在有了emptyDirectory &lt;&amp;&gt; copyStubFileTo :: FilePath -&gt; IO ((),()),这是一个改进。但是我们真的不需要那个元组(尽管我们可能会进行更有趣的操作)。相反,我们想要(&gt;&amp;&gt;) :: Monad m =&gt; (a -&gt; m b) -&gt; (a -&gt; m c) -&gt; a -&gt; m c&gt;&gt; 的类似物),这是我们开始定义的(事实上,上面已经定义了)。因为无论如何我都在定义各种组合子,所以我将通过 &lt;.&gt; :: Functor f =&gt; (b -&gt; c) -&gt; (a -&gt; f b) -&gt; a -&gt; f c(来自 Control.Monad 的 &lt;=&lt; 的功能类似物)来定义它。

    (<.>) :: Functor f => (b -> c) -> (a -> f b) -> a -> f c
    (f <.> g) x = f <$> g x
    
    (>&>) :: (Monad m, Functor m) => (a -> m b) -> (a -> m c) -> a -> m c
    f >&> g = snd <.> (f <&> g)
    

    (如果您不喜欢Functor m 约束,请将&lt;$&gt; 替换为`liftM`。)最有趣的是,我们得到了一个完全不同的&gt;&amp;&gt; 实现。第一个实现侧重于&gt;&amp;&gt; 的“做两件事”方面;第二个重点是“评估两个功能”方面。这第二个实际上是我想到的第一个实现;我没有尝试编写第一个实现,因为我假设它会很丑。这可能是一个教训:-)

    【讨论】:

    • 我喜欢 Haskell 中的所有这些 ASCII 艺术。它让我微笑。 (就像:&gt; 组合器`)
    • 供以后参考,下次可以省点力气:(Control.Arrow.&amp;&amp;&amp;) :: (Arrow ~&gt;) =&gt; (a ~&gt; b) -&gt; (a ~&gt; c) -&gt; (a ~&gt; (b, c))。你的 (&lt;&amp;&gt;) 在 Kleisli 箭头上是一样的,而不是纯函数。
    • 其实你的“做两件事”(&gt;&amp;&gt;)只是在添加了ReaderT转换器之后的(&gt;&gt;),这是有道理的;并且“评估两个函数”(&gt;&amp;&gt;) 是 Kleisli 箭头的\f g -&gt; f &amp;&amp;&amp; g &gt;&gt;&gt; arr snd,这也是有道理的。
    • @camccann:谢谢。我以前用过箭头,但我还不明白。而且我认为Hoogle不会将功能箭头与其他类型统一,所以(&amp;&amp;&amp;)(***)和朋友们不要出现在那里。
    • 看来您对 Hoogle 的看法是正确的,这当然解释了为什么您不会注意到 (&amp;&amp;&amp;)。无论如何,要理解的主要事情是使用Arrows 基本上都是关于以一种非咖喱的无点风格写作;所以Control.Arrow 中的组合器对此很有帮助,即使您使用的只是旧的(-&gt;)
    【解决方案3】:

    免责声明:我认为您的原始代码可读性很强,重复变量n 两次并没有错。

    也就是说,如果你想变得花哨,((-&gt;) a) 形成一个 Applicative 实例,所以你可以这样做:

    import Control.Applicative
    
    forM_ ["1", "2"] $ (>>) <$> emptyDirectory <*> copyStubFileTo
    

    【讨论】:

    • :) 是的,我明白了。从 Antal 的答案中复制的 'mapM_ (emptyDirectory >&> copyStubFileTo) ["1", "2"]' 是我想要的。我认为 Haskell 中已经内置了类似 '>&>' 的内容。
    • @aXqd:它有,有点--它与ReaderT FilePath IO(&gt;&gt;) 相同。但是对于这么简单的事情,使用 monad 转换器是很笨拙的。
    【解决方案4】:

    你可以试试mapM_:

    mapM_ ($input) [list of functions]
    

    所以在你的情况下:

    mapM_ forM_ [emptyDirectory, copyStubFileTo] . flip ($) $ ["1", "2"] 
    

    未经测试。但应该是相似的。

    【讨论】:

    • 'mapM_ ($ input) [list of functions]' 是有道理的。它可以将“do”块中的这些行组合成一行 => 'mapM_ ($ n) [emptyDirectory, copyStubFileTo]'。我不知道 Control.Arrow 是否可以更进一步。无论如何谢谢。
    【解决方案5】:

    我仍然不确定我是否完全理解这一点,但它就是这样。 (基本上,我是从 FUZxxl 那里偷来的这个想法,但简化并解释了它)

    mapM_ (forM_ [list of inputs]) [list of functions]
    

    例如

    mapM_ (forM_ ["foo", "bar"]) [putStrLn, putStrLn . reverse]
    

    生产

    foo
    bar
    oof
    rab
    

    这可以概括

    xs >.> fs = mapM_ (forM_ xs) fs
    

    所以这会产生相同的输出

    main = ["foo","bar"] >.> [putStrLn, putStrLn . reverse]
    

    另外,&gt;.&gt; 是一张有趣的脸。

    基本上,mapM_ 接受一个函数和一个列表,并将该函数应用于列表的每个成员。所以我们创建了函数(forM_ ["foo", "bar"]),它的类型为(Monad m) =&gt; (String -&gt; m b) -&gt; m ()。它期望输入一个函数。幸运的是,第二个列表的每个成员都是一个函数!因此,第二个列表中的每个函数都将被馈送到这个高阶函数,从而产生所需的效果。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-03-06
      • 1970-01-01
      • 1970-01-01
      • 2012-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多