Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shouldBeNear expectation #23

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

Add shouldBeNear expectation #23

wants to merge 3 commits into from

Conversation

erikd
Copy link

@erikd erikd commented Aug 17, 2016

Comparing floating point numbers for equalty is a bad idea. Instead
they should tested to see if they are close enough to one another.

@erikd
Copy link
Author

erikd commented Sep 3, 2016

Ping?

@sol
Copy link
Member

sol commented Sep 3, 2016

Hey, sorry for the delay. For this to be useful this would at least need tests and more detailed documentation. Specifically I don't understand

  • why is the first case necessary
  • what happens on division by zero, e.g. when comparing 1e-20 and -1e-20

@erikd
Copy link
Author

erikd commented Sep 3, 2016

For this to be useful this would at least need tests

There's no tests listed in the cabal file. I've found the tests in the repo and I'm adding tests, but how do I run them?

why is the first case necessary

This is short circuit evaluation for the case where they are actually equal.

what happens on division by zero, e.g. when comparing 1e-20 and -1e-20

The code for the third case says (abs actual + abs expected) which could only be zero if both values are zero and that would be caught be the second case.

@sol
Copy link
Member

sol commented Sep 3, 2016

I use ghci or sensei to run tests

Sent from mobile

On 3 Sep 2016, at 4:53 PM, Erik de Castro Lopo notifications@github.com wrote:

For this to be useful this would at least need tests

There's not tests listed on the cabal file. How do I run them?

why is the first case necessary

This is short circuit evaluation for the case where they are actually equal.

what happens on division by zero, e.g. when comparing 1e-20 and -1e-20

The code for the third case says (abs actual + abs expected) which could only be zero if both values are zero and that would be caught be the second case.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or mute the thread.

@erikd erikd mentioned this pull request Sep 4, 2016
@erikd erikd force-pushed the master branch 2 times, most recently from c5210c1 to f8582f5 Compare September 4, 2016 07:10
@sol
Copy link
Member

sol commented Sep 6, 2016

Tests are running on Travis now. Can you rebase on current master and make sure the build is green.

I also wouldn't mind if you remove some of the code duplication, e.g. by factoring common stuff out into a where-clause.

Then I would love to discuss the actual stuff, that is the formula and the chosen value for epsilon.

@erikd erikd force-pushed the master branch 2 times, most recently from dec8922 to 8e4cc5a Compare September 6, 2016 08:28
@erikd
Copy link
Author

erikd commented Sep 6, 2016

Ok, the build has passed.

Since the current code uses guards, its not possible to refactor common code into a where clause, but I could make it a separate top level function (and then not export it).

@sol
Copy link
Member

sol commented Sep 6, 2016

I think you can use a where clause with guards, no?

Sent from mobile

On 6 Sep 2016, at 5:03 PM, Erik de Castro Lopo notifications@github.com wrote:

Ok, the build has passed.

Since the current code uses guards, its not possible to refactor common code into a where clause, but I could make it a separate top level function (and then not export it).


You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or mute the thread.

@erikd
Copy link
Author

erikd commented Sep 6, 2016

It works! Fixed, rebased and pushed.

@erikd erikd force-pushed the master branch 3 times, most recently from e43ce35 to 69bfa94 Compare September 6, 2016 10:09
@erikd
Copy link
Author

erikd commented Sep 6, 2016

For some reason I couldn't get one the tests working with ghc 7.10 even though it passed on all the other versions.

@@ -25,6 +25,8 @@ library
hs-source-dirs:
src
ghc-options: -Wall
if impl(ghc >= 8.0)
ghc-options: -fno-warn-redundant-constraints
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this on master, so this is no longer necessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, dropped that patch.

@erikd erikd force-pushed the master branch 2 times, most recently from 263f489 to 4c7b60b Compare September 7, 2016 04:25
| actual == expected = reportFail (actual == expected)
-- If either input is zero, we check if the absolute difference is less than epsilon..
| actual == 0 || expected == 0 = reportFail (abs (actual - expected) < 1e-15)
-- In all other cases we check that the relative difference is less than epsilon.
Copy link
Member

@sol sol Sep 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source code comments are an anti-pattern, rather than commenting make your code more expressive, e.g.

otherwise = reportFail (relativeDifference < epsilon)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that is neater.

| otherwise = reportFail (relativeDifference < epsilon)
where
epsilon = 1e-15
absDifference = abs (actual - expected)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for me it's much easier to work with code that does not use abbreviations, the reason for this is that I have to use speech recognition for programming due to RSI. for that reason I would much prefer absoluteDifference here

-- Short circuit if they are actually equal.
| actual == expected = reportFail (actual == expected)
| actual == 0 || expected == 0 = reportFail (absoluteDifference < epsilon)
| otherwise = reportFail (relativeDifference < epsilon)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now finally regarding the interesting stuff, the formula, looking at the following example

x `shouldBeNear` 1e-300

I haven't tried, but what happens if x is

  1. 0.9e-300
  2. 0

would (1) fail and (2) pass?

should we always normalize against the expected value instead?

actual `shouldBeNear` expected
  | delta < epsilon = return ()
  | otherwise = actual `shouldBe` expected
  where
    epsilon = 1e-15
    absoluteDifference = abs (expected - actual)
    relativeDifference = absoluteDifference / abs expected
    delta
      | expected == 0 = absoluteDifference
      | otherwise = relativeDifference

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tried, but what happens if x is

  1. 0.9e-300
  2. 0

would (1) fail and (2) pass?

That is what currently happens, and I agree, at first sight, doesn't make much sense, but this is the usual recommended way to compare floating point numbers.

However, now that I think about it,

1e-300 `shouldBeNear` 0.0

should pass (because actual is close to expected), but I think maybe

0.0 `shouldBeNear` 1e-300

should fail.

Let me think about this some more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have encoded my expectations in the tests.

Comparing floating point numbers for equalty is a bad idea. Instead
they should tested to see if they are close enough to one another.
@erikd
Copy link
Author

erikd commented Sep 15, 2016

@sol Does this seem ok?

absoluteDifference = abs (expected - actual)
relativeDifference = absoluteDifference / (abs actual + abs expected)
reportFail =
expectTrue ("expected: " ++ show expected ++ "\n but got: " ++ show actual)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on this change BTW!

Wouldn't it be nicer to report things like expected 123 ± 1% but got 125 or similar, as the expectation is of it being within a tolerance of the value not being the value?

Base automatically changed from master to main January 21, 2021 03:48
@istathar
Copy link

@erikd is this still a valid approach or have you found a different way to tackle comparing Doubles?

@erikd
Copy link
Author

erikd commented Mar 16, 2022

@istathar I stopped using hspec about 6 years ago, in favor of hedgehog. However, I suspect this may still be a valid approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants