Boost logo

Boost :

Subject: Re: [boost] [review][constrained_value] Review ofConstrainedValueLibrary begins today
From: Stjepan Rajko (stjepan.rajko_at_[hidden])
Date: 2008-12-08 20:22:15


On Mon, Dec 8, 2008 at 4:39 PM, Robert Kawulak <robert.kawulak_at_[hidden]> 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


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk