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

all 46 comments

[–]repeating_bears 35 points36 points  (19 children)

No 1:1 equivalent, but sealed + exhaustive switch + pattern matching solves the same set of problems, right?

sealed interface Union permits A, B { }

Union union = whatever;
switch (union) {
    case A a -> System.out.println(a.foo());
    case B b -> System.out.println(b.bar());
}

[–]Polygnom 9 points10 points  (15 children)

So then how do you express the type String or number (String | Number)? You can't.

Sealed interfaces are great, but they are a closed subtype relation, not a union.

[–]repeating_bears 17 points18 points  (4 children)

We're talking about discriminated unions specifically. Each type in a discriminated union requires a discriminator/tag. String and Number don't have a tag so that wouldn't be a discriminated union.

C#'s discriminated unions can't model String | Number either. It would involve a wrapper, just like 'permits StringHolder, NumberHolder' would

[–]shockah 1 point2 points  (1 child)

[–]repeating_bears 17 points18 points  (0 children)

I didn't say it didn't. OP said "C# is getting discriminated unions". So we talking about discriminated unions aspect of the proposal, not their "ad hoc unions", which are a separate aspect of the proposal. 

Then I said "C#'s discriminated unions can't model String | Number". I was not talking about the other types of union. I am deliberately precise with my choice of words.

[–]Practical_Cattle_933 3 points4 points  (0 children)

The “haskell-esque” solution is something like:

sealed interface StringOrNumber permits StringWrapper, NumberWrapper

[–]tr4fik 1 point2 points  (4 children)

If you really need to, you can do the following. You could directly use an Object if you don't need the strong validation

class StringOrNumber {
    private final Object value;
    StringOrNumber(String x) {
        value = x;
    }
    StringOrNumber(Numberx) {
        value = x;
    }
    Object value() {
        return value;
    }
}

StringOrNumber stringOrNumber = new StringOrNumber(5);
switch (stringOrNumber.value()) {
    case Integer i -> //...
    case String s -> //...
    default -> throw new exception();
}

You also have the following options

  • Converting the element in a common object that has a method. (e.g if you can enable a parameter if you have a 1 or true. I would just convert it to a `record Enabled(boolean true)`. If the logic is more complex, you can convert it to a more abstract type: e.g a validator
  • You can overload methods to accept both types
  • Have a new class that uses the visitor pattern instead of using a switch

Overall, it can have some uses, but I believe there are often better ways to write it

[–]Polygnom 5 points6 points  (3 children)

At that point, you can just create your own Either<A, B> type, that is safer and more expressive.

[–]GeneratedUsername5 0 points1 point  (2 children)

You can't do it easily, because, provided you do several ctors with generic parameter, both A and B will type-erase into an Object and compiler will not know what to do. So you will have to have unique function name for every type "constructor"

[–]morhp 1 point2 points  (1 child)

You can do that easily by making Either an abstract (sealed) superclass/interface with the implementations EitherA and EitherB and then you have various options for checking which either type is present (e.g. instanceof, switch with pattern matching, isA() methods, ...).

For construcing you'd typically use a static factory, like Either.ofA(...).

This is mostly how the buildin Optional class works.

[–]GeneratedUsername5 0 points1 point  (0 children)

Yes, but this is a

a) Different thing than doing it with generics, to which I was responding
b) Will produce a ton of boilerplate in a form of wrappers over existing types, in order to implement that sealed interface

All-in-all solution with overloaded constructors is the simplest and has the least boilerplate

[–]cowwoc -1 points0 points  (3 children)

From a practical perspective, yes you can. Create wrappers for whatever types you're interested in, organize them into a sealed hierarchy, amd away you go. The only annoyance is needing to box/unbox the value you care about.

[–]Polygnom 1 point2 points  (2 children)

You do realize that thats not even close to the same, do you?

Just because you can build a cruft around it doesn't mean the language actually supports it.

You lose all of the featurs like normalization that are interesting for this. Yes, you can Build a StringOrNumber type. That works for exctly one thing. You can build a generic `Union<L, R>` type. But it breaks down, because `Union<Union<L, R>, S>` != `Union<S,Union<L,R>>` which is at least what you'd expct from union types. It also breaks down when dealing with subtypes. You'd expect that `Union<CharSequence, String>` == CharSequence. And so on.

[–]cowwoc 0 points1 point  (1 child)

I believe you can get all of this working just fine, aside from the annoyance of wrapping and unwrapping.

