Hacker News new | ask | show | jobs
by naasking 767 days ago
> I have multiple times wondered which of those two approaches would be better in a given situation, often wanting some aspects of both.

ADTs are closed to extension with new cases but open to extension with new functions, eg. anytime you want to add new cases, you have to update all functions that depend on the ADT, but you can add as many functions for that ADT as you like with no issues.

Traits are open to extension with new cases but closed to extension with new functions, eg. you can add as many impl as you like with no issues (new cases), but if you want to add a new function to the trait you have to update all impl to support it.

They are logical duals, and the problem of designing systems that are open to extension in both cases and functions is known as the expression problem:

https://en.wikipedia.org/wiki/Expression_problem

2 comments

I suppose this is a genuine dichotomy, but I feel like it’s missing a more critical difference: ADTs cleanly represent data, even when nothing can be, or needs to be, extended from outside.

For example, a result is a success value or an error. A stock order is a market order or a limit order, and nothing else, at least until someone updates the spec and recompiles the code. Situations like this happen all the time. I don’t want to extend a result to include gizmos in addition to success value or errors, nor do I generally want to extend the set of functions that operate on a certain sort of result. But I very, very frequently want to represent values with a specific, simple schema, and ADTs fit the bill. A bunch of structs/classes, interfaces/traits and getters/setters can do this, but the result would look like the worst stereotypes of enterprise Java code to accomplish what a language with nice ADTs can do with basically no boilerplate.

> For example, a result is a success value or an error. A stock order is a market order or a limit order, and nothing else, at least until someone updates the spec and recompiles the code.

But that's just it, specs are rarely complete because reality is fluid. For example, a result is a success or an error, until maybe you want an errors to prompt the user to correct something and then the computation can be resumed (see resumable exceptions).

Should you even have to recompile your code to handle new cases? Why can't you just add the new case, and define new handlers for the functions that depend on your ADT without recompiling that code? That's the expression problem.

I want more syntax sugar for my ADTs that mirror what traits have. I don't need that kind of double extensibility.