Hacker News new | past | comments | ask | show | jobs | submit login
The Numeric Tower Fiasco (mmapped.blog)
17 points by tie-in 4 months ago | hide | past | favorite | 17 comments



Numeric towers are indeed a sore point.

* There are more 64-bit integers than C floats/doubles. But in math, reals are a superset of the integers.

* Because floats are rounded, `x < x + 1`, is not an invariant.

* The possibility of a `NaN` value means that the assignment `y = x` does not guarantee that x and y are equal.

* Floats are implemented as binary fractions, so they are actually rationals.

I disagree with the OP that OOP is entirely flawed. The core ideas of encapsulation and messaging are a really useful organizing principle. And polymorphism beats maintaining giant case-statements.

Only when inheritance is added to the mix does it get dicey. As a tool for code reuse, it is not a bad idea. I think the central problem is that people want more from inheritance that it has to give (especially if you expect that children are always substitutable for their parents).


> I disagree with the OP that OOP is entirely flawed. The core ideas of encapsulation and messaging are a really useful organizing principle. And polymorphism beats maintaining giant case-statements.

Does OOP actually do encapsulation, messaging or polymorphism better than other languages though?


As described by Alan Kay, what OOP means is encapsulation, messaging, and late-binding.

IIRC the inspiration came from multicellular organisms. A cell "encapsulates" complexity within a cell wall. The organism as a whole works by have the cells work together via "messaging". Any shared interfaces (e.g. oxygen transpiration and nutrient absorption) can be viewed as "polymorphism".

If you buy into the definition and biological analogy, then any language that implements "encapsulation, messaging or polymorphism" actually is OOP and your question is a tautology.


> The object-oriented tradition obsesses with encoding is a relations among types as a class hierarchy.

That's a fallacy. Complicated type hierarchies are a mistake in object oriented programming. There are a lot of good reasons to use inheritance, but it's not really something that should be obsessed about.

A large "object oriented" program will still declare the large majority of its types without inheritance. (Or with minimal inheritance, SomethingService inherits from ServiceBase, SomethingRecord inherits from RecordBase.)

> When I use a tool and get unsatisfactory results, I blame myself for not using the tool correctly.

> The approach of piling classes on top of one another and trying to make this stack coherent is fundamentally flawed. It fails spectacularly even on tiny examples where the problem domain is mathematically specified.

Yes, you aren't using the tool correctly. You can not interchange rational and natural numbers in a computer without conversion. Typically rational and natural "classes" wouldn't share a common base class, but might share common-ish interfaces.

For example, in C# I could declare a generic INumber interface, and all my number types could support it, but by the nature of being generic, you can't interchange a rational and natural without performing some kind of conversion. (INumber<TNumber> where TNumber : INumber) This would allow a programmer to write generic code that handles an INumber, but not a mix of different INumber types. (SomeMethod<TNumber>(TNumber arg) where TNumber : INumber<TNumber>) (I believe newer C# has something like this baked-in, but I haven't used it yet.)


OOP is a tool like any other and isn't always suitable. A similar example from geometry: all squares are rectangles, but you wouldn't implement a square type by deriving from rectangle because it breaks LSP.

> Adding a new operation on numbers requires modifying all the classes.

This is part of the expression problem. With an OOP approach, adding a new operation is a "heavy" task. With a functional approach, adding a new type is a heavy task.

In the case of the numerical tower, the set of types is more or less locked down.


> OOP is a tool like any other and isn't always suitable. A similar example from geometry: all squares are rectangles, but you wouldn't implement a square type by deriving from rectangle because it breaks LSP.

Deriving a class where each instance models a geometric square from one that models a rectangle would not violate LSP.

Deriving a class where each instance models an entity which is at any point in time a geometric square but can become a different square while retaining its identity from one that models an entity with the same relationship to geometric rectangles, in a language where mutable objects must be of a fixed class, will violate the LSP.

People often discuss OOP but fail to differentiate those two very different cases. (And more generally, pay too little attention to the impact of mutability on is-a relationhlships.)


When approaching object orientation, I usually take a low level approach.

A class is a data structure.

An object is the actual data.

A method is a function talking an object as the first parameter

A subclass is a data structure containing first the parent class data structure, then the elements specific to the subclass. It means that inheritance is a special case of composition.

Virtual methods are function pointers in an object (often with a level of indirection: the vtable).

So instead of thinking of inheritance as a "is a" relationship, I treat is as "has (the properties of) a", because that's how it is in memory. For example, we often do stuff like an employee is a person, so employee inherits from person, and adds some extra properties, like pay. But I prefer to think of it as "an employee has the properties of a person". But we also decide that the "person" properties are special, because these are the properties you will use often, so you use inheritance instead of composition, and doing that, you take advantage of some syntactic sugar and an efficient memory representation.

So for that number tower, do you want "integer" to inherit "natural". Probably not, because while an integer has the properties of a natural with an additional "sign" property, you are unlikely to want to address your integer as a natural, there are few cases where it makes sense, so explicit composition would be more appropriate. In the reverse direction, do you want "natural" to inherit "integer". Maybe, it depends on how "special" natural numbers are over integers.

