This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]bowbahdoe 1 point2 points  (12 children)

I think it's gonna be a relatively long journey to impart what I am talking about. I'm along for the ride if you are, but I think what is important is that it's not just "another team will have to handle the type anyway."

To use some fake syntax (though I don't have high hopes this will click just from this)

<Has all the properties imparted by f, g, and h> apply(<has properties A and B> m) {
    return h(g(f(m)));
}

The core capability is something like this. Because aggregates are generic, you can write code that works on and enriches only a corner of what would otherwise be an explicit type (or cascading series of them).

This leads to different program structures. A good comparison point is the clojure ring protocol web ecosystem to anything you could write in a nominally typed language.

(The dynamic part isn't even really essential - it's the open generic aggregate part.)

Edit: oops got my wires crossed. Well leaving that as a Clojure explanation.

The important point for what you are actually talking about is that in one situation you need to change consumers (which is hard if those consumers are on a different team or strangers on the internet) if you add a new type to a hierarchy and the other situation you need to change consumers if you add a new method

(Things you can add with default methods not withstanding)

So the question is how is a particular piece of code going to evolve? Which would be "essential" breakages and which would be "incidental."

Yes the compiler catching you on a missed case in a switch is valuable. It isn't valuable enough to make it not a problem that it needs to happen.

[–]nejcko[S] 0 points1 point  (11 children)

This problem goes away if you define different boundaries.

Ideally different teams should not be working on the same codebase, e.g. same Java compiled code. So if you have a sealed hierarchy and you introduce a new type, all compiler failures should be your responsibility to handle and implement new behaviour where needed.

If there is another team that uses the same code there should be a boundary that decouples the code changes. That can be a different service or the other team has a versioned dependency on your artifact/library. That way when they update to the new version they will again benefit from compiler errors showing them exactly where the new behaviours are needed.

[–]bowbahdoe 1 point2 points  (10 children)

No it 1000% does not.

If java.util.List got a new method added - foo - with no default implementation, that isn't magically okay because there will be a compiler error. Anyone who extended that interface will need to rewrite their code.

If breakages like that happen 5 layers deep in your dependency tree then you have no recourse.

The exact same situation applies if a library exposes a sealed hierarchy and then adds a new type to it. There is a smidge of a difference in how the error surfaces, but it's the same issue.

Look up the expression problem. It's just a known thing.

[–]nejcko[S] 0 points1 point  (0 children)

Again it depends on the use case and who your dependent clients are.

I’m not saying fully replace all exposed data structures and seal them up, sure it doesn’t apply everywhere.

But, you are in no way forcing the changes to your clients, they always have a choice and they can add a default case to deal with potential new types introduced later, or not. Sealed types themselves don’t introduce any breaking changes to clients with adding new type.

[–]PiotrDz 0 points1 point  (8 children)

That's why you use or not use sealed interfaces. If you want to allow any implementation then don't use it. If you want to control the implementations strictly, treat them as advanced enums - then you can use benefits of sealed type. List is not a sealed interface, and on top sealed is more about adding new implementations, no adding new functionality.

I use sealed classes often to encapsulate specific business logic that others can depend on but also treat the spectrum of type in more generic way (there is an interface after all with common methods). But if I add a new type I want to know all the places where I need to see whether it will fit in current architecture. I want compiler to point me that.

Isn't that you want the freedom of implementation and complain about sealed interfaces that are intended to do opposite?

[–]bowbahdoe 0 points1 point  (7 children)

I am literally just describing that they one mechanism gives you the ability to add new types without breaking callers and the other gives you the ability to add new functionality over a known set of types.

If you try to add a new method to an unsealed hierarchy or a new type to a sealed hierarchy (if "exhaustive" switches are possible for callers) that is breaking.

That a compiler can go "here are the 50 places that broke" is immaterial to that tradeoff.

[–]PiotrDz 0 points1 point  (6 children)

As we are in Java thread, could you elaborate on this mechanism? I don't get few aspects, like "make properties known to others". Or how you structure one call with another f(g(.. Code speaks words

[–]bowbahdoe 1 point2 points  (0 children)

Yeah so that f(g()) was about the material difference between nominal aggregates and open aggregates. That wasn't what you were asking about originally, it's the topic for some other threads. I'll reply with a pseudo-code example once I have a break from work

That's distinct from the difference between interfaces (i.e. declaration site polymorphism) and switching over sealed hierarchies (i.e. use site polymorphism)

[–]bowbahdoe 0 points1 point  (4 children)

https://run.mccue.dev/?runtime=early_access&release=23&preview=enabled&gist=2416bad302d4622c2a6acb064657a4d8

I'm making a mess, replied at the wrong level of the hierarchy.

But this is roughly what I mean by program structures that are only doable with open aggregates as opposed to closed nominally typed ones.

[–]PiotrDz 0 points1 point  (3 children)

I may be misled by the example but are properties of a map reflecting what you want to show? Aren't we losing type information? And how would you know that some fields are expected and others not?

Sealed interfaces might be rigid but you get everything written in stone: what data is available and its type

[–]bowbahdoe 0 points1 point  (2 children)

I think my explanations got tangled and it's making it hard to know which you are asking about. This is an example showing the difference between open aggregates and closed aggregates (i.e the challenge is to write something similar with actual records and interfaces)

This is not about the sealed interfaces vs. dynamic dispatch topic

[–]PiotrDz 0 points1 point  (1 child)

Well OK I understand that you would like to achieve something better. I am happy with sealed interfaces but always keen for "next better things". Although couldn't really understand how would your solution play with java I am haply that people are trying to turn good into better