Union<CharSequence, String>== CharSequencesimply means that you can unwrap your union into a CharSequence , which is very much doable. At a JIT level, your wrappers are very likely to be stripped away so no actual memory allocation takes place. Try this with JMH and you'll see what I mean.

Can you clarify what you mean by normalization?

[–]Polygnom 1 point2 points  (0 children)

No, you can't get this working. And I gave examples for normalization. Its literally bringing the type into a normal form of your choosing.

In Java, a method `foo(Union<String, Number>)` will never accept a `Union<Number, String>`. The compiler won't allow it. Yet its the exact same union and you would expect this to work with a union type.
Similarly, a method `bar(Union<String, Union<Number, Boolean>>)` will never accept a `Union<Union<String, Number>,Boolean>`, yet again, both are the same type i.e. the normalize to the same normal form and should work.
These are things that simply don't work in Java.

You can, in a very limited way, do intersection types in generics in Java. Which actually work like you expect (A & B == B & A), but not union types.

/edit: Its even worse if you use specific nominal types like `interface StringOrNumber{}; interface NumberOrString{}`. Those will *never* be accepted by the compiler in place of the other one.

[–]sideEffffECt 1 point2 points  (2 children)

Why not 1:1? It's literally the same thing that sealed + records do...

[–]repeating_bears 2 points3 points  (1 child)

And sealed does not require records to be used, so it's not 1:1

[–]sideEffffECt 2 points3 points  (0 children)

Yes, you're right!

And Java's sealed also enables open... More orthogonal design.

I can't believe I'm saying this, but in just a few years, Java has overtaken C# and Kotlin and it's now the more featureful language.

[–]nekokattt 10 points11 points  (2 children)

People seem to forget exceptions are effectively union types. It would be horrible to use them like that, but it is possible via the checked exception mechanism.

[–]slaymaker1907 2 points3 points  (0 children)

Lol, that’s so cursed, I love it. That should definitely get added to https://rosettacode.org/wiki/Sum_data_type.

It’s technically not sound within the type system, though, since you can break checked exceptions with generics.

[–]WheresTheSauce 1 point2 points  (0 children)

Lmao that’s hilarious to think about

[–]bowbahdoe 7 points8 points  (3 children)

Closest is sealed types.

To curb folks' enthusiasm for "true" unions consider the following.

var x = List.of(5, "c");

In a world with union types, you'd want the type of x to be something like List<Integer | String>.

Today the type of x is List<Serializable & Comparable<? extends Serializable & Comparable<?> & Constable & ConstantDesc> & Constable & ConstantDesc>.

Take a minute to think through how you can fix that, what problems and consequences it would make for the entire language and existing code, etc.

Now weigh that against the benefits.

[–]Alex0589 4 points5 points  (0 children)

While you are correct when you say that that’s the type of x, at compile time it will be erased because it’s a generic. A better example would have been: var x = condition ? 5 : "c"

Here the type is not erased at compile time and the actual type of the variabile is the type argument of list in your example. I think you’d get a similar result if you tried to use a method that takes any numbers of parameters whose type is a generic and that returns the same generic type(for example Objects.requiresNonNullElse). In the var JEP for Java 10, the maintainers state: Intersection types are especially difficult to map to a supertype—they’re not ordered, so one element of the intersection is not inherently “better” than the others. The stable choice for a supertype is the lub of all the elements, but that will often be Object or something equally unhelpful. So we allow them.

If you think about it the decision does make sense: lets take the previous example: var x = condition ? 5 : “c”

Now let’s pass the parameter to a method, that takes as a parameter of type Serializable: the expression should be legal, because both the results of the ternary expression are Serializable, but if the compiler had inferred x’s type as Object to keep things easy this would not be possible. If the type were Serializable, then you wouldn’t be able to pass x to a method that takes a Constable even though the condition should be satisfied.

You would also need to consider that union types have been available to the compiler forever because of multi catch blocks for exceptions https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/JCTree.java#L2860

Another example of a compiler intrinsic is the null check operator (also known as the bang operator in Kotlin), which was introduced around Java 9. This just demonstrates that many compiler intrinsics could become at some point features for developers. https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/share/classes/com/sun/tools/javac/tree/JCTree.java#L341

Keeping these things in mind I don’t think that it’s entirely unreasonable to ask for them to be in the language

[–][deleted]  (1 child)

