如何在 Haskell 递归调用中打印迭代?

Posted

技术标签:

【中文标题】如何在 Haskell 递归调用中打印迭代?【英文标题】:how to print the iterations inside a Haskell recursion call? 【发布时间】:2018-02-17 20:29:23 【问题描述】:

考虑下面的代码,它返回一个数字的阶乘:

factorial n = go n 1
  where
    go n ret | n > 1 = go (n-1) (ret * n)
             | otherwise = ret

如何在go n ret 的每个递归调用中打印n?由于n 每次都在递减,我想看看是否可以在每次递减时打印它(如5 4 3 2 1)。 这就是我尝试这样做的方式(这是完全错误的),但我想不出其他方式:

factorial n = go n 1
  where
    go n ret | n > 1 = go (n-1) (ret * n) print n
             | otherwise = ret

【问题讨论】:

出于调试目的,您可以使用 Debug.Trace: hackage.haskell.org/package/base-4.10.1.0/docs/Debug-Trace.html 【参考方案1】:

类型

要打印,您需要将值提升到 I/O 应用程序;类型将从

factorial :: (Num a, Ord a) => a -> a

factorial :: (Num a, Ord a, Show a) => a -> IO a

go 操作的作用

go 动作需要做两件事:

    打印号码n 递归调用go 或产生结果

如何依次执行两个 I/O 操作

要做两件事,我们需要一些组合器来组合它们。这里合适的是*>

(*>) :: Applicative f => f a -> f b -> f b

顺序动作,丢弃第一个参数的值。

这正是我们需要的,因为我们不需要使用第一个动作的值(print 动作的类型是IO (),所以它不包含任何有用的结果)。

将值提升到 I/O

最后我们还需要pure

pure :: Applicative f => a -> f a

提升一个值。

将结果提升到 I/O 应用程序中。

代码

factorial n = go n 1
  where
    go n acc =
      print n *>
      if n > 1
        then let !acc' = acc * n
             in  go (n-1) acc'
        else pure acc

acc'forces 绑定中的! 立即发生乘法(感谢Daniel Wagner 指出需要这样做)。

在 GHCi 中测试:

λ> factorial 5 >>= print
5
4
3
2
1
120

【讨论】:

(不要忘记使用$! 或类似的方法将乘法与打印交错。)【参考方案2】:

这里的问题是你试图在一个纯函数 (factorial) 中做一些 IO (print):你不能在 Haskell 中这样做。

一种解决方法是同样使用模块Debug.Trace

import Debug.Trace

factorial n = go n 1
  where
  go n ret | ("n value is: " ++ show n) `trace` n > 1 = go (n-1) (ret * n)
           | otherwise = ret

你会得到:

*Main Debug.Trace> factorial 5
n value is: 5
n value is: 4
n value is: 3
n value is: 2
n value is: 1
120

尽管如库中所述,但有一些注意事项:

trace 函数只能用于调试,或 监控执行。该函数不是引用透明的: 它的类型表明它是一个纯函数但它有边 输出跟踪消息的效果。

正如 Daniel 指出的,如果我们想在评估的每个步骤中强制执行乘法,我们必须将其重写如下:

factorial n = go n 1
  where
  go n ret | ("n value is: " ++ show n) `trace` n > 1 = go (n-1) $! (ret * n)
           | otherwise = ret

【讨论】:

我在回答中提到的关于懒惰的警告当然也适用于这里:在所有打印完成之前,不会进行任何乘法运算,这可能不是所需的行为.可以通过在go (n-1)(ret * n) 之间插入$! 以与我的答案相同的方式修复它。【参考方案3】:

您可以使用(>>)IO 操作进行排序,并使用return 将纯计算转换为IO 操作。所以:

factorial n = go n 1 where
    go n ret | n > 1 = print n >> go (n-1) (ret*n)
             | otherwise = return ret

当然,factorial 现在是一个产生IO 动作的函数;它的调用者可能需要修复以适应这种情况。

另外,请注意:由于懒惰,直到 所有打印之后才完成实际的乘法运算!这可能不是你想要的。对于您将在这样的微基准上进行的测试类型并不重要,但是对于每次迭代执行更多计算的较大函数,您可能会开始注意到。您可以使用$! 或类似方法解决此问题,以使IO 操作的计算取决于此迭代工作的计算。所以:

factorial n = go n 1 where
    go n ret | n > 1 = print n >> (go (n-1) $! ret*n)
             | otherwise = return ret

【讨论】:

以上是关于如何在 Haskell 递归调用中打印迭代?的主要内容,如果未能解决你的问题,请参考以下文章

如何在二叉树中返回迭代遍历的迭代器?

迭代的是人,递归的是神。——L. Peter Deutsch

递归函数如何迭代?

单向链表反转算法——递归版和迭代版

如何以递归方式思考?

如何以递归方式思考?