ekmett / intervals

Interval Arithmetic
http://hackage.haskell.org/package/intervals
BSD 2-Clause "Simplified" License
27 stars 13 forks source link

"Safe" constructors #11

Closed dmcclean closed 10 years ago

dmcclean commented 10 years ago

Am I correctly understanding the comment on (...) (excerpted below) to mean that the difference between I and (...) is that the latter is lazier? Does inlining change that?

-- | The rule of thumb is you should only use this to construct using values
-- that you took out of the interval. Otherwise, use I, to force rounding
(...) :: a -> a -> Interval a
(...) = I
{-# INLINE (...) #-}

Both constructors allow constructing lots of syntactically distinct but semantically equivalent empty intervals when the first field is greater than the second field.

How do you feel about creating two new construction functions like the following. (I am having trouble coming up with the names.)

createInterval :: Ord a => a -> a -> Maybe (Interval a)
createInterval a b | a <= b = Just $ I a b
                   | otherwise = Nothing

createIntervalUnordered :: Ord a=> a -> a -> Interval a
createIntervalUnordered a b | a <= b    = I a b
                            | otherwise = I b a

These come up in my code because I am converting from another language where I had a library that dynamically ensured that there was only one representation of the empty interval, and threw exceptions if you gave values the wrong way around. The unordered flavor is used for user interfaces where you want to guess what they most likely "really meant".

ekmett commented 10 years ago

(...) is just syntactic sugar for I really, no laziness benefits to speak of.

I don't have a problem with adding a non-empty interval constructions, though I tend to favor shorter names. =)

ekmett commented 10 years ago

I adopted the practice that in Numeric.Interval.NonEmpty the (...) combinator follows createIntervalUnordered here.

Close enough?

dmcclean commented 10 years ago

Hmmm. Tough call. I am going to argue the side that it isn't close enough, but I might be persuaded.

If you get the numbers from a human, the createIntervalUnordered semantics are "friendly". If you get the numbers from a calculation, silently accepting unordered ones potentially masks problems that it would be better to bring to the surface.

I am not totally sure how defensively the rest of your libraries are written, so I don't want to push you to a style that is discordant or not haskell-y.

If it was just me and I was writing from scratch, I would not export I, I would make (...) use error, and I would make both of these flavors. In my C# interval class I routinely use all three flavors (exception, Optional<Interval>, and the one that reorders them).

Maybe name createIntervalUnordered as (~) or (.~.) or (..~) or (~~~)? And createInterval as (.?.) or (..?)?

dmcclean commented 10 years ago

I should add that if we can only have one I want the one that errors. You can build the others around it, and it also (along with hiding I) lets the consumer of an Interval a safely assume that it satisfies the invariant.

ekmett commented 10 years ago

(~) isn't legal syntax. For NonEmpty I think (...) having the unordered semantics is the right call. It means you are always defining a legal interval.

For the other two, having (...) yield a possible Empty or a negative interval means you can null check them and aren't forced into Maybe everywhere.

let's go with interval :: Ord a => a -> a -> Maybe (Interval a) for an explicit constructor with the ordered semantics. It can be sensibly defined for all 3.

Kaucher can expose the I constructor, its Functor, Monad, etc. and Empty without guilt. That is one reason why that was the form of interval arithmetic I was using all along.

The other two have to sacrifice a lot of instances to get any semblance of protection, but I do have a policy of at least exposing the guts through an Internal module if I hide it in the main public API, to avoid power-users having to go off to use another library when they have a need I don't anticipate.

That would lead to

Numeric.Interval
Numeric.Interval.Kaucher
Numeric.Interval.NonEmpty
Numeric.Interval.Internal
Numeric.Interval.NonEmpty.Internal

Messy, but I could live with it.

dmcclean commented 10 years ago

Independent of whether to hide I, I'd actually like to remove the unsafe instances from Numeric.Interval and Numeric.Interval.NonEmpty. As you say, the Kaucher variant is fine, but the others really aren't.

The Functor instance only works if the function is monotonically non-decreasing. (You could write a flavor that worked for monotonic functions in either direction, I suppose.)

The Foldable instance is fine because it only has Intervals in negative position. (I can't think what you would use it for, but I have a feeling that says more about my creativity today than it does about the definition.)

The Applicative (and hence Monad) instance is strange because it requires you at some point to have a value of the type Interval (a -> b), which it treats as an ordered pair but has to put under the I constructor instead of the (,) constructor to match the shape of the class. In my opinion this one is questionable even for Kaucher intervals, although I may be missing something. What does it mean to have an Interval (Double -> Double)? Perhaps there are compelling use cases that justify it though.

I don't understand the Distributive instance. The Distributive class is like a magic eye poster for me, I only sometimes understand it.

ekmett commented 10 years ago

I'm agree with removing those instances from those types. They were only sound before under the Kaucher semantics.

ekmett commented 10 years ago

Done. I've removed the instances, split out Internal modules and shipped out a version 0.5 since someone wanted to use it.

I'm happy to keep going on adding the interval smart constructors though. I just forgot about it before shipping 0.5

ekmett commented 10 years ago

0.5.1 has interval with the createInterval semantics above.