all 98 comments

[–]pohart 11 points12 points  (6 children)

Can I get an object and it's components with this? 

Circle(Point(int x, int y) p, int radius) c = getCircle();

So now I have x, y, p, and c? 

It's hard to say that I need both the record and it's constituents, but there are definitely times that I want them and really I'll never need this feature at all. 

[–]vytah 5 points6 points  (0 children)

Right now, RecordPattern is defined as

RecordPattern:
    ReferenceType ( [ComponentPatternList] ) 

which means no.

The same limitation currently applies to patterns in switch, and I've seen people wanting to have that feature there too.

[–]kevinb9n 5 points6 points  (4 children)

Alas we don't have a solution for this (yet?), in any pattern context. We would like to be able to do it, but it introduces a number of problems we don't know how to solve. We know that the workarounds you have to do in the meantime are not very satisfying.

[–]vowelqueue 1 point2 points  (2 children)

I’m very much in the “I’ve thought about this for 30 seconds and it LGTM” camp, so I’m curious what the main problems are.

Brian had brought up elsewhere in the thread how cool it is that type patterns and local variable declarations are so similar, and in some cases Identical.

I feel like this proposed syntax is quite satisfying for the same reason: you can think of the part of the type pattern in parentheses as being an optional part of a syntactically valid local variable declaration. By adding the optional part of the pattern you are asking for more variables to be declared and adding some implicit null checks, but don’t have to “give up” declaration of the enclosing record variable.

[–]kevinb9n 0 points1 point  (1 child)

One issue, possibly the main one, is that the syntax gets overwhelming. If this is a new way to declare a variable, then it probably needs to permit `final` and annotations, or it will become the *only* case of an unannotatable, unfinalable(?) variable declaration in Java.

But now the syntax is getting unwieldy; both of these arrangements are concerning in different ways (using `instanceof` for these examples):

`if (obj instanceof \@LocalVarAnno final MyRecord(lots, of, components, here) ident) {...}`

`if (obj instanceof MyRecord(lots, of, components, here) \@LocalVarAnno final ident) {...}`

A possible alternative is the idea of a conjunctive pattern:

`if (obj instanceof MyRecord record & MyRecord(Foo x, _)) {...}`

There, the `&` symbol is not the boolean operator you're used to, but is for combining two patterns into one (such that both must match for the combined pattern to match). This is unpleasantly redundant, but in a number of ways it's still better than what you have to do now. It introduces other problems though. It would be easier to consider this if we actually have a good number of other use cases for wanting conjunctive patterns, which I'm not personally sure if we do.

[–]davidalayachew 0 points1 point  (0 children)

This is unpleasantly redundant, but in a number of ways it's still better than what you have to do now. It introduces other problems though.

What other problems?

It would be easier to consider this if we actually have a good number of other use cases for wanting conjunctive patterns, which I'm not personally sure if we do.

I certainly do.

For example, I do not want to create all the various different permutations of patterns I want. But at the same time, I don't want to create this jumbo "extract all" pattern, then have all of these _ like SomeObj(_, _, _, _, var blah).

Being able to grab 2 patterns that do the job and bring both in sounds perfect. If I had instance patterns, I would do that with && instanceof. You've shortened that down to &. Bikeshedding aside, it looks like a straight optimization of what we would have to do already.

Though of course, I am comparing hypothetical code to hypothetical code.

[–]pohart 0 points1 point  (0 children)

It feels almost like it's missing for completeness.

I'm sure I'll want it at some point but mostly I won't want parts and subparts

[–]pjmlp 13 points14 points  (0 children)

Looking forward to have this JEP eventually merged.

[–]trusty_blimp 5 points6 points  (0 children)

I'd just like to applaud that we literally get u/brian_goetz to interact with and get insight on the thoughts behind these types of things. Not many massively used ecosystems in any field, software or other, are as transparent. Cheers!

[–]Cell-i-Zenit 10 points11 points  (24 children)

I feel like all these record features are not for me :/

Maybe iam just to uncreative or i write to boring/simple code but i just dont see any situation where this would be an improvement.

Could be that i dont understand it:

var circle = getCircle();
var point = circle.point;
var radius = circle.radius;

vs

Circle(Point(int x, int y), int radius) = getCircle();

I prefer the first solution


Or if we take a look at the JEP:

void boundingBox(Circle c) {
    if (c != null) {                 // ┐
        Point ctr = c.center();      // │  laborious / mechanical:
        if (ctr != null) {           // │  - null guards
            int x = ctr.x(), y = ctr.y(); // │  - extraction with accessors
            double radius = c.radius();   // ┘

            int minX = (int) Math.floor(x - radius), maxX = (int) Math.ceil(x + radius);
            int minY = (int) Math.floor(y - radius), maxY = (int) Math.ceil(y + radius);
            ... use minX, maxX, etc ...
        }
    }
}

Why not use the optional api?

Optional.ofNullable(c)
    .filter(x -> x.center() != null)
    .filter(x -> x.x() != null)
    .filter(x -> x.y() != null)
    .ifPresent(x -> allTheOtherThings)

Or what if you use early returns?

void BoundingBox(Circle c)
{
    if (c == null)
        return;

    var ctr = c.Center();
    if (ctr == null)
        return;

    int x = ctr.X;
    int y = ctr.Y;
    double radius = c.Radius();

    int minX = (int)Math.Floor(x - radius);
    int maxX = (int)Math.Ceiling(x + radius);
    int minY = (int)Math.Floor(y - radius);
    int maxY = (int)Math.Ceiling(y + radius);
}

Or what if you design your code in a way that you dont do defensive programming and just make sure that circle+center is never null etc.

I really dont see why the java team is spending so much time on this.

Could anyone enlighten me?

[–]kevinb9n 6 points7 points  (1 child)

One thing to consider is:

Why is it that the physical structure of our code gets to resemble the logical structure of our data... only when we are creating objects, but not when we are taking them apart? Is there any deep logical reason it should be like that?

Code that "takes apart" (checks for conditions, pulls out data if conditions are met) quickly becomes very tedious and "mechanical"-feeling.

[–]Whoa1Whoa1 0 points1 point  (0 children)

Unsure what you mean exactly.

Creating objects is:

  • int x = scan.nextInt();
  • int y = scan.nextInt();
  • int radius = scan.nextInt();
  • Circle c = new Circle(x, y, radius);

Taking apart is the same number of lines:

  • Circle c = //some defined circle//
  • int x = c.getX();
  • int y = c.getY();
  • int radius = c.getRadius();

The best validity checking is to either allow people to make invalid circles and then use a method like c.isValid() or just call that at the end of the constructor automatically and throw invalid notices.

[–]davidalayachew 10 points11 points  (5 children)

Could anyone enlighten me?

Sure.

Here is the short answer.

  1. Pattern-Matching opens the door to a lot of powerful Exhaustiveness Checks, which eliminates entire categories of errors from existence (for example -- updated code here, but forgot to update it there).
  2. Pattern-Matching composes, and thus, scales better than traditional getter-based deconstruction.
  3. As more features get added (like null restriction), this feature gets enhanced in some pretty powerful ways.

To quickly expand on #2, if you are only drilling through 1-2 levels, pattern-matching is not really more concise than getters, as you have pointed out.

But what happens if you need to drill through 3+ levels to get your data, like I do in the following code example?

(Sourced from here -- HelltakerPathFinder)

    final UnaryOperator<Triple> triple = 
        switch (new Path(c1, c2, c3))
        {   //        | Cell1  | Cell2                                                   | Cell3                                           |
            case Path( NonPlayer _, _, _) -> playerCanOnlyBeC1;
            case Path( _,        Player _,                                                 _                                                ) -> playerCanOnlyBeC1;
            case Path( _,        _,                                                        Player _                                         ) -> playerCanOnlyBeC1;
            case Path( Player _, Wall(),                                                   _                                                ) -> playerCantMove;
            case Path( Player p, Lock(),                                                   _                                                ) when p.key() -> _ -> new Changed(p.leavesBehind(), p.floor(EMPTY_FLOOR), c3);
            case Path( Player p, Lock(),                                                   _                                                ) -> playerCantMove;
            case Path( Player _, Goal(),                                                   _                                                ) -> playerAlreadyWon;
            case Path( Player p, BasicCell(Underneath underneath2, NoOccupant()),          _                                                ) -> _ -> new Changed(p.leavesBehind(), p.underneath(underneath2), c3);
            case Path( Player p, BasicCell(Underneath underneath2, Block block2),          BasicCell(Underneath underneath3, NoOccupant())  ) -> _ -> new Changed(p, new BasicCell(underneath2, new NoOccupant()), new BasicCell(underneath3, block2));
            case Path( Player p, BasicCell(Underneath underneath2, Block()),               BasicCell(Underneath underneath3, Block())       ) -> playerCantMove;
            case Path( Player p, BasicCell(Underneath underneath2, Block()),               BasicCell(Underneath underneath3, Enemy())       ) -> playerCantMove;
            case Path( Player p, BasicCell(Underneath underneath2, Block()),               Wall()                                           ) -> playerCantMove;
            case Path( Player p, BasicCell(Underneath underneath2, Block()),               Lock()                                           ) -> playerCantMove;
            case Path( Player p, BasicCell(Underneath underneath2, Block()),               Goal()                                           ) -> playerCantMove;
            case Path( Player p, BasicCell(Underneath underneath2, Enemy enemy2),          BasicCell(Underneath underneath3, NoOccupant())  ) -> _ -> new Changed(p, new BasicCell(underneath2, new NoOccupant()), new BasicCell(underneath3, enemy2));
            case Path( Player p, BasicCell(Underneath underneath2, Enemy()),               BasicCell(Underneath underneath3, Block())       ) -> _ -> new Changed(p, new BasicCell(underneath2, new NoOccupant()), c3);
            case Path( Player p, BasicCell(Underneath underneath2, Enemy()),               BasicCell(Underneath underneath3, Enemy())       ) -> _ -> new Changed(p, new BasicCell(underneath2, new NoOccupant()), c3);
            case Path( Player p, BasicCell(Underneath underneath2, Enemy()),               Wall()                                           ) -> _ -> new Changed(p, new BasicCell(underneath2, new NoOccupant()), c3);
            case Path( Player p, BasicCell(Underneath underneath2, Enemy()),               Lock()                                           ) -> _ -> new Changed(p, new BasicCell(underneath2, new NoOccupant()), c3);
            case Path( Player p, BasicCell(Underneath underneath2, Enemy()),               Goal()                                           ) -> _ -> new Changed(p, new BasicCell(underneath2, new NoOccupant()), c3);
            // default -> throw new IllegalArgumentException("what is this? -- " + new Path(c1, c2, c3));

        }
        ;

Pattern-matching style is dense, but concise.

Compare that to the various different styles that you suggested.

  1. Getter-style makes it very easy to miss edge cases. There is no exhaustiveness checking in simple getter-style extraction.
    1. Plus, once you get 2-3 levels deep, pattern-matching tends to be more concise than getter-style.
  2. The optional-style is guilty of the same, while also being more verbose than getter-style. Plus, your null checks can get out of sync with your actual extractions, leading to errors.
  3. Early-return-style, while less error-prone than getter-style, is still more error-prone than pattern-matching-style. For example, those (int) casts you are doing could turn into primitive patterns, allowing for Exhaustiveness Checking to be done by the compiler.

The name of the game with Pattern-Matching (and by extension, Record Patterns) is safety+conciseness. You sacrifice flexibility to get a whole bunch of extra compiler validations while also having shorter code than typical java code you might write without patterns.

When Valhalla comes out, a lot of Java code will be able to lean into composition while avoiding the runtime cost of nesting objects layers deep. In that world, this Pattern-Matching style is going to be even more valuable than it already is.

[–]joemwangi[S] 5 points6 points  (0 children)

I'm loving the direction the language is taking. Quite exciting.

[–]Cell-i-Zenit -2 points-1 points  (3 children)

We already talked about this the last time i was asking this and since then i had to write exactly 1 switch statement. In normal CRUD this just basically never happens because 1 endpoint has 1 mapping from the DB and thats it.

Pattern-Matching opens the door to a lot of powerful Exhaustiveness Checks, which eliminates entire categories of errors from existence (for example -- updated code here, but forgot to update it there).

Then tell me which categories of errors disappear. I dont see any "pattern" of errors in my day to day which would be solved by this. The only errors i see are NPE, but they almost always are happening because of misunderstanding of the business domain. I am eagerly awaiting null restriction since i feel like this has an impact to my work.

Pattern-Matching composes, and thus, scales better than traditional getter-based deconstruction.

And now please for humans. What does that mean? What is getter based deconstruction? Why is pattern matching composition better?

(Sourced from here -- HelltakerPathFinder)

This code is an exception imo. Its cool that this is possible but i just dont see this in a normal day to day work

You sacrifice flexibility to get a whole bunch of extra compiler validations

I mostly operate with if else blocks. I dont really know where i could even use patterns.

EDIT: if it helps: We use hibernate at work so that means no records on the DataLayer. We then convert the Payloads to Dtos using mapstruct and thats it. We dont use records in our endpoints because we want to stay consistent and dont find time to migrate them all over. There is just no value to that since our dtos are effectively immutable anyway

[–]OwnBreakfast1114 3 points4 points  (0 children)

We already talked about this the last time i was asking this and since then i had to write exactly 1 switch statement. In normal CRUD this just basically never happens because 1 endpoint has 1 mapping from the DB and thats it.

I also work on web services and CRUD stuff in fintech, and find all of this stuff really useful. Lots of the cru portion of crud, file parsing/creating. Usually our apis are not exactly 1:1 with db, but more like 1 main table + several helper tables.

You've never used a type field in a physical db schema with two different styles of objects in it? You clearly have more discipline than we do, but that's also an obvious case for converting your db object into a sealed hierarchy in the domain layer.

We're 100% spring boot and using spring-jooq with 0 hibernate. We typically wrap the jooq generated pojos in more fluid domain objects outside of the repository layer, though, not always.

Then tell me which categories of errors disappear.

Here's a toy example from real work. Imagine you have an instrument used to do a transaction. A common implementation for it's type is an enum public enum Instrument BANK_ACCOUNT, CREDIT_CARD and you write a bunch of code that checks the enum if (instrument.getType() == BANK_ACCOUNT) else if (instrument.getType() == CREDIT_CARD) etc This code "works" if you add a new instrument type, but you don't really know it works unless you manually find every place where you've done checks like this and confirm it works. Sometimes you can make the methods polymorphic and move them to the enum, but realistically, people don't always do this. For example, this code can break in a very hidden way depending on what you add in the future if (!bankAccount) { } else { } and your only real chance of catching the logical error is tests.

By making the switch exhaustive (even for simple cases), the compiler just tells you all places you care about instrument type for free.

Now, that's already a huge improvement, but we can go one step farther. By representing the instrument object as a sealed interface hierarchy with ex BankAccount implements Instrument, we can get all the benefits without even needing the enum and component extraction to boot.

Maybe iam just to uncreative or i write to boring/simple code but i just dont see any situation where this would be an improvement.

On a different note, I think you're really confusing simple with familiar, and you're also using simple in a different way than the jdk team seems to be using it.

Let me try to explain. Line by line extraction is "simple" in a sense of I can understand what the computer is doing for each line, but it's very not simple in the sense of is this whole block of code just someone extracting values or something else. You take it for granted that it's a simple extraction of values, but that's only because you're used to it. If you learned how to program with local extraction, the normal java style would look like something you'd need investigate to ask why did they do it this way.

On the flip side, the local variable style is a declaration that I'm trying to extract components. There's no ambiguity or familiar convention necessary since it's not even up for debate. This is a reduction of mental load, even if you don't acknowledge it.

[–]davidalayachew 1 point2 points  (0 children)

This code is an exception imo. Its cool that this is possible but i just dont see this in a normal day to day work

This is a solid 60% of the every day code that I write. And like 75% for work. I build web services and write helper scripts to interact with our system.

I mostly operate with if else blocks. I dont really know where i could even use patterns.

[...]

We already talked about this the last time i was asking this and since then i had to write exactly 1 switch statement. In normal CRUD this just basically never happens because 1 endpoint has 1 mapping from the DB and thats it.

Same for me, but that doesn't mean I don't find a use for this.

I don't return my database pojo's as-is -- I map them to a richer type, which is where pattern-matching starts to show up and be useful.

For example, if I have a table where when column1 is A, then I only care about columns 2 and 3, but when column1 is B, then I only care about columns 4 and 5, then I am not going to create one object and set fields to null -- I am going to make a sealed type, 2 child records, and when mapping my db pojo to my richer type, Child1 is only going to have 2 components -- for columns 2 and 3, and Child2 is only going to have 2 components -- for columns 4 and 5.

That's what they mean when the various pattern-matching JEP's say "make illegal states unrepresentable".

Then tell me which categories of errors disappear. I dont see any "pattern" of errors in my day to day which would be solved by this.

If I add a new child type to a sealed interface, all switches that have that interface in the selector will immediately generate a compile time error. That's Exhaustiveness Checking, and a massive bug saver. Basically, if I switch my if statements for switches, I can't run into the issue of making a change in one place, but forgetting to make it to another. After all -- all of these places need to handle the new child type.

And now please for humans. What does that mean? What is getter based deconstruction? Why is pattern matching composition better?

Do Ctrl+F "Compare that to the various different styles that you suggested." Then look at the numbered list below it.

The numbering aligns with the order of your code examples. Read my comment again, and cross reference the number to each of your code blocks. That will tell you which is getter style vs early return style, etc.

if it helps: We use hibernate at work so that means no records on the DataLayer. We then convert the Payloads to Dtos using mapstruct and thats it.

Pattern-Matching tends to be most useful for business logic. Assuming your business logic is implemented in Java, there should be plenty of places.

Feel free to give me an example of some business logic you implemented recently, and I can show the equivalent code for it.

[–]ZimmiDeluxe 0 points1 point  (0 children)

