Haskell Data.Decimal 四舍五入的问题?

Haskell Data.Decimal for rounding issue?

我想创建一个列表,其中包含 1.0 到 2.0 之间的所有实数,以两位小数为增量。这个

dList = [1.00,1.01..2.00]

然而,创建浮动 运行-on 问题

dList = [1.0,1.01,1.02,1.03,1.04,1.05,1.06,1.07,1.08,1.09,1.1,1.11,1.12,
1.1300000000000001,1.1400000000000001,1.1500000000000001, ...

为了解决这个问题,我在 Data.Decimal 中发现了我认为是一个函数,即 roundTo。我希望最终 运行 这个

map (roundTo 2) [1.1,1.2..2.0]

并摆脱浮动 运行-on,但它会产生巨大的错误消息。 This page 对于这个初学者来说是难以理解的。因此,我尝试使用在 ghci REPL 加载的 hs 文件来执行此操作。这是代码

import Data.Decimal

dList :: [Decimal]
dList = [1.00,1.01..2.0]

main = print dList

它产生

Could not find module ‘Data.Decimal’

我迷路了....

具体浮动问题的答案

到目前为止,在这种情况下最简单的选择是

[n/100 | n<-[0..200]]

或同一想法的变体:

map (/100) [0..200]
(*1e-2) . fromIntegral <$> [0 .. 200 :: Int] -- (*) is more efficient than (/),
                                             -- but will actually introduce rounding
                                             -- errors (non-accumulating) again

不需要任何特殊的十进制库或有理数。

这种方法比 [x₀, x₁ .. xe] 方法更好的原因是 252 以下的整数可以精确地用浮点数表示(而小数不能)。因此,范围 [0..200] 正是您想要的。然后在最后,将这些数字中的每一个除以 100 仍然不会为您提供 精确 您想要得到的百分之一的表示形式 – 因为这样的表示形式不存在 – 但您 为每个元素得到最接近的近似值。最接近的近似值实际上以 x.yz 形式打印,即使使用标准 print 函数也是如此。相比之下,在 [1.00,1.01..2.0] 中,您不断将已经近似的值相加,从而使误差更加复杂。

可以也使用精确有理数的原始范围计算,然后才将它们转换为浮点数 – 这仍然不行'不需要十进制库

map fromRational [0, 0.01 .. 2]

有理算术通常可以很容易地解决类似的问题——但我倾向于反对这种做法,因为它通常很难扩展。在浮点数中存在舍入问题的算法通常会 运行 成为有理算术中的 内存 问题,因为您需要通过整个计算。更好的解决方案是首先避免需要精度,就像 n/100 建议一样。

此外,可以说 [x₀, x₁ .. xe] 语法无论如何都是一个错误的设计;整数范围具有更清晰的语义。

另请注意,您最初尝试中的浮点数错误根本根本不一定是问题。现实世界测量量中 10-9 的误差对于所有有意义的目的都是可以忽略的。如果您需要真正精确的东西,您可能根本不应该使用小数值 ,而是直接整数。因此,请考虑 Carl 的建议,即只接受浮动偏差,但只需通过 showFFloat, printf, or Text.Show.Pragmatic.print.

以适当的圆形形式 打印 它们

实际上,在这种特定情况下,两种解决方案几乎是等效的,因为将 Rational 转换为浮点数涉及浮点数除以分子分母.

模块加载问题的答案

如果您确实需要 Decimal 库(或其他一些库),则需要 依赖

  • 最简单的方法是使用 Stack 并将 Decimal 添加到您的 全局项目 。然后,您可以使用 stack ghci 加载文件,它会知道在哪里寻找 Data.Decimal.

  • 或者,IMO 最好,您应该自己创建一个 项目包 ,并且只依赖于 Decimal。这可以通过 Stack 或 Cabal-install.

    来完成
    $ mkdir my-decimal-project
    $ cd my-decimal-project
    $ cabal init
    

    现在你被问到一些关于项目名称等的问题,你大部分都可以使用默认值来回答。假设您的项目定义了一个 (如果需要,您可以稍后添加可执行文件。)

    cabal init 创建了一个 my-decimal-project.cabal 文件。在该文件中,将 Decimal 添加到依赖项,并将您自己的源文件添加到 exposed-modules.

    然后您需要(仍在您的项目目录中)cabal install --dependencies-only 获取 Decimal 库,然后 cabal repl 加载您的模块。

备注

这个答案仅供所有初学者参考,只是想按照一本初学者 Haskell 的书学习,这本书让您在文本编辑器中输入代码,启动 ghci REPL 并执行 :load my-haskell-code.hs.

YMMV 解决方案

从上面可以看出,Data.Decimal 不是标准的 Prelude 类包。它必须独立加载——不,简单地将 import Data.Decimal 放在代码的顶部是行不通的。正如leftaroundabout在上面的评论中所说,对于Haskell还没有做项目的初学者来说,最简单的方法就是这样启动ghci

stack ghci --package Decimal

当然是 YMMV,具体取决于您的安装方式 Haskell。我通过堆栈项目管理安装了 Haskell,因此 stackghci --package Decimal 之前。关于我的设置的另一个独特之处是我正在使用 Emacs org-mode 的 Babel 代码块,它与基本类型和加载方式基本相同,即非项目。我确实尝试通过添加 --package Decimal 来改变 Emacs 的 haskell-process-args-stack-ghci ,它位于 haskell-customize.el 中,但它没有用。相反,我只是转到我的 bash 命令行并输入 stack ghci --package Decimal,然后我重新启动了一个单独的 org-mode Babel ghci 并且它工作了。现在,

dList :: [Decimal]
dList = [1.00,1.01..2.00]

> dList
[1,1.01,1.02,1.03,1.04,1.05,1.06,1.07,1.08,1.09,1.10,1.11,1.12,1.13,1.14,1.15,1.16,1.17,1.18,1.19,1.20,1.21,1.22,1.23,1.24,1.25,1.26,1.27,1.28,1.29,1.30,1.31,1.32,1.33,1.34,1.35,1.36,1.37,1.38,1.39,1.40,1.41,1.42,1.43,1.44,1.45,1.46,1.47,1.48,1.49,1.50,1.51,1.52,1.53,1.54,1.55,1.56,1.57,1.58,1.59,1.60,1.61,1.62,1.63,1.64,1.65,1.66,1.67,1.68,1.69,1.70,1.71,1.72,1.73,1.74,1.75,1.76,1.77,1.78,1.79,1.80,1.81,1.82,1.83,1.84,1.85,1.86,1.87,1.88,1.89,1.90,1.91,1.92,1.93,1.94,1.95,1.96,1.97,1.98,1.99,2.00]

没有混乱,没有大惊小怪。我杀死了 ghci 并在没有 --package Decimal 的情况下加载了它,它仍然知道 Decimal,所以这个更改几乎永久地记录在我的 ~/.stack 目录中的某个地方?奇怪的是,bash ghci 会话不知道 org-mode 中的 *haskell* ghci 会话。此外,当仅使用 Emacs haskell 模式独立进行类型和加载时,其 ghci 会话也不能很好地与 org-mode ghci 配合使用。我选择了极简主义的 Emacs org-mode babel,因为它看起来比 lhs 文字 Haskell 更好。如果有人知道如何让文字 Haskell 像组织模式一样唱歌,我很想知道。

事后分析

我想我坚持要弄清楚 Decimal 因为在研究整个舍入问题时,我开始看到建议的解决方案(不一定在这里,但其他地方)和可怕的技术争论的巨大分歧。 Decimal 似乎是竞争舍入策略的狂风暴雨中最简单的一个。四舍五入应该很简单,但在 Haskell 中,它变成了一个耗时的多个兔子洞之旅。