OOP may be flawed from a mathematical perspective, but it is a good tool for practical programming, that's why it is so popular. The "is a" and "has a" relationships are just to help understand a concept that is really about chunks of memory.


Funny, because inheritance was the least important aspect of OOP when I did it (in part, because it was the one most abused)


I also distinctly remember even in my intro-to-OOP programming book, it was explicitly called out that mathematical-isa relationships (the square/rectangle/parallelogram case was the one they used in that example) don't qualify as is-a relationships for the purposes of OOP!


FWIW I think that's the essence of the complaint here: "is a" can mean various things in various contexts, but Liskov substitution principle and OOP design generally requires it to mean something specific, and using "OOP is a" when you meant another "is a" is an easy trap to fall into, because the language makes it so easy to do.

But I don't see why this is a complaint about OOP in general. It's a complaint specifically about inheritance.

That said, the claim that integers "are" naturals is mathematically 100% false as well, so the example in the article is also kind of a silly straw man.


> That said, the claim that integers "are" naturals is mathematically 100% false as well, so the example in the article is also kind of a silly straw man.

That's what the article said though.


I agree, I do not see a natural object relationship between the types of numbers.

'Natural' (unsigned int) numbers might be a building block of signed integers, fractional pairs, and composed floating point types; but they seem like three distinct use cases, with perhaps a way of converting a whole fraction or positive signed number into another type either if it fits, or with an accepted loss of information.

When OOP was covered in classes years ago the typical example was something like...

Class Animal

Class Pet extends Animal

Class Dog extends Pet

Class Cat extends Pet

etc

Edit: These days I prefer golang's example of Interfaces which specify what methods and members must exist for something to be 'used as an X'; very useful IOReader IOWriter and https://pkg.go.dev/io#ReadWriter which naturally is an Interface containing only a Reader and Writer


Yeah,Pet extends Animal is just sad...


For an RPN calculator I discovered similar problems with a number hierarchy as well. The calculator has four number types:

- integers (for hexadecimal operations),

- quotients (like some HP calculators),

- IEEE 756 floating point numbers (because that is the most simple thing on computers) and

- complex numbers based on two IEEE 756 components

Mathematically these types are subsets of each other:

    ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ
But number values in a computer aren't subsets. Not all 64 bit integers are representable as 64 bit IEEE 756 numbers, for example odd integers beyond 10^53.

Hand-held calculators need to hide a lot of complexity to be intuitive.

One example. If you have two values in the calculator that approximate e and i * pi as good as the calculator can and request the power operation, then you have the Euler identity and should get the integer 1. The implementation uses complex numbers and because of rounding errors you might get something like 1.0000000003. To be user-friendly, the calculator needs to hide that.

That's what I did:

- Use four distinct types without any object-oriented features

- Operations aren't methods but functions with a given signature, for example as a Rust function:

    fn sin(c: Complex) -> Complex
- Coerce when an operation expects a different type

- Don't use the whole precision of floating point (truncate to a lower precision and hide one decimal precision digit after truncation)

- The default display omits both the imaginary part and the fraction if zero


> Most people (me included) will instinctively reach for the class structure where Natural is the base class, and Integer extends that base.

Huh? Why? That's mathematically nonsense, so why would you encode it in your type system?

Naturals are a strict subset of Integers. The Liskov substitution principle applies perfectly well when you set this up the right way.

And guess what: actual "numerical tower" implementations (yes, even in languages with object systems like Common Lisp and Python) don't work this way either.

For example, in Python, the numerical tower is literally the opposite of what is proposed here. The base class is Number. Then Complex inherits from Number. And so on down to Integral, which is the most derived number type, not the most basic.

The numerical towers in Common Lisp and Scheme behave similarly. They all differ in structure details, but they all work the same way: the largest collections are the most basic types, the smallest collections are the most derived types.

This whole article is complaining about OOP being bad, when in reality OOP is helping to reveal a fundamentally bad design, which would be wrong in any programming paradigm.

There's nothing wrong with Peano numbers of course, but to introduce them in order to avoid fixing your unsound conceptual model, and to claim that it's an improvement over the OO-with-inheritance approach, is just silly.

It's certainly interesting that OO inheritance only supports "subset" relationships and not "superset" relationships, which I think is maybe what the author was originally trying to illustrate. It's something we do in math all the time. But that's not where the article goes and that's not what it presents as its own conclusion.

---

Edit / addendum:

You can build up a class hierarchy "additively" (adding structure/operations in more-derived types) rather than "subtractively" (removing structure/operations in more-derived types) using mixins in OO or type classes in FP.

The author mentioned Haskell and went so far as to define a custom ADT for integers but didn't notice that Haskell itself has its own kind of numerical tower in its type classes Num, Integral, etc. which works in exactly the additive style that they realized was incompatible with traditional OO inheritance.


> Naturals are a strict subset of Integers. The Liskov substitution principle applies perfectly well when you set this up the right way.

The article modelled it in both directions...


> All types in the hierarchy need to know about one another. Adding a new type requires changing all other types.

This was solves a few decades ago in smalltalk with the coerce protocol (aka double dispatch).

This article feels quite strawmanish.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: