【问题标题】:Is it recommended to use recursive IO actions in the tail recursive form?是否推荐使用尾递归形式的递归 IO 动作?
【发布时间】:2018-04-24 08:38:33
【问题描述】:

考虑以下两种变体:

myReadListTailRecursive :: IO [String]
myReadListTailRecursive = go []
    where 
    go :: [String] -> IO [String]   
    go l = do {
                 inp <- getLine;
                 if (inp == "") then 
                     return l;
                 else go (inp:l);
                }

myReadListOrdinary :: IO [String]
myReadListOrdinary = do
        inp <- getLine
        if inp == "" then
            return []
        else
            do
                moreInps <- myReadListOrdinary
                return (inp:moreInps)

在普通编程语言中,人们会知道尾递归变体是更好的选择。

但是,通过this answer,很明显haskell的递归实现与反复使用递归栈的实现并不相似。

但是因为在这种情况下,所讨论的程序涉及动作和严格的单子,我不确定是否适用相同的推理。其实我觉得在IO的情况下,尾递归的形式确实更好。我不确定如何正确推理。


编辑: David Young 指出这里最外面的电话是(&gt;&gt;=)。即使在这种情况下,其中一种风格是否比另一种更有优势?

【问题讨论】:

  • 这实际上不是尾递归。如果你对第一个脱糖,你会得到go l = getLine &gt;&gt;= (\inp -&gt; if (inp == "") then return l else go (inp:l))。最外面的电话是(&gt;&gt;=)
  • @DavidYoung 对,我完全忘记了这一点。在这种情况下,一种风格比另一种风格有什么优势吗?
  • 我认为第一个至少在道德上是尾递归的。相比之下,f x = bool (x==0) (f (x-1)) 0 不是尾递归,但bool 最终会将控制权转移给f (x-1),所以它在恒定空间中运行。我想这与 IO 的 &gt;&gt;= 相同,只是因为我们需要深入研究 IO 实现,所以它不那么容易引起注意。 (另外,请注意,两个发布的 IO 操作的结果是不同的,因为它们以不同的顺序返回列表)

标签: haskell recursion io tail-recursion


【解决方案1】:

FWIW,我会选择现有的单子组合器并专注于可读性/简洁性。使用unfoldM :: Monad m =&gt; m (Maybe a) -&gt; m [a]

import Control.Monad (liftM, mfilter)
import Control.Monad.Loops (unfoldM)

myReadListTailRecursive :: IO [String]
myReadListTailRecursive = unfoldM go
  where
    go :: IO (Maybe String)
    go = do
        line <- getLine
        return $ case line of
            "" -> Nothing
            s -> Just s

或使用MonadPlusMaybe 实例,与mfilter :: MonadPlus m =&gt; (a -&gt; Bool) -&gt; m a -&gt; m a

myReadListTailRecursive :: IO [String]
myReadListTailRecursive = unfoldM (liftM (mfilter (/= "") . Just) getLine)

另一个更通用的选择可能是使用LoopT

【讨论】:

    【解决方案2】:

    我真的不会这么写,但你在做什么已经足够清楚了。 (顺便说一句,如果您希望能够有效地从链中的任何函数插入任意输出,而不使用 monad,您可以尝试Data.ByteString.Builder。)

    您的第一个实现非常类似于左折叠,而您的第二个实现非常类似于右折叠或地图。 (您可以尝试实际编写它们!)第二个对于 I/O 有几个优点。对于处理输入和输出来说,其中最重要的一点是它可以是交互式的

    你会注意到第一个从外部构建整个列表:为了确定列表的第一个元素是什么,程序需要计算整个结构以到达最里面的 thunk,即 @ 987654322@。程序首先生成整个数据结构,然后开始处理它。这在您减少列表时很有用,因为尾递归函数和严格的左折叠非常有效。

    用第二个,最外层的thunk包含列表的头和尾,所以你可以抓住尾巴,然后调用thunk生成第二个列表。这可以用于无限列表,并且可以生成和返回部分结果。

    这是一个人为的例子:一个程序每行读取一个整数并打印到目前为止的总和。

    main :: IO ()
    main = interact( display . compute 0 . parse . lines )
      where parse :: [String] -> [Int]
            parse [] = []
            parse (x:xs) = (read x):(parse xs)
    
            compute :: Int -> [Int] -> [Int]
            compute _ [] = []
            compute accum (x:xs) = let accum' = accum + x
                                   in accum':(compute accum' xs)
    
            display = unlines . map show
    

    如果你以交互方式运行它,你会得到类似的东西:

    $ 1
    1
    $ 2
    3
    $ 3
    6
    $ 4
    10
    

    但你也可以用一个累加参数递归地写compute

    main :: IO ()
    main = interact( display . compute [] . parse . lines )
      where parse :: [String] -> [Int]
            parse = map read
    
            compute :: [Int] -> [Int] -> [Int]
            compute xs [] = reverse xs
            compute [] (y:ys) = compute [y] ys
            compute (x:xs) (y:ys) = compute (x+y:x:xs) ys
    
            display = unlines . map show
    

    这是一个人为的例子,但严格的左折叠是一种常见的模式。但是,如果您使用累积参数编写 computeparse,这就是您尝试以交互方式运行时得到的结果,并在数字后点击 EOF(Unix 上的control-D,Windows 上的control-Z) 4:

    $ 1
    $ 2
    $ 3
    $ 4
    1
    3
    6
    10
    

    这个左折叠版本需要计算整个数据结构,然后才能读取其中的任何一个。这永远不能在无限列表上工作(你什么时候会达到基本情况?如果你这样做了,你甚至会如何反转无限列表?)并且在退出之前无法响应用户输入的应用程序是一个交易 -断路器。

    另一方面,尾递归版本的累积参数可以很严格,并且运行效率更高,尤其是当它没有立即被消耗时。除了参数之外,它不需要保留任何 thunk 或上下文,它甚至可以重用相同的堆栈帧。一个严格的累加函数,例如Data.List.foldl',是一个很好的选择,当你将一个列表减少为一个值,而不是建立一个急切评估的输出列表时。 sumproductany 等函数不能返回任何有用的中间值。它们本质上必须先完成计算,然后返回最终结果。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-03-21
      • 2019-07-08
      • 2019-03-26
      • 1970-01-01
      • 2017-11-11
      • 2013-09-14
      • 2018-03-17
      • 1970-01-01
      相关资源
      最近更新 更多