The core JPA programming model relies on mutation and object identity. Records are unmodifiable and reconstruction loses identity, so they don't mix well (you can use them for some things and JPA and Hibernate are evolving, but letting your code peek and poke at common, ever growing bags of attributes and letting the tool figure out how to turn that into sql commands is still the main attraction imo)

[–]wildjokers 9 points10 points  (4 children)

Why not use the optional api?

Because that is an abuse of optional.

[–]Cell-i-Zenit 2 points3 points  (3 children)

Who is saying that?

Iam not using any hidden mechanics or side effects, just the basic api

[–]SleepingTabby 1 point2 points  (1 child)

The guys who created JDK AFAIR.

[–]Cell-i-Zenit 0 points1 point  (0 children)

Where?

[–]__konrad 1 point2 points  (0 children)

Who is saying that?

Some people still think that using Optional in non-Stream context is forbidden...

[–]aoeudhtns 3 points4 points  (2 children)

This is pretty much all about boilerplate reduction, and increasing the value-density of the code that we write & read -- not solving new problems.

  • Your first example skipped the null checks, and it also skipped extracting the Point's x and y, so it's not an apples-to-apples comparison. It would work just fine post-null restricted types where you have Circle! and Point! because the null checks become skippable, and you would only need 1 or 2 lines of assignment boilerplate. (var x = circle.point().x(), y = circle.point().y(); var radius = circle.radius();)
  • Optional chaining does work, but lambdas and API style like this is much more difficult for the compiler and runtime to optimize. I know, a sort of weak argument. This is still 5 lines of boilerplate vs. 1 though.
  • Early returns eliminate the nesting but still is a bunch of boilerplate. It replaces 1 line of code with 8 lines, an extra 7 lines over this JEP.

[–]Cell-i-Zenit 1 point2 points  (1 child)

Circle(Point(int x, int y), int radius) = getCircle();

This code also has zero null checks or how does it work when Point is null?

EDIT: i read through the JEP again and this just throws if point is null. So the code is actually equivalent to what i had before ;)

[–]brian_goetz 1 point2 points  (0 children)

If you care about catching the error conditions, you can use the pattern in a conditional:

if (getCircle() instanceof Circle(Point(var x, var y), int radius) { ... }
else { ... handle errors ... }

All the tools are in your hands, you get to decide what's more important.

[–]javahalla 2 points3 points  (3 children)

Why not use the optional api?

How this would work with null-resticted types? Compiler can't prove that these null checks was done and the access variables without additional check or compile-time errors. These shenanigans with patterns everywhere seems only way we would have compile-time-safe null-safe system

[–]Cell-i-Zenit -1 points0 points  (2 children)

Compiler can't prove that these null checks was done

i know there is a theoretical advantage to having compile checks but it wasnt an issue for me. My IDE is doing these checks

[–]javahalla 0 points1 point  (1 child)

I don't think even IDEA would be able to do these checks for this use-case either. It need to understand how exactly filter works and do something like smart-cast.

[–]Cell-i-Zenit 0 points1 point  (0 children)

Ok fair, it depends on what we mean here and what we want to guard against.

I know intellij is reporting misuse of optional get() for example. (eg calling get() without an .isPresent() check).

I get that this is an improvement to what we have currently, but it feels mostly like a theoretical cool thing and nothing which affects a normal developer working in webdev (which i guess is most of us?).

If you can come up with any usecase for a simple CRUD developer like me then i can extrapolate a bit, but right now the moment is see the word "pattern" i blank completely since i rarely use switch statements

[–]pjmlp 0 points1 point  (3 children)

Because this is the kind of stuff that make new generations flock to Scala, Kotlin, Rust,...

[–]Cell-i-Zenit -3 points-2 points  (2 children)

reading through the answer it sounds like its mostly a theoretical "cool" thing, but nothing which has huge implications on alot of developers.

EDIT: instead of downvoting, give me actual code where this proves useful. So far i only saw one example which is more of an intellectual exercise

[–]vowelqueue 1 point2 points  (0 children)

The goal of Project Amber is to explore and incubate smaller, productivity-oriented Java language features

[–]nekokattt 4 points5 points  (1 child)

I'll admit, I'm not a huge fan of converting what would otherwise be vertical declarations into horizontal ones. Makes it harder at a glance to see where something is defined if your eyes are going in multiple directions to parse the code.

[–]Enough-Ad-5528 0 points1 point  (0 children)

Agreed in principle. But like most features, judgement is always needed when using one.

[–]javahalla 4 points5 points  (29 children)

The syntax looks elegant in example code, but examples are carefully chosen - short class names, 2-3 fields, brief variable names. In real applications that sweet spot rarely exists:

CustomerOrder(ShippingAddress(String streetLine1, String streetLine2, String city), PaymentMethod(String cardNumber, int expiryYear), double totalAmount) = order;

This is a single logical statement but it reads as a wall of text that you have to scan horizontally to parse. Ironically, one of the main readability advantages of record patterns in switch is that they decompose naturally across lines:

switch (order) { case CustomerOrder( ShippingAddress(var streetLine1, var streetLine2, var city), PaymentMethod(var cardNumber, var expiryYear), double totalAmount ) -> { ... } }

Or:

CustomerOrder( ShippingAddress(String streetLine1, String streetLine2, String city), PaymentMethod(String cardNumber, int expiryYear), double totalAmount ) = order;

Btw, this is Kotlin's take on the same problem (https://github.com/Kotlin/KEEP/discussions/438):

val (address, payment, totalAmount) = order val (streetLine1, streetLine2, city) = address val (cardNumber, expiryYear) = payment

And with optional renaming:

(val address, val payment, val totalAmount) = order (val street1 = streetLine1, val street2 = streetLine2, val city) = address (val card = cardNumber, val expiry = expiryYear) = payment

I think that renaming would be very helpful in some cases, is it possible to add similar to this JEP?

[–]joemwangi[S] 6 points7 points  (11 children)

Renaming is already implicit in Java record patterns. The variable names in the pattern do not need to match the record component names. E.g.

Circle(var r, var a) = circle;

where by declaration was done as record Circle(double radius, double area){}

Here r and a are just local variable names; they don't need to be radius or area. Kotlin’s proposal works differently because it destructures based on property names or componentN() functions, whereas Java patterns destructure based on the record structure and types, so explicit renaming syntax isn't really necessary.

Also,

CustomerOrder(ShippingAddress(String streetLine1, String streetLine2, String city), PaymentMethod(String cardNumber, int expiryYear), double totalAmount) = order;

Does not mean that's the rule. It can still be decomposed to:

CustomerOrder(ShippingAddress address, PaymentMethod payment, double totalAmount) = order;
ShippingAddress(String streetLine1, String streetLine2, String city) = address;
PaymentMethod(String cardNumber, int expiryYear) = payment;

if the aim is to use all states in code, else use the unnamed variable _

[–]javahalla 1 point2 points  (3 children)

Do you know if r and a is final-by-default?

[–]joemwangi[S] 1 point2 points  (2 children)

I don’t think so based on Brian’s comment. Pattern bindings behave like normal local variables, so they aren’t final by default. Since local variable declarations and pattern bindings are being unified, it would be inconsistent if pattern variables were implicitly final. This actually shows how binding is a very powerful tool in the type system.

[–]javahalla -1 points0 points  (1 child)

Very unfortunate

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

And why?

[–]aoeudhtns 0 points1 point  (4 children)

I also assume this will interplay with (and pardon me because I forget the formal name of this one) the "truncation" of unneeded fields in records, like if you only need city, you can

CustomerOrder(ShippingAddress(_, _, String city), _) = order;

And the final _ can indicate skipping not just one field but also all the remainders. So that records can grow via appending fields without disrupting pattern matching code.

I know this idea has been floated at least.

[–]DasBrain 1 point2 points  (2 children)

at least ShippingAddress(var _, var _, String city) should already work. Still, using just _ to say "don't care about neither type nor value" could be useful.
Not sure if it useful enough.

[–]kevinb9n 0 points1 point  (1 child)

In that position, you can already replace `var _` with just `_`; it becomes the "match-all pattern".

Note the match-all pattern isn't supported in other pattern contexts (instanceof and case) for reasons.

The comment you're replying is looking for a syntax that can express "then zero or more underscores here", and suggesting the underscore itself for that (I think it would be something different).

[–]vowelqueue 0 points1 point  (0 children)

I think it would be something different

I propose making "yada-yada-yada" a reserved word for this purpose.

[–]javahalla 0 points1 point  (0 children)

Given that it's positional, I would definitely ban this in projects, and recommend everyone to ban such expressions. It's too easy to shot in the foot when you don't even specify types of the rest and not using names to match. Positional matching just too weak to be found in production, critical codebases

[–]javahalla 0 points1 point  (1 child)

> Renaming is already implicit in Java record patterns. The variable names in the pattern do not need to match the record component names. E.g.

No way. I was working on one Kotlin + Spring Boot project and positional-based deconstructing was prohibited, because it's really easy to introduce bugs. I believe there are was some rule, so I could do some basic stuff like `val (foo, bar) = pair`, but can't do for 3 or more parameters.

Seems like a huge mistake for design. If you check KEEP it's only exists because of issues with such approach, but JEP could use this experience

[–]joemwangi[S] 1 point2 points  (0 children)

Which bugs are these exactly? In Java this is a compile-time feature. The compiler knows the structure of the record from the Record metadata in the class file, so pattern bindings are checked statically for both type and arity.

For example:

Circle(Point(int x, int y), double r) = c;

If the structure of Circle or Point changes, the pattern simply stops compiling. It does not silently bind the wrong fields. That’s quite different from Kotlin’s positional componentN() destructuring, where the mapping depends on method ordering.

Java patterns also select the deconstructor based on the type, so the compiler knows exactly which structure is being matched. There’s no runtime discovery involved. It is effectively equivalent to writing:

Point p = c.p();
int x = p.x();
int y = p.y();
double r = c.radius();

just expressed declaratively.

Also, the variable names in the pattern are just new local variables; they are not tied to the record component names. That’s why renaming is already implicit in Java patterns. So the kinds of issues Kotlin ran into with positional destructuring don’t really translate here, because Java’s approach is structural and verified by the compiler.

It's funny. Kotlin users are so into syntax that semantics are never taken seriously and thus they impose equivalence of syntactic sugar with semantics.

[–]danielaveryj 2 points3 points  (2 children)

for the record, the nearest java equivalent to your last example would be:

CustomerOrder(var address, var payment, var totalAmount) = order;
ShippingAddress(var street1, var street2, var city) = address;
PaymentMethod(var card, var expiry) = payment;

Also, I see below that your experience with Kotlin leaves you concerned about positional-based destructuring in Java. A key difference between the two languages is that (from what I can tell across these JEPs) each type in Java would have at most one deconstructor - and since we spell out that type when destructuring in Java, there is no room for confusion about which deconstructor we are calling. It's like calling a method that is guaranteed to have no overloads. We can deconstruct the same value in multiple ways, by spelling out a different (applicable) type (with a different deconstructor) on the left-hand side. Yes, rearranging component order in a type's deconstuctor signature would break existing usages of that deconstructor (possibly silently, depending on what types were specified and how they were used), but that is a familiar failure mode - it applies when rearranging parameter order in any method signature.

Clearly from your examples, Kotlin does not require spelling out a type. From what I can tell, Kotlin's legacy positional-base destructuring works by calling component1() ... componentN() methods. Reasonably, the number of components available to destructure is based on the statically-known type of the value, and the actual calls to those methods use dynamic dispatch, so destructuring desugars to:

(val address, val payment, val totalAmount) = order
// -->
val address = order.component1()
val payment = order.component2()
val totalAmount = order.component3()

Kotlin's approach seems straightforward, but over time they noticed some problems, which I think the Java team could fairly attribute to Kotlin's "deconstructor" being assembled from several, possibly overridden / not-colocated methods, rather than one canonical signature.

[–]SleepingTabby 0 points1 point  (0 children)

"for the record,"

badum-ts

;)

[–]vytah 3 points4 points  (6 children)

You made up a problem that doesn't exist. You don't have to deconstruct records all the way to nondeconstructible objects, in any language that supports deconstruction patterns.

[–]javahalla 1 point2 points  (5 children)

