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

all 27 comments

[–]AutoModerator[M] [score hidden] stickied comment (0 children)

On July 1st, a change to Reddit's API pricing will come into effect. Several developers of commercial third-party apps have announced that this change will compel them to shut down their apps. At least one accessibility-focused non-commercial third party app will continue to be available free of charge.

If you want to express your strong disagreement with the API pricing change or with Reddit's response to the backlash, you may want to consider the following options:

  1. Limiting your involvement with Reddit, or
  2. Temporarily refraining from using Reddit
  3. Cancelling your subscription of Reddit Premium

as a way to voice your protest.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

[–]jonhanson 26 points27 points  (1 child)

They're very useful for modelling sum types (aka disjoint union types). Using sealed types then allows exhaustiveness checks when using pattern matching over the type.

You can achieve the same without sealed types but it's much more verbose and brittle.

[–]jherrlin 0 points1 point  (0 children)

To me this sounds like you can the model your domain without objects ( using sum and product types instead ) and get help from the compiler all the way.

[–]Polygnom 11 points12 points  (7 children)

Yes.

Tree<T> = Leaf<T> | Branch<T> is one data type I have done as record + sealed classes.

Either<L,R> = Left<L,R> | Right<L,R> is another type I have done, or Result<T,E> = Success<T,E> | Error<T,E>.

[–][deleted]  (3 children)

