如何在 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 递归调用中打印迭代?的主要内容,如果未能解决你的问题,请参考以下文章