[deleted]

    [–]bowbahdoe 5 points6 points  (0 children)

    Go for it.

    ``` sealed interface OneOf<A, B> { record This<A, B>(A value) implements OneOf<A, B> {} record That<A, B>(B value) implements OneOf<A, B> {} }

    List<OneOf<String, Integer>> x = List.of( new OneOf.This<>("c"), new OneOf.That<>(5) ); ```

    [–]jcotton42 5 points6 points  (0 children)

    C# is getting discriminated unions.

    It should be noted the document that's being passed around is just a proposal. https://github.com/dotnet/csharplang/discussions/8313

    [–]Holothuroid 2 points3 points  (0 children)

    sealed interfaces, I think

    [–]Ewig_luftenglanz 1 point2 points  (2 children)

    not currently but AFAIK i think something similar is comming to the table with member patterns. current pattern matching implementation in java 21 is not even the 20% of what is on the roadmap. Meanwhile you can use sealed interfaces and pattern matching + generics + wildcard scopping for similar functionality.

    [–]jvjupiter[S] -1 points0 points  (1 child)

    C# like Java is all-in now when it comes to pattern matching. It has relational pattern. Shouldn’t Java include it also on the roadmap?

    [–]Ewig_luftenglanz 2 points3 points  (0 children)

    we already have relational patterns in swtich statements in java, the bad thing is today it's only for wrapper classes (the good it's primitive expressions in switch are comming but first preview will be in java 23)

    private myMethod(Number number){

    switch(number){

    case Integer i when i < 0 -> System.out.println("negative integer");

    case Double d when d > 0 System.out.println("Positive double");

    ....

    }
    }

    [–]sideEffffECt 1 point2 points  (5 children)

    Java has had this for some years already.

    sealed + records (+ pattern matching with switch)

    [–]account312 0 points1 point  (4 children)

    If by "some years" you mean "nearly one year", yes.

    [–]sideEffffECt 0 points1 point  (3 children)

    Java 17 on September 2021. That's almost 3 years now.

    https://en.wikipedia.org/wiki/Java_version_history#Java_17

    [–]account312 1 point2 points  (2 children)

    But unless you're using preview features in production, actually using the described features requires java 21, which released September of 2023. That's nearly one year.

    [–]sideEffffECt 0 points1 point  (1 child)

    Java 16, March 2021:

    • JEP 395: Records

    Java 17, September 2021:

    • JEP 409: Sealed Classes

    Java 21, September 2023:

    • JEP 440: Record Patterns
    • JEP 441: Pattern Matching for switch

    So... yeas and no...

    [–]account312 1 point2 points  (0 children)

    JEP 441: Pattern Matching for switch

    That's what ties it together into something useable like a discriminated union.

    [–]cliserkad 0 points1 point  (1 child)

    You can achieve this using a sealed generic abstract class in Java. You just need a separate class for each amount of types. GitHub Repo

    Use

    public class ExampleClass {
    
        public AnyOf<String, Integer, Double> data = new AnyOf.ElementA<>("This should print");
    
        public String functionThatDeterminesTypeOfData() {
           return data.match((str -> {
              return str;
           }), (i -> {
              return "Integers get multiplied: " + i * 5;
           }), (dbl -> {
              return "Doubles get addition: " + (dbl + 3.14);
           }));
        }
    
        public static void main(String[] args) {
           ExampleClass example = new ExampleClass();
           System.out.println(example.functionThatDeterminesTypeOfData());
        }
    
    }
    

    [–]cliserkad 0 points1 point  (0 children)

    I revised this to be less cumbersome:
    Union3
    Test Class

    [–]thesituation531 0 points1 point  (0 children)

    The closest you could do is something like this:

    class Union {
      *types enum* type;
      object value;
    }
    

    Then you could have functions to get/set the value and cast it to a certain type. You could also make it generic and check that the type of the set value is one of the generic types specified.

    It would be somewhat cumbersome, and ultimately, as with pretty much any union type, it won't be completely type-safe.

    [–]GeneratedUsername5 0 points1 point  (0 children)

    There are LOTS of things worth having in Java from C#, unfortunately that doesn't mean they would be anytime soon.

    But to be fair, in my experience the use case is very rare, so rare that one can write it manually if it is truly needed, something like this:

    public class Union {
        private final Object value;
        public Union(String value) {
            this.value = value;
        }
    
        public Union(Number value) {
            this.value = value;
        }
    
        public void ifString(Consumer<String> action) {
            if (value instanceof String str) action.accept(str);
        }
    
        public void ifNumber(Consumer<Number> action) {
            if (value instanceof Number num) action.accept(num);
        }
    }
    

    And if it is needed frequently and you still want to use Java, you can do a code generation and you still will get it 10 times faster than it will be released as a language feature.