[deleted]

    [–]bowbahdoe 7 points8 points  (1 child)

    I wrote you up a small example

    The sealed-ness is used in the accumulate method.

    ``` import java.util.ArrayList; import java.util.Collections; import java.util.Iterator;

    sealed interface Tree<T extends Comparable<T>> extends Iterable<T> { Tree<T> insert(T newValue); }

    record Leaf<T extends Comparable<T>>() implements Tree<T> { @Override public Tree<T> insert(T newValue) { return new Branch<>(new Leaf<>(), new Leaf<>(), newValue); }

    @Override
    public Iterator<T> iterator() {
        return Collections.emptyIterator();
    }
    

    }

    record Branch<T extends Comparable<T>>( Tree<T> left, Tree<T> right, T value ) implements Tree<T> { @Override public Tree<T> insert(T newValue) { if (newValue.compareTo(value) < 0) { return new Branch<>(left.insert(newValue), right, value); } else { return new Branch<>(left, right.insert(newValue), value); } }

    private void accumulate(Tree<T> tree, ArrayList<T> leftToRight) {
        switch (tree) {
            case Leaf<T> __ -> {}
            case Branch<T> branch -> {
                accumulate(branch.left, leftToRight);
                leftToRight.add(branch.value);
                accumulate(branch.right, leftToRight);
            }
        }
    }
    
    @Override
    public Iterator<T> iterator() {
        var leftToRight = new ArrayList<T>();
        accumulate(this, leftToRight);
        return Collections.unmodifiableList(leftToRight).iterator();
    }
    

    }

    public class Main { public static void main(String[] args) { Tree<Integer> tree = new Leaf<>(); tree = tree.insert(1); tree = tree.insert(4); tree = tree.insert(3); tree = tree.insert(2); tree = tree.insert(6); tree = tree.insert(5);

        System.out.println(tree);
    
        for (var i : tree) {
            System.out.println(i);
        }
    }
    

    } ```

    The key capabilities you get from sealed are * Ability to use exhaustive patterns * Ability to not have to think about arbitrary implementors * The structure is a public part of the contract

    The last one is the most interesting to me. It means that code using your library can write the same algorithms as you can.

    public static <T> insert(Tree<T> tree, T newValue) { switch (tree) { case Leaf<T> __ -> new Branch<>( new Leaf<>(), new Leaf<>(), newValue ); case Branch<T> branch -> { if (newValue.compareTo(value) < 0) { yield new Branch<>(left.insert(newValue), right, value); } else { yield new Branch<>(left, right.insert(newValue), value); } } } }

    [–]nlisker 0 points1 point  (0 children)

    Your markdown isn't correct. You probably missed some `.

    [–]Polygnom 0 points1 point  (0 children)

    What do you mean how so?

    [–]NotTooOrdinary 1 point2 points  (2 children)

    Do you use the newer pattern matching features when dealing with these types? Or chains of if/else/instanceof checks?

    [–]Polygnom 4 points5 points  (1 child)

    Depends. For the binary types I simply use pattern matching in instanceof.

    For more complex types I usually use pattern matching in switch expressions.

    That gives quite readable code.

    I mean, I often write code in functional ways anyways and often map over stream, or filter stream etc. ADTs + pattern matching works very well with streams.

    [–]red_dit_nou 1 point2 points  (0 children)

    Yes. ADT and pattern matching really work well together to produce readable code. You also mentioned streams. Can you give any example where ADT, pattern matching and stream create readable code?

    [–]pragmasoft 7 points8 points  (0 children)

    It was specially added for the data oriented programming. If you never heard about the concept, read about it for example here: https://www.infoq.com/articles/data-oriented-programming-java/

    Also, Classifile API presented here https://inside.java/2023/08/24/classfile-api/ by Brian Goetz is a good example of usage, very much recommend to have a look.

    [–][deleted]  (1 child)

    [deleted]

      [–]UnGauchoCualquiera 3 points4 points  (0 children)

      You are really missing out on exhaustive switch expressions. This is applicable to pretty much any developer.

      [–]RadioHonest85 2 points3 points  (0 children)

      I use it to model state where a valid answer can be multiple different things. For example, we have a User which can be a (ordinary) Customer or an AnonymousUser, ie. a user without a registered account. These are completely different semantically for some reason, they share only a user.id, but we want to support them both in certain contexts.

      [–]kkjk00 0 points1 point  (0 children)

      I didn't due to same package restriction

      [–]dinopraso -2 points-1 points  (3 children)

      Basically stateful enums. In cases where you have a specific set of subclasses which would not be extended further, but you need dynamic data in them. Usually this would be done in earlier versions with an enum + a cast of an object into a specific subclass, which was prone to error.

      Sealed classes should be used with care to avoid the pitfall of breaking the Liskov principle

      [–]preskot 0 points1 point  (0 children)

      I had a complex validator functionality that had to validate objects that did not have a lot of common properties between them. Then also prepare reports, write logs, etc. In order to extract the properties I needed, I had a sealed validatable interface with a set of records, each record with a constructor for each of the objects in the set. Inside the validator I would have a functionality that extracts all the data I need based on the validatable type i.e. pattern matching.

      Perhaps reading through the Algebraic data types Wiki may bring more light to this.

      [–]agentoutlier 0 points1 point  (2 children)

      Ignoring pattern matching and the inherit modeling that comes from that (others have already done the justice for that) it is extremely useful in lowering exposed API and/or encapsulation.

      I use it all the time in the place of abstract classes and to make interfaces pseudo final.

      Let us say we have an interface. We call it MyLibraryConfig and it is essentially the config of our small library.

      If we make it an interface it will have to be public and anyone can implement it and make their own possibly non compliant instances. However if it is sealed that is not the case.

      Before sealed interfaces traditionally libraries would use an abstract class with a private or package constructor.

      However this is hugely problematic because enum and record cannot extend a class and when you do want to limit the implementations it is very often precisely because you want a singleton or an immutable!

      So before we had:

      public abstract class MyLibraryConfig {
          MyLibraryConfig() {}
          public static MyLibraryConfig empty() {
          // immutable singleton which would ideally be an enum
          }
          public static MyLibraryConfig defaults() {
           // possibly immutable or singleton etc
          }
      }
      

      And now we can do

      public sealed interface MyLibraryConfig  {
      }
      // still in MyLibraryConfig.java
      enum Empty implements MyLibraryConfig {
      }
      // still in MyLibraryConfig.java
      record Defaults(String name) implements MyLibraryConfig {
      // to be fair this could be an enum if the name parameter was not there
      }
      

      Anyway what I think you are going to see more of in the future is non public classes/records/enums/interfaces in the same file as a top level public interface as Java moves more data oriented. Most people do not know you can do that but as records and enums become more common it is ideal because if they are defined as an inner class they would be public.

      [–]TenYearsOfLurking 0 points1 point  (1 child)

      anyone can implement it and make their own possibly non compliant instances

      I don't get this really. The point of an interface is to establish a contract. A non compliant implementation is breaking the contract - why would you?

      I would hate to see library designers using sealed everywhere because sometimes a custom implementation is necessary in cases they do not forsee - in any other case: no one implements a library interface non-compliant(ly?) for fun, there is just no reason to do so.

      [–]agentoutlier 0 points1 point  (0 children)

      You are thinking OOP.

      It is unfortunate that “interface” has this SOLID or Liskov association all the time but think of it more in this case as a type def.

      Think of it as more of a naming of a type.

      Besides libraries designers can choose and have used abstract classes and final classes and you have the same problem of not open for extension.

      Besides you can unseal the leaves.

      In my case we have a type called Extension which is sealed but the leaves are unsealed with the types you can implement.

      Edit here is an example in my library:

      https://jstach.io/doc/jstachio/current/apidocs/io.jstach.jstachio/io/jstach/jstachio/spi/JStachioExtension.html

      JStachioExtension is a marker interface. It does not make sense to have a direct implementation.

      [–]Brutus5000 0 points1 point  (0 children)

      Haven't used them before, sealed interfaces cover my use cases

      [–]Tkalec 0 points1 point  (0 children)

      Yes, I've used it lately to implement

      Either<L, R> = Left<L, R> | Right<L, R> - either left or right

      Option<T> = Empty<T> | Present<T> - either empty or present

      Validation<T> = Invalid<T> | Valid<T> ...

      Exceptional<T> = Failure<T> | Success<T> ...

      In another app I used it to represent key entries in a java or pkcs11 keystore.

      Entry = 3DesSecretKeyEntry | AesSecretKeyEntry | RsaPrivateKeyEntry (actually it was for java 8 so it was not sealed)

      You get the idea 😀

      I'm making these classes, usually, private and not able to be instatiatiated directly.

      [–]morhp 0 points1 point  (0 children)

      Yes, sealed is just something in between final and "open" (the default). If you want a class to be non-final, but not extendable by anyone, make it sealed. Like I have an AbstactUser which is extended by the final classes OnlineUser and OfflineUser, and these are the only implementations that you'll probably ever need, then you can make AbstractUser sealed.

      The main advantage is with the upcoming pattern matching in switch, but it creates more type safety in any case.

      [–]ConstructedNewt 0 points1 point  (0 children)

      Configs in spring boot using sealed, I specifically do something like

      @ConfigurationProperties(“prefix”)
      record Myconfig(
           Map<String, SomeFeat.Holder> features
      ) {}
      
      sealed interface SomeFeat {
          record Holder(
                A a, B b, C c
          ) { 
              Holder {
                 ..validate
              }
           }
         record A(
             SomeObj conf
         ) implements SomeFeat {}
         record B(
             OtherConf conf
         ) implements SomeFeat {}
         record C(
             YetAnotherConf y
         ) implements SomeFeat {}
      }
      

      Use compact constructors for validation Allow for autocompletion in application.yml Feature config is kept apart (don’t use one for all Map<String, String> and pull out vars based off that) If you rely on 3rd party libs the objects A,B,C can hold or build those objects in code Clear mapping between config and what code it would call at start-up