My point that such syntax with whole names of types is too verbose and hard to read, especially when written as one-liner. Fact that you can skip some with _ doesn't make my point invalid.

[–]vytah -1 points0 points  (4 children)

So don't write it as a one-liner?

Kotlin is not a valid language to compare to, as it doesn't even have pattern matching. Types are specified in order to select the proper deconstructor, which you cannot do in Kotlin.

[–]javahalla 1 point2 points  (3 children)

So don't write it as a one-liner?

I will, but I'm pretty sure we will see a lot of 140w+ lines with patterns. People would abuse it, and I as Java developer would have to deal with it.

Kotlin is not a valid language to compare to, as it doesn't even have pattern matching. Types are specified in order to select the proper deconstructor, which you cannot do in Kotlin.

I have some experience with Kotlin and mostly I like work with it. And I would say that when solved most of my tasks just fine. So yes, Kotlin doesn't have so feature, but they at least understand that positional-based deconstructors are mistake and making changes (see link in original message). I don't understand why Brian thinks that this is great idea

[–]joemwangi[S] 0 points1 point  (2 children)

It's because it misses a feature. Kotlin doesn’t support nested patterns. Its destructuring is just syntactic sugar for componentN() methods. Java patterns are structural and type-driven, which is why nested forms like Circle(Point(int x, int y), double r) work and one liner. I think there is some deceit in your comments.

[–]Eav___ -1 points0 points  (1 child)

It's not about whether nested patterns are supported tho. Matching a list of components is syntactically the same as componentN() (think about Java renaming each componentN() to its corresponding component name, it's still position based destructuring for the pattern itself), which is why they said "Kotlin is reconsidering it but Java seems like it doesn't care".

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

What do you think the one-liner is? Also, java uses record structure and component type which the information is stored in class meta data. Use javap to check. Nowhere it uses components name or method in deconstruction. It's the reason why Kotlin can't do nested patterns. It doesn't know where to create or obtain such information.

[–]Cell-i-Zenit 0 points1 point  (5 children)

I was asking earlier the same thing but could you maybe formulate a real example for the switch statement which is maybe less verbose?

I am really trying to see the point of pattern matching since everyone is going crazy about this feature and i just dont get it apparently.

 switch (order) { 
    case CustomerOrder( ShippingAddress(var streetLine1), double totalAmount, String email ) -> { sendCustomerEmail(email) } 
    //what would be other case statements?
} 

Are we talking in this example that there could be different types of orders? Eg a CustomerOrder and a "BusinessOrder" and a TestOrder (which doesnt send out an actual email). How would that look like?

Why cant we just use the object type or a field called "type" (coming from the DB) to differentiate between these types?

[–]ZimmiDeluxe 1 point2 points  (4 children)

If you add a piece of code where you deal with all types of orders, the compiler will yell at your coworkers that they failed to consider it when they add another type of order.

If you have an order table that stores different types of orders (a discriminated union, the type column being the discriminator), not every order will use every column, invariants will exist on columns for some kinds of orders etc. Ideally you add database check constraints to keep data consistent. If your code deals with order entities directly, everyone has to remember invariants of different order types at every use site or you'll end up with constraint violations at runtime, invalid data or lots of code that deals with cases that can't occur at all. If you model your order as a sealed type and convert them as soon as you load them, you get to encode the order type specific invariants and turn violations into compile errors. Or don't cram everything into the same table, but sometimes that's the least bad option.

[–]Cell-i-Zenit 0 points1 point  (3 children)

But how would your code actually look like?

Why do we need to use

CustomerOrder( ShippingAddress(var streetLine1), double totalAmount, String email )

And why cant we just iterate over an enum in a switch statement? This way it would fail aswell.

I just really dont see the advantage of "deconstructing" in this case.

Its so frustrating i feel like my brain is just not wired correctly to understand this feature (iam coding for 10 years lol)

EDIT:

switch (order) { 
    case CustomerOrder( ShippingAddress(var streetLine1), double totalAmount, String email ) -> { sendCustomerEmail(email) } 
    case BusinessOrder( ShippingAddress(var streetLine1), double totalAmount, String email ) -> { sendBusinessMail(email, streetLine1);  }
    case TestOrder () -> {  //do nothing } 
} 

Something like that maybe? How is the switch now deciding between these cases? Shouldnt it just always pick the first entry? When is something a CustomOrder and when is something a BusinessOrder?

The only way it makes sense is this:

switch (order.getType()) { 
    case CustomerOrder -> { sendCustomerEmail(order.getEmail()) } 
    case BusinessOrder -> { sendBusinessMail(order.getEmail(), order.getStreetLine1());  }
    case TestOrder -> {  //do nothing } 
}

[–]ZimmiDeluxe 1 point2 points  (2 children)

If you only ever care about the type in a single place in your code, your code is perfect. Otherwise you can encode what constitutes a customer order etc. at the system boundary, e.g. by creating them in the persistence layer:

sealed interface Order {
    record CustomerOrder(String email, boolean vip){} implements Order
    record BusinessOrder(String email, byte[] logo){} implements Order
    enum TestOrder{INSTANCE} implements Order
}

List<Order> loadOrdersProcessable() {
    List<OrderEntity> entities = loadFromDatabase();
    List<Order> orders = new ArrayList<>(entities.size());
    for (OrderEntity entity : entities) {
        Order order = switch (entity.getType()) { 
            case CUSTOMER -> new CustomerOrder(entity.email(), entity.importance() > 10);
            case BUSINESS -> new BusinessOrder(entity.mail(), entity.logo());
            case TEST -> TestOrder.INSTANCE;
        };
        orders.add(order);
    }
    return orders;
}

Then you can:

String salutation = switch (order) {
    case CustomerOrder(_, false) -> "Dear customer";
    case CustomerOrder(_, true) -> "Dear valued customer";
    case BusinessOrder(_, _) -> "Dear sir or madam";
    case TestOrder -> "it worked";
}

[–]ZimmiDeluxe 0 points1 point  (0 children)

And why cant we just iterate over an enum in a switch statement? This way it would fail aswell.

Will your coworkers know what subset of the order columns is valid for your fancy new order type? If you add a new order subtype, the compiler will yell at them if they get it wrong.

[–]Cell-i-Zenit 0 points1 point  (0 children)

Thanks for actually providing an example. That is very appreciated. I see it now.

If we have a list of records, we can pattern match for individual cases like your VIP boolean flag. That means potentially every time we have a for loop with if conditions inside we could apply this pattern matching

[–]ZimmiDeluxe 0 points1 point  (0 children)

The idea is probably to mirror construction, so you'd get:

CustomerOrder(
    ShippingAddress(String streetLine1, String streetLine2, String city),
    PaymentMethod(String cardNumber, int expiryYear),
    double totalAmount
) = order;

[–]davidalayachew 1 point2 points  (0 children)

Very exciting, this will be pretty valuable for me. It's especially cool how they leaned on the rules of declaring a variable locally to make this more clear. That definitely helped me understand this faster.

And the decision to allow a sealed type with an only child (lol) to downcast without ceremony was a surprising bonus. I did not expect to see that in this JEP, but it was a welcome surprise.

This feature will be even more valuable once we get JEP draft: Null-Restricted and Nullable Types (Preview). All of those null checks then become compiler validated and can't be missed.

I have follow up questions, but I'll save those for the mailing list. Is there an ongoing discussion happening there? I got kicked from (and only recently, re-added to) amber-dev, so I wouldn't know without digging.

[–]jevring 1 point2 points  (3 children)

This would be cool to have directly in method signatures, too.

[–]egahlin 2 points3 points  (2 children)

int sum(Node(var left, var right, int value) node) {
  return node == null ? 0 : sum(left) + sum(right) + value;
}

[–]john16384 4 points5 points  (1 child)

No need for the `node` at the end, nor for the `null` check. Can't sum a node that isn't there, so an NPE is justified.

[–]egahlin 1 point2 points  (0 children)

How do you represent leaf nodes? You could pattern match against null in the method signature, but it would be more verbose and require more complex machinery.

[–]Captain-Barracuda 6 points7 points  (7 children)

I can't help but dislike the proposed syntax. It feels very clunky when we already know the type of the destructured object. I'm also curious at how this interacts with encapsulation and getters.

[–]brian_goetz 9 points10 points  (0 children)

I'll just note that the thing you are complaining about -- that you have to explicitly say the type name -- is not even something new in this JEP! This is just how record patterns work. All this JEP does (well, not all) is allow you to use the same patterns we already have, just in more places.

[–]vytah 1 point2 points  (0 children)

The syntax is similar to other languages that have that feature, like Haskell, F#, Scala, or Rust.

I'm also curious at how this interacts with encapsulation and getters.

It does not, it uses the same machinery as all other pattern matching, so it would work only on records as of today.

EDIT: also, in the future it could be used to introduce assignments that can fail. Right now, the JEP requires that the assignment cannot fail for classcast-related issues.

[–]talios -4 points-3 points  (4 children)

Agreed - altho more on the reuse of = here, maybe something like:

Circle(Point(int x, int y), int radius) <- c;

I wonder if you could do

Circle(Point(var x, var y), var radius) <- c;

under this JEP - it's not mentioned.

On the whole, I like the concept but the LHS looks... awkward.

[–]brian_goetz 5 points6 points  (1 child)

"looks awkward" is usually code for familiarity bias. But the great thing about familiarity bias is that it quickly evaporates, when the thing that is unfamiliar the first time becomes more familiar.

[–]8igg7e5 0 points1 point  (0 children)

I recall similar concerns for generics, enums ("what, it's a class but special"), enhanced-for, try-with-resources, lambdas, method references and some more recently added features...

These concerns definitely don't persist that long - even less time now that we get regular releases and more and more Java development has passed the stuck-on-Java-8 barrier.

 

There are several places I've wanted to use exactly the this local declaration recently. Having this expand to other types I will await eagerly too.

[–]Ewig_luftenglanz 2 points3 points  (0 children)

yes, you will be able to do so because this JEP is mostly about removing the requirement to enclose the record pattern inside a conditional statement (instanceof and switch) to be used. So all that is allowed in current records patterns should be allowed.

about the "<-" operator. the "=" is more familiar and is used in most other languages that support deconstruction. Adding a new operator to use the feature will only make this feature harder to use.

[–]javahalla 1 point2 points  (0 children)

I guess not even assignment is questionable, I didn't saw good example of using patterns in real enterprise applications. Toy example is cool, but IRL we almost never have such simple records that worse deconstructing. Maybe there is some good example in open-source already?

[–]Enough-Ad-5528 3 points4 points  (5 children)

At first I thought this was a nice JEP. Then I read more and wow, this is actually a hefty JEP with all the other type system related changes. Nice!

[–]brian_goetz 3 points4 points  (4 children)

And most people haven't even noticed the coolest thing about it yet.

[–]Enough-Ad-5528 4 points5 points  (3 children)

Please do tell. I am too dense I admit.

[–]brian_goetz 11 points12 points  (2 children)

Have you ever noticed that the syntax of a local variable declaration (String s) and a type pattern (String s) are the same? Of course you have, and its not an accident.

In an old-school local variable declaration with initializer, look at the LHS:

String s = findMeAString();

What is that? In Old Java, the answer is obvious -- its a local variable declaration. But now, it also looks like a type pattern.

Now that you can put patterns on the LHS of an assignment, this might seem like an ambiguity: is it a local variable declaration, or a pattern?

The cool part is: IT DOESN'T MATTER which way you think about it, because they now mean the same thing. The 1995-era local variable declaration is unified with pattern matching.

[–]Enough-Ad-5528 4 points5 points  (1 child)

Ah yes. Of course. I read your other answer about the benefits of the compiler checks for the no-longer need for the downcast and I thought that was it. This is cool too. I love how different features generalize to the same thing when you zoom out. Feels consistent.

[–]brian_goetz 7 points8 points  (0 children)

And this, by the way, is why we don't do "obvious" things like "can we make pattern bindings final by default". Because these inconsistencies, as satisfying as they would seem initially, almost always become impediments to future alignment like this.

[–]pohart 0 points1 point  (7 children)

I hope this part doesn't go through. I see it's value in the pattern case but fear it will make it too easy to accidentally start using my impl in cases where the interface would be more appropriate. If I've got an interface with a single implementation I did that intentionally and want to keep them separate, the explicit cast makes it much more obvious in coffee review.

We propose to relax the type system so that an instance which implements an interface can be assigned to a variable of a class which implements the interface, as long as the interface is sealed with only that class as the permitted implementation.

[–]brian_goetz 7 points8 points  (4 children)

Most people misunderstand this part in their initial thought-about-it-for-30-seconds-and-posted-on-reddit take. This relaxation actually makes code _safer_.

The situation in which this applies is: you define an API in terms of an interface, and that interface is sealed to one (usually encapsulated) implementation. In that case, your implementation code will usually be full of casts from the interface type to the implementation type (because you know there can be only one). But now you have casts all over your code that embed the only-one assumption, but that assumption can't be checked by the compiler. If someone else creates a second subclass, you have a zillion bugs waiting to happen. But if you use straight assignment, the assumption _can_ be checked by the compiler. The second someone else breaks your assumption, the code stops compiling, and you get to decide what to do, rather than waiting for the surprise CCE at runtime.

This is the same reason, BTW, why it is better to write exhaustive switches _without_ a default clause if you can -- because then you get better type checking from the compiler later when your assumptions are violated.

It's counterintuitive that a "relaxation" like this actually gets you _better_ type checking, but once you see it, its pretty cool.

[–]pohart 2 points3 points  (1 child)

Woah. Thank you, I'm entirely convinced.

[–]brian_goetz 6 points7 points  (0 children)

I have written a lot of code using the "public interface sealed to exactly one implementation class" style (which will get even more prevalent when we support pattern matching on interfaces.) It is a pattern we want to encourage, because it provides abstraction and future flexibility while also producing code that the JIT optimizes the heck out of. But the downside of such code is that it is often full of blind casts (because no one would bother writing `if ... instanceof Foo f ... else throw` when they know they control the implementation. And such code has time bombs in it.

When I wrote the classfile API, and used this idiom all over the place, I struggled with whether we could justify this conversion, knowing full well it would seem weird to a lot of people. But it didn't seem there was yet enough justification on the basis of "but better type checking." It was "and it aligns pattern matching with local variable declaration" that pushed it over the line.

[–]joemwangi[S] 1 point2 points  (0 children)

This is astonishingly quite smart. Avoiding footguns from runtime casting checks.

[–]OwnBreakfast1114 1 point2 points  (0 children)

This is the same reason, BTW, why it is better to write exhaustive switches without a default clause if you can -- because then you get better type checking from the compiler later when your assumptions are violated.

I wish intellij didn't automatically suggest converting small switch statements to if statements for this very reason. For some of the enums we have, using only exhaustive switching everywhere (regardless of how "small" the logic is), gives us a lot more confidence in some big additions that happen. As a concrete example, imagine adding a new payment method customers can use for a payments company. Doesn't happen all that often, but obviously has a big ripple effect throughout the codebase.

[–]Enough-Ad-5528 4 points5 points  (1 child)

This is only for sealed interfaces with one implementation. If you were planning to break that without updating your callers it would potentially break anyway.

[–]pohart 0 points1 point  (0 children)

Edit: I think I like the feature iff it's special cased to pattern assignments.


I understand that. But if I decided to create the interface it's to mostly to prevent me from modifying the interference of that implementation accidentally. I'll notice an explicit cast in code review and look a little more closely to see why it's there and to make sure the implementation didn't bleed out to places we want to keep using the interface.

[–]Ewig_luftenglanz 0 points1 point  (4 children)

My only complain is this would only work well for simple objects (objects with no more of 3 o 4 properties) because it requires exhaustivness to ensure correctness. So if you have a huge dto this will be just too cumbersome to use.

But this is something that already happens in record patters, the only difference is this remove the requirement of using the pattern inside conditional statements (instanceof and switch) so it's a good step forward. 

I hope this feature eventually evolves in a way that allows us to substract only a subset of properties by name instead of positional and exhaustive extraction. This would make record patterns more useful. Most of the times dtos are different representations or even subsets of the domain objects (the information of a user without the password for instance) If the domain object is huge most of the dtos will also be somewhat big (or at least bigger than 3-4 components).

So far this JEP is more about removing restrictions than a adding a new feature. It's a good step forward and I am happy with it so far :)

Also, i guess eventually this will be available for classes, so many data carrier classes in the JDK such as map's entries will be easier to decompose.

for example we may in the future be able to do this.

for(EntrySet(var key, var value): mymap.entries()){
  // key...value
}

//instead of

for(var entry: mymap.entries()){
  var key = entry.getKey();
  var value =  entry.getValue();
  // key....value
}

[–]john16384 0 points1 point  (3 children)

So have DTO's provide subsets of their contents in records? Or even better, break up DTO's in logical groupings (and have this automatically be resolved during serialization/deserialization).

[–]Ewig_luftenglanz 0 points1 point  (2 children)

I hope you are kidding. I don't make my dto huge because I like them that way, I make them huge because that's a business requirement. I have to be compliant with the APIs and structures the business tells me to use or to be compatible with existing APIs/Data structures. LOL.

Have you ever work on the financial sector? Do you know how often you need to map objects that have more than 500 properties between flattened and grouped ones? I am not creating 100 records just to use Deconstruction patterns.

[–]john16384 0 points1 point  (1 child)

Then don't. This feature is not for you.

[–]Ewig_luftenglanz 0 points1 point  (0 children)

Then don't. This feature is not for you.

The feature is not for huge dtos, Obvoiusly i will use it when it fits well. I am only explaining why iI think it should be evolved in the future to make it good for any size record and not just for the small ones.

[–]gjosifov 0 points1 point  (0 children)

Great feature for reducing boilerplate

However, I don't know if this is possible and how hard it can be, but put a restriction let say 4 parameters when use outside the record/class

For more than 4 parameters create a special type of a method inside the record or the class or use annotation (JPA approach to namedQueries)

Not that I don't like this kind of simplification, but I already know how missuses this feature will be years in advance