On Mar 4, 2020, at 19:12, Richard Damon <Richard@damon-family.org> wrote:
Yes, because of the NaN issue, you sort of need an 'Almost Total Order' and 'Really Truly a Total Order', the first allowing the small exception of a very limited (maybe only one) special value that breaks the true definition of Total Order so that we could call Floats to be an Almost Total Order.
The obvious answer is to have PartialOrder < NaNOrder < TotalOrder, where a type is NaN-ordered if it’s partially ordered, and its subset of all self-equal objects is totally ordered. I can’t imagine when a generic AlmostTotalOrder that didn’t specify how it fails could be useful; a NaNOrder is useful all over the place. A median that ignores NaN values requires NaNOrdered input. Even a median that does something stupid with NaN—see below. Of course there might be other kinds of almost total orders that might be useful. If anyone runs into one, they can write their own ABC with the proper relationships to the stdlib ones. Or even propose it for the stdlib. But they couldn’t do anything useful with a general AlmostTotalOrder if it had to handle both NaN and whatever their different case was.
This wouldn't help the median issue as having median reject being given Float values because they don't form a true Total Order would be much worse than the issue of it getting confused with NaNs.
Well, if you want to handle floats and Decimals and do something stupid with NaN values so the user has to be careful, it seems to me the right thing to do is require NaNOrder but document that you do something stupid with NaN values so the user has to be careful.
The presence of NaN in the float system does add a significant complexity to dealing with floating point numbers. Sometimes I wonder if since Python supports dynamic typing of results, might not do better by removing the NaN value from Floats and Decimals, and make the operations that generate the NaN generate an object of a special NaN type.
The thing is, in most cases you’d be duck typing and get no benefit, because float and floatnan both have the same methods, etc. Sure, when you’re type-checking with isinstance you could choose whether to check float|floatnan or just float, but how often do you write such type checks? And if you really want that, you could write a Float ABC that does the same thing, although IIRC ABCs only have a subclasshook rather than an instancehook so you need a new metaclass: class FloatMeta(ABCMeta): def __instancecheck__(self, instance): return isinstance(instance, float) and not math.isnan(instance) class Float(metaclass=FloatMeta): pass People have experimented a lot with similar things and beyond in static type systems: you can define a new type which is just the non-NaN floats, or just the finite floats, or just the floats from -1 to 1, or just the three floats -1 or 0 or 1, and the compiler can check that you’re always using it safely. (If you call a function that takes one of those non-NaN floats with a float, unless the compiler can see an if not isnan(x) or a pattern match that excludes NaN or something else that guarantees correctness, it’s a TypeError.) Fitting this into the type algebra (& and | and -) is pretty easy. I’m not aware of anyone translating that idea to dynamic type systems, but it could be interesting.