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

Comparing lazy values #7021

Closed
vicuna opened this issue Oct 16, 2015 · 9 comments
Closed

Comparing lazy values #7021

vicuna opened this issue Oct 16, 2015 · 9 comments
Assignees

Comments

@vicuna
Copy link

vicuna commented Oct 16, 2015

Original bug ID: 7021
Reporter: bouaziz
Assigned to: @garrigue
Status: assigned (set by bouaziz on 2015-10-16T02:52:34Z)
Resolution: open
Priority: normal
Severity: minor
Platform: x64
OS: Ubuntu
OS Version: 14.04
Version: 4.02.3
Category: standard library
Monitored by: @gasche

Bug description

Comparing lazy values either

  1. fails with exception: Invalid_argument("equal: functional value")
  2. or returns an incorrect result.

I changed a little type in a big data type to be lazy and I got the exception, which seems reasonable knowing how lazy values are represented.
Trying to reproduce on a small example, I didn't get the exception but just wrong results.

Since I am very lazy and I don't want to add compare functions everywhere in my Pervasives.compare-safe data structure, I would love lazy values to be forced when compared.

Steps to reproduce

For (2):
compare (lazy (1+1)) (lazy 2)
returns 1

@vicuna
Copy link
Author

vicuna commented Oct 16, 2015

Comment author: @garrigue

Well, I'm afraid Pervasives.compare makes assumptions about values not moving around while it's comparing them, such that you cannot call ocaml code inside it.
As a result, evaluating lazy values while comparing them would require big changes, making comparison slower for everybody...

What about just defining a normalizing function, that forces all lazy values in your data structure? Then you could just use the standard comparison function after normalizing.

@vicuna
Copy link
Author

vicuna commented Oct 16, 2015

Comment author: bouaziz

That's unfortunate, the point of having lazy values is to avoid forcing them whenever it's not needed.

At least when there is no side-effect, the ideal semantics for lazy values would be to behave like non-lazy values, i.e., shouldn't depend on them having been forced or not.

Fortunately forced lazy values behave as non-lazy values (except for lazy float).
Unfortunately unforced lazy values are not well handled by magic functions:

  • comparison: two non-forced values throw an exception; if only one is forced, a forced integer is always smaller than a non-forced value, tags are compared for non-integer values

  • hashing: non-forced values are hashed with function pointers; two calls to hash with the same lazy value may return two different hashes if the value hash been forced in the meantime

  • marshaling: non-forced values throw an exception

@vicuna
Copy link
Author

vicuna commented Oct 16, 2015

Comment author: @garrigue

I agree that the handling of lazy values in ocaml could be more transparent.
However, I think that you are making one wrong assumption here: as soon as you want to check for equality, you often have to evaluate completely your lazy value anyway.
Well, if the two values differ, it may be sufficient to evaluate until you find a difference.
But if they are identical, you have to fully evaluate.
So the normalize-and-compare approach is not unreasonable.

Damien, do you have something to add here?

@vicuna
Copy link
Author

vicuna commented Oct 16, 2015

Comment author: @alainfrisch

Making lazy more transparent is dangerous in OCaml, since forcing can cause arbitrary side effects, including exceptions and non termination. It's a rather nice property that forcing is always marked by some local-enough syntax (Lazy.force, lazy pattern).

@vicuna
Copy link
Author

vicuna commented Oct 16, 2015

Comment author: lavi

The solution is parhaps to add a Lazy.compare function, preventing all user to be slowed with Pervasive.compare.

@vicuna
Copy link
Author

vicuna commented Oct 16, 2015

Comment author: bouaziz

Yes, worst cases are what they are :)
Note that I am not comparing lazy values directly, they are just at the leaves of a bigger data structure.

Of course, there can be side-effects, and I understand it can surprising if an exception was thrown from Pervasives.compare; but... it's already the case with non-forced lazy values (I personally think lazy values should have no side-effects).

If it had to slow down every compare, it's not worth it, but would there be a cheap way to enter the "slow" mode only when lazy values are encountered? (worst case could be: found Lazy_tag -> start over with the lazy-safe version)

Whatever your choice is, it's probably worth adding documentation on lazy with compare/marshal/hash.

@vicuna
Copy link
Author

vicuna commented Oct 19, 2015

Comment author: @damiendoligez

I agree it would be nice to have hashing/comparison/marshaling force the lazy values as needed, but it would be a large amount of difficult work to implement it correctly.

In the meantime, it would be good to have the generic functions (especially hashing) throw an exception when they encounter an unforced lazy value.

And of course this problem should be mentioned in the documentation of the generic functions.

@vicuna
Copy link
Author

vicuna commented Mar 1, 2017

Comment author: @yallop

I think the correct behaviour is to fail when comparing two unforced lazy values. Having comparison perform forcing doesn't give the right notion of equality. For example, these two lazy values shouldn't be considered equal, just because they both return unit

lazy (print_endline "foo")
lazy (print_endline "bar")

any more than these two functions should be considered equal:

fun () -> print_endline "foo"
fun () -> print_endline "bar"

Treating lazy values as run-once computations fits better with how they actually behave in OCaml than treating them as values that have not yet been reduced.

@github-actions
Copy link

This issue has been open one year with no activity. Consequently, it is being marked with the "stale" label. What this means is that the issue will be automatically closed in 30 days unless more comments are added or the "stale" label is removed. Comments that provide new information on the issue are especially welcome: is it still reproducible? did it appear in other contexts? how critical is it? etc.

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

No branches or pull requests

2 participants