是否建议在尾递归形式中使用递归IO操作?

时间:2017-11-11 04:12:19

标签: haskell recursion io tail-recursion

考虑以下两个变体:

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)

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

然而,经过http://localhost:4200,显然haskell的递归实现与重复使用递归堆栈的实现并不相似。

但是因为在这种情况下,有问题的程序涉及行动和严格的单子,我不确定是否适用相同的推理。事实上,我认为在IO情况下,尾递归形式确实更好。我不确定如何正确推理这个。

编辑: David Young指出,此处最外面的电话是(>>=)。即使在这种情况下,这些风格中的一种是否优于另一种?

2 个答案:

答案 0 :(得分:2)

这真的不是我写的方式,但是你所做的很清楚。 (顺便说一句,如果你想能够有效地从链中的任何函数插入任意输出,而不使用monad,你可以尝试Data.ByteString.Builder。)

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

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

对于第二个,最外面的thunk包含列表的头部和尾部,因此你可以抓住尾部,然后调用thunk来生成第二个列表。这可以使用无限列表,它可以生成并返回部分结果。

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

return l

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

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 compute,则这是您尝试以交互方式运行时所获得的,并且在Unix上命中EOF(parse,{{1在Windows上)在数字4之后:

control-D

这个左折版本需要先计算整个数据结构才能读取任何数据结构。这无法在无限列表上工作(你何时会达到基本情况?如果你这样做,你甚至会如何反转无限列表?)和一个在退出之前无法响应用户输入的应用程序 - 断路器。

另一方面,尾递归版本在其累积参数中可以是严格的,并且将更有效地运行,尤其是当它没有被立即消耗时。除了参数之外,它不需要保留任何thunk或上下文,甚至可以重用相同的堆栈帧。当您将列表缩减为值,而不是构建急切评估的输出列表时,严格的累积函数(例如control-Z)是一个很好的选择。 $ 1 $ 2 $ 3 $ 4 1 3 6 10 Data.List.foldl'sum等函数无法返回任何有用的中间值。它们本身必须首先完成计算,然后返回最终结果。

答案 1 :(得分:2)

FWIW,我会选择现有的monadic组合器,专注于可读性/安心性。使用unfoldM :: Monad m => m (Maybe a) -> 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

使用MonadPlus Maybe myReadListTailRecursive :: IO [String] myReadListTailRecursive = unfoldM (liftM (mfilter (/= "") . Just) getLine) 实例,mfilter :: MonadPlus m => (a -> Bool) -> m a -> m a

/* PSEUDOCODE based on PHP */
$selection = $this->getSelection($id);

/* Below code will not work in a strict language */
if (is_array($selection))
    show_template($selection);//forwards *array* results to the view script
else 
    print $selection; //prints the *string* notice message

/* Returns either array or string */
/* return param doubles as a business-payload or error message */
function getSelection($id)
{
    $result = $db->getResultsById($id);
    if ($result) return $result;//array
    else return "No results";//string
}

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