147pm
147pm

Reputation: 2239

Haskell Data.Decimal for rounding issue?

I would like to create a list of all the real numbers between 1.0 and 2.0 in two-decimal-place increments. This

dList = [1.00,1.01..2.00]

however, creates float run-on problems

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, ...

To remedy this I found what I thought was a function in Data.Decimal namely, roundTo. I was hoping to eventually run this

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

and get rid of the float run-on, but it produces a giant error message. This page is for this beginner undecipherable. And so I'm trying to do this with an hs file loaded at a ghci REPL. This is the code

import Data.Decimal

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

main = print dList

and it produces

Could not find module ‘Data.Decimal’

Lost, I am....

Upvotes: 1

Views: 494

Answers (2)

147pm
147pm

Reputation: 2239

Note

This answer is an FYI for all of you beginners simply trying to follow along in a beginner Haskell book that has you typing code into a text editor, firing up the ghci REPL and doing :load my-haskell-code.hs.

YMMV Solution

As can be gleaned above, Data.Decimal is not a standard included Prelude sort of package. It must be loaded independently -- and no, it doesn't work to simply put import Data.Decimal at the top of your code. As leftaroundabout said in a comment above, the very simplest way for Haskell beginners not yet doing projects is to start ghci thusly

stack ghci --package Decimal

Of course YMMV depending on how you installed Haskell. I installed Haskell through the stack project management, hence, the stack before ghci --package Decimal. Another unique thing about my setup is I'm using Emacs org-mode's Babel code blocks, which is largely the same as the basic type-and-load way, i.e., non-project. I did try to alter Emacs's haskell-process-args-stack-ghci which is in haskell-customize.el by just adding --package Decimal but it didn't work. Instead, I simply went to my bash command line and put in stack ghci --package Decimal, then I restarted a separate org-mode Babel ghci and it worked. Now,

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]

no muss, no fuss. I killed the ghci and loaded it without the --package Decimal and it still knew about Decimal, so this change is recorded quasi-permanently in my ~/.stack directory somewhere? Oddly, the bash ghci session doesn't know about the *haskell* ghci session in org-mode. Also, when just using the Emacs haskell mode stand-alone for type-and-load, its ghci session also doesn't play well with the org-mode ghci. I've gone with the minimalist Emacs org-mode babel because it seems better than just lhs literal Haskell. If anyone knows how to make literal Haskell sing like org-mode, I'd like to know.

Postmortem

I guess I insisted on figuring out Decimal because in researching the whole rounding issue I started seeing a wild divergence of suggested solutions (not necessarily here, but other places) and scary technical arguments. Decimal seemed the simplest in a wild storm of competing rounding strategies. Rounding should be simple, but in Haskell it turned into a time-sucking trip down multiple rabbit holes.

Upvotes: 3

leftaroundabout
leftaroundabout

Reputation: 120711

Answer to your specific float problem

By far the simplest option in this case is

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

or variations on the same idea:

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

No need for any special decimal library or rational numbers.

The reason this works better than the [x₀, x₁ .. xe] approach is that integers below 252 can be expressed exactly in floating point (whereas decimal fractions cannot). Therefore, the range [0..200] is exactly what you want it to be. Then at the end, dividing each of these numbers by 100 will still not give you exact representations of the hundredths you want to get – because such representations don't exist – but you will for each element get the closest possible approximation. And that closest possible approximation is in fact printed in x.yz form, even with the standard print function. By comparison, in [1.00,1.01..2.0] you keep adding up already-approximate values, thereby compounding the errors.

You could also use your original range calculation in exact rationals, and only then convert them to float – this still doesn't require a decimal library

map fromRational [0, 0.01 .. 2]

Rational arithmetic is often an easy fix for similar problems – but I tend to advise against this, because it usually scales badly. Algorithms that have rounding problems in floating-point will more often than not run into memory problems in rational arithmetic, because you need to carry an ever-growing range of precision through the entire calculation. The better solution is to avoid needing the precision in the first place, like with the n/100 suggestion.

Also, arguably the [x₀, x₁ .. xe] syntax is a broken design anyway; integer ranges have far clearer semantics.

Note also that the float errors in your original attempt aren't necessarily a problem at all. An error of 10-9 in a real-world measured quantity is for all meaningful purposes neglectable. If you need something to be truely exact, you probably shouldn't be using fractional values at all, but straight integers. So, conside Carl's suggestion of just living with the float-deviations, but simply printing them in a suitably rounded form, by way of showFFloat, printf, or Text.Show.Pragmatic.print.


Actually, in this specific case both solutions are almost equivalent, because converting Rational to float involves floating-point dividing the numerator by the denominator.

Answer to your module-loading problem

In cases where you do need the Decimal library (or some other library), you need to depend on it.

  • The easiest way to do this is to use Stack and add Decimal to your global project. Then, you can load the file with stack ghci and it'll know where to look for Data.Decimal.

  • Alternatively, and IMO preferrably, you should create a project package yourself and only make that depend on Decimal. This can be done with either Stack or Cabal-install.

    $ mkdir my-decimal-project
    $ cd my-decimal-project
    $ cabal init
    

    Now you're being asked some questions about the name etc. of your project, you can mostly answer with the defaults. Say your project defines a library (you can add an executable later on if you want.)

    cabal init creates amongst other things a my-decimal-project.cabal file. In that file, add Decimal to the dependency, and your own source file to exposed-modules.

    Then you need to (still in your project directory) cabal install --dependencies-only to fetch the Decimal library, and then cabal repl to load your module.

Upvotes: 2

Related Questions