
On Mon, Dec 8, 2008 at 4:39 PM, Robert Kawulak <robert.kawulak@gmail.com> wrote:
From: Stjepan Rajko
The invariant is still: x < y
Are we still talking about the case when test ==> invariant? I'm confused -- if we allow for a value (x) being a "delta" bigger than the upper bound (y), then why the invariant should be "x < y" rather than "x - delta < y"?
OK, if you make your test be "x < y" then you can guarantee "x - delta < y". But the user doesn't want to deal with the deltas - that is the whole issue here. So, you make your test "x + epsilon < y" and you can guarantee "x < y".
If you think about it, you are already separating the test from the invariant in your advanced examples. Think about the object that uses the library to keep track of it's min/max. The test checks for whether you have crossed the previous min/max. Sure, you could say the invariant is the same: "the object is between the min and max present in the constraint object". But really, what kind of guarantee is this? If I need to look at the constraint to figure out what I'm being guaranteed, I might as well look at the value itself and see where it stands. I would consider this as "no invariant". There, you already have docs for this case :-)
I would say the invariant is still there, but it is "inverted" -- in typical bounded objects it is: "the value always lays within the bounds", while here it is "the bounds always contain the value". When the value is going to be modified, the error policy ensures that the invariant is still upheld by modifying the constraint (in contrast to the more common case when it would modify the value). The test here is always equal to the invariant, so it doesn't seem to be a representative example for test ==> invariant concept.
That depends on what you claim the invariant to be. If you claim that the invariant is equivalent to the test, then it is not a representative case. If you claim that the invariant is "nothing", then it is a representative case. My point was that (unless accessing the constraint costs less than accessing the value) there is no additional benefit in saying that the invariant is equivalent to the test compared to simply saying there is no invariant. Granted, there is no additional benefit in the other direction either, but that's kind of my point - there is no benefit from the stated invariant.
If you are sticking with test == invariant just for the sake of test == invariant (rather than a lack of time to investigate and document the other case), I think you are settling to sell your library for way shorter than you can.
Please forgive me my resistance, but I stick with test == invariant because I believe that as a person responsible for a library I have to think 100 times and be really convinced before I add/change anything. I wouldn't have so much doubts if I see that there are useful and general applications that would outweigh the added complexity (I hope you agree that it will be more difficult to explain test ==> invariant approach to the users?) and some extra work needed.
Depends on the user. Fundamentally, all that you would have to communicate in the docs (and assure yourself of) is that "if the test guarantees the invariant, and the policy guarantees the invariant, the library guarantees the invariant". If a user is unclear, they can fall back to your great discussion of the test == invariant case, and stick with that type of use. As far as "if the test guarantees...the library guarantees...", it should be no more complicated to understand that "if X is thread-safe then something<X> is thread-safe", or something similar regarding exception safety.
So far you've shown one application that is dealing with the FP issue using epsilon, but we don't know yet if this approach is leading to (best or any) solution of the problem.Are there any other use cases that I should consider?
Hmm.. I think I mentioned more examples than just the FP case (e.g, monitored values with no invariant - your library doesn't have to call it a 'monitored_value' for me to use it as such, but if you require test == invariant I won't because you can break my code, e.g. with an assert whenever you'd like and I can't complain).
Maybe it's best to leave it as is for now, and when you test whether the approach is really sound and useful, we could make the necessary changes (before the first official release)?
I've already voted to accept the library, so I believe that the library is a valuable addition as it stands (modulo conditions, and as far as FP goes, I said I find the exact(value) solution acceptable). As far as what is best, I don't know.
And another issue is NaN -- it breaks the strict weak ordering, so it may or may not be allowed as a valid value depening on the direction of comparison ("<" or ">"). I guess NaN should not be an allowed value in any case, but I have no idea yet how to enforce this without float-specific implementation of within_bounds.
I haven't taken a close look at bounded values, I'm just thinking of them as a specific case of constrained values. What is your invariant here? That ((min <= value) && (value <= max)) or that !((value < min) || (max < value))? Why do you need a strict weak ordering for either one? I believe NaN will fail the first test but pass the second one - if that is true, why is NaN a problem if you use the first test? (sorry if I'm missing something, like I said I'm not well versed in the details of floats)
The problem with NaN is that any comparison with this value yields false. So:
NaN < x == false NaN > x == false NaN <= x == false ... and so on.
This violates the rules of strict weak ordering, which guarantee that we can perform tests for bounds inclusion without surprises. For example, when x == NaN, the following "obvious" statement may be false:
(l < x && x < u) ==> (l < u)
Why would I care that l < u? If I'm using a bounded type with lower bound l and upper bound u, presumably I just care that (l < x && x < u).
Maybe the requirement could be loosened if I find a generic way to implement the bounds inclusion test which always returns false for NaN. Currently, to test x for inclusion in a closed range [lower, upper], we have:
!(x < lower) && !(upper < x)
But as the NaN case illustrates, !(x < lower) && !(upper < x) is not the same as (lower <= x) && (x <= upper). If I'm using a bounded type, I would want the latter. In non-numerical settings, and with custom comparisons, the two might have nothing to do with each other. I think bounded_value should *always* use the test (and invariant) compare(lower, x) && compare(x, upper). If you want boundaries excluded, use < for compare. If you want them included, use <=. If the type doesn't offer <=, use "<(a, b) || ==(a, b)".
While for an open range (lower, upper):
(lower < x) && (x < upper)
Now, if we try to check if NaN is within the closed range, we get true, while for the open range we get false. Therefore NaN belongs to the subset, but does not belong to the superset, which is obviously a contradiction. I'm not sure if such properties of NaN could lead to broken invariant, but surely it would be good to avoid the strange results.
Agreed, but I think using the test/constraint as I suggest above would avoid strange results in more cases (using two completely different types of tests also strikes me as a source of unexpected behavior). And you don't have to worry about NaNs. Unless I'm missing something. Stjepan