This article discusses details of trying to use language features to model a fairly basic problem but doesn’t discuss the fundamentals of the problem very much.

If I have O operations (e.g. evaluate, stringify) and T types, then I need O•T implementations. If I fix O, I can add types until the cows come home by adding O implementations per new type. If I fix T, I can add new operations with T implementations per operation. If I want to let O and T vary, then the number of implementations I need to add per additional operation/type varies depending on what I’ve added before. No amount of programming language magic will change this basic count.

ISTM what’s really going on in the article is that the author is using the programming language as a registry of implementations. Those O•T implementations need to be stored somewhere so their callers can find them. In C++ this can be done when very verbose inheritance and virtual calls. In other languages, it’s less verbose. If multiple dispatch is built in, then one can use it directly. One can, of course, also build an actual table indexed on operation and type as a data structure in just about any language and use it, and in a language like C++ the result may well be quite a bit nicer than using a class hierarchy.

The problem is how to statically ensure that you don’t run into the case of a missing implementation for some combination at runtime, in particular in languages with separate compilation. If you have a library A that defines a basic (T, O) set, and two libraries B and C that aren’t aware of each other but both use A, where B adds a new type and C adds a new operation, and finally you have a program D that uses both B and C, how do you statically detect or prevent the case where D tries to use the new type from B with the new operation from C, but no one has provided an implementation for that combination; while also supporting the case of the required implementation being present.

> The problem is how to statically ensure that you don’t run into the case of a missing implementation

Static (compile-time) guarantees are only applicable to languages with static binding rules, in which case there is no problem - the compiler will just report "cannot resolve overloaded function" or something similar.

This is of course one of the downsides to languages with dynamic types and dynamic binding ... that many errors can only be detected at runtime.

I think the article as it stands makes that all pretty clear. But the point of the article is more that the language selection will determine how difficult it is to do that if you don't have control over the original definition of the operations or types.

> One can, of course, also build an actual table indexed on operation and type as a data structure in just about any language and use it, and in a language like C++ the result may well be quite a bit nicer than using a class hierarchy.

This requires unsafe casting and thus doesn't actually solve the expression problem, which is about how to do this without compromising safety. Your approach is what the solution to the expression problem in Haskell effectively does at runtime though, via type classes and the dictionary-passing translation that they undergo during compilation, but it's all type safe.

> In C++ this can be done when very verbose inheritance and virtual calls.

No - in C++ you'd just define the operators (either as global functions, or member functions of the types/classes in question), and the language's overload resolution rules would select the right implementation for each usage.

You could use inheritence (and virtual overrides if needed), or templates/generics, if you wanted to - C++ certainly gives you a lot of ways to make things more complex and shoot yourself in the foot, but the normal/obvious way would just be to define the overloaded operators and be done with it.

The expression problem is about dynamic dispatch, not static dispatch. For example, you call a function F that returns an object of one of the relevant types, and another function G that returns one of the relevant operations, and then you apply the operation to the object. How can you have F extend the set of types, and also G extend the set of operations, without having to recompile the caller, or without F and G having to coordinate, and also avoid a runtime error due to a missing implementation of some operation for some type.

I agree (although others in this thread do not), but the comment I was responding to was claiming that in C++ operator overloading requires inheritance and virtual methods, and I was just pointing out that this is not true.

The comment you responded to doesn’t mention overloading.

> In C++ this can be done when very verbose inheritance and virtual calls

Virtual methods overload the corresponding method in the parent class they are inheriting from. It could be a named method like "add()", or an operator like "+()".

The author of that comment is implicitly assuming that all types are derived via inheritance from a common base type that defines all the (virtual) methods/operators being overloaded.

The operations are different methods, not overloads of each other, and the different types are the types on which the methods are defined, not arguments to the methods. That’s also how the article presents it. There are no overloads in the scenario.

The article presents examples in many languages. His C++ inheritance/override example was an example of how NOT to do it, since if you add a new virtual method to the base class then all the derived classes either have to use that base class method or be modified to override it in an appropriate way.

Overuse of inheritance is one of the ways to shoot yourself in the foot with C++. If you want to overload (apply same operator to multiple types - related or not), then just overload - don't use inheritance.

Your own example, above (B defines new type, C defines new operator) doesn't appear to be about inheritance at all - you don't even say what language you are talking about, and elsewhere you say (contracticting the article author) that this "expression problem" only applies to dynamic languages ... you seem to be all over the map on this one!

I’m sorry, but the expression problem is a fairly well-known problem, and you don’t seem to be familiar with it, so are drawing the wrong conclusions.

The problem statement is: You have code (e.g. a library) that supports polymorphic operations on a range of types. It is called the “expression problem” because the prototypical example is to have a type representing an expression (like an arithmetic expression) that can have various forms (subtypes, not necessarily in the OOP sense), and there are operations like evaluating the expression or printing the expression, which of course need to be implemented for all those subtypes (you need to know how to print a constant, how to print a binary expression, and so on).

There is code that can take an arbitrary expression whose exact subtype will only be known at runtime, and which invokes one of the available operations on the expression object.

Now, you want to be able to write secondary libraries (or programs) that extend the first library by extending the set of subtypes and/or the set of operations, but without modifying the first library (and without duplicating code from the first library). With OOP inheritance, adding new subtypes is easy. With the visitor pattern (or equivalent runtime case-distinction mechanisms like pattern matching in functional languages), adding new operations is easy. But the combination of both is difficult, if not unsolvable.

Overloading doesn’t apply, because overloading would dispatch on the static type of the expression variable, but the use case is that this is an expression parsed from a textual representation at runtime (e.g. in the context of an interpreter or compiler, or maybe some data format parser), so the variable holding the expression object has a generic type and could be holding any expression.

I'm just going by the article this thread is responding to. Perhaps the author's C++ example that he lead with was meant to be a strawman "look how bad C++ is (when we use it this way)"?

If the problem statement is just how to add operators and types to a dynamically typed expression (in some arbitrary language), then in addition to implementing any new operator overloads, there is the question of how to dispatch, which would most obviously be done by mapping (operator, operand type) pairs to the registered type-specific operator implementation (aka overload). Again, not sure what the big deal is here.

Virtual methods and overloading are not the same thing.

You are likely mixing up the term overload with the term override.

Apples and oranges.

Mechanism vs functionality.

Huh, this is a very neat way to put it. Something about it seems... too neat? But I'm the wrong person to poke holes in it. Can I see other people debate it civilly?

My first exposure to the expression problem is the book Crafting Interpreters, Section 5.3.1,[1] which is very similar to the GP comment. The subsection is a short few paragraphs, with one or two tables for illustration, and it was one of the most eureka moments of my programming career. Ever since then, I see this trade-off everywhere, and I get reminded that engineering is all about finding the best trade-off and that the holy-grail of a perfect language or design is a waste of time.

[1]: https://craftinginterpreters.com/representing-code.html#the-...

this is counting it as if O only takes an argument, which is single dispatch, which OOP covers already (the `self` is the single argument).

In practice, Multiple Dispatch shines when you have 1) more than one argument type (duh) 2) higher order `O` operation [1]

[1]: think of a numerical routine that calls eigen or something, and you want eigen to exploit the matrix's type, such as symmetry or upper-triangular, and this is encoded as a matrix type)

In the expression problem, the other argument is the operation to be invoked, which can vary at runtime as well. OOP doesn’t cover that.

Overloading operators like "+" to work on new types, and maybe mixed types, requires each desired combination of operator and operand types to be implemented (as one can easily do in a language like C++, and presumably also in more modern languages). If there is any commonality between types as to how these operators are meant to work, then generics such as C++'s templates can also be used to reduce the number of implementations to those that are actually distinct.

I don't understand what problem the author is trying to solve here - maybe it's language specific? More related to dynamic typing and efficient dispatch?

In any case, operator overloading, while it can make for cute-to-read code, isn't really a good idea for code readability and maintenance.

> operator overloading, while it can make for cute-to-read code, isn't really a good idea for code readability and maintenance

so should we write `add_int_int(1,1)` and `add_int_double(1, 1.0)` and `add_double_double(1.0, 1.0)`?...

The expectation is that arithmetic operators are performing the corresponding arithmetic operations, so overloading for different arithmetic types (int, float, complex) doesn't produce any surprises or need to lookup operator definitions.

Applying arithmetic operators to non-arithmetic types starts to become a problem, even if some cases (set union/difference, string catenation) are natural...

The same goes for other operators... overloading '[]' indexing for maps as well as arrays seems natural, but would be highly confusing if you overloaded it for something unrelated to an index-like concept.

I think a problem here is that everyone has a different idea of when you hit the boundary between obvious/non-surprising and confusing, so you can't just say that overloading is OK as long as it is unsurprising.

Just because people have differing opinions (some well founded, some not!) doesn't mean that defining best practices doesn't make sense. Code readability and lack of surprise objectively are good things, and certainly important for large enterprise projects.

I think there are two alternate guidelines for use of operator overloading that would make sense.

1) Just don't overload operators at all for your own types! Leave that to the standard libraries and types that they define.

OR

2) Overload, but keep to the semantics of the operators as used by the language's built-in types (e.g. use arithmetic operators for arithmetic operations, comparison operators for ordering operations, etc). If your project was using a style guideline like this, then violations would be caught by code review (maybe automated by AI in the future?).

>I don't understand what problem the author is trying to solve here - maybe it's language specific? More related to dynamic typing and efficient dispatch?

The expression problem only arises in statically typed programming languages, it does not exist in dynamically typed programming languages.

Operating overloading has nothing to do with the problem and can not be used to resolve it. Operators are nothing more than a one-off syntax so we can use familiar childhood notation like a + b, etc... they are not particularly meaningful. The ability to overload operators or functions in general is also irrelevant since such overloads are resolved statically at compile time.

> The expression problem only arises in statically typed programming languages, it does not exist in dynamically typed programming languages.

Wadler's list of requirements for solving the expression problem include "with static checking that all cases are covered", so in one sense, yes, dynamic languages don't have this "problem". But in another sense, dynamic languages simply have no chance to solve the problem because they don't statically check anything.

It is true that it's much easier to do multiple dispatch and open-ended extension in dymamic languages. That's a nice benefit of them. But you do sacrifice all of the safety, code navigation, and performance of static types in order to get that.

The problem Wadler is trying to solve is "how can we have this sort of open-ended extension in both directions without giving up static safety?"

This isn't true. Julia manages to maintain good performance with multiple dispatch through lots of type inference in the compiler (and carefully written code in the language to preserve "type stability")

Yes, Julia is an interesting outlier in the design space. My understanding is that Julia gets the speed it has largely by JITting a lot of code for every instantation of a generic/dynamic function that it sees with all of the concrete incoming types.

That's an interesting point in the design space where you get the flexibility of dynamic types (and multiple dispatch!) and good runtime speed. But you pay for it with slower startup times, less predictable performance, and much higher memory usage. You are also married to a JIT. Julia really is designed to be run interactively in a REPL from source. The language isn't well-suited to compiling a standalone executable ahead of time. That makes perfect sense for its use cases, but would make it a challenge to adopt in other use cases.

(For example, I work on Dart which is mostly used for building mobile apps. That means we care deeply about executable size, startup speed, and the ability to compile ahead-of-time to native executables.)

> does not exist in dynamically typed programming languages

The "problem" exists for dynamically-typed languages as well. If you define a new class in Python, you still need to teach existing code how to operate on it (though you might be using inheritance to automatically derive some of those implementations, but inheritance obviously isn't limited to dynamic languages).

> Operating overloading has nothing to do with the problem

You've got T types (some new), and O operators (some new) and want to implement all operators for all types ... This is the exact definition of operator overloading.

There is no magic (other than inheritance or generics, if applicable) that will remove the need to individually implement all those O x T cases, and while that is obviously necessary, it is also all that you need to do.

If you are not talking about operator overloading - supporting the same operator for multiple different custom types, then what are you talking about ?!