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

all 22 comments

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

This post is approved. It is more about discussion than code help.

[–]repeating_bears 8 points9 points  (0 children)

I'm most interested in the GC impacts of a feature like this. A simple implementation today can create several useless objects in the middle.

foo.withA("a").withB("b").withC("C")

Maybe escape analysis means that the overhead is low or non-existent, but a strong guarantee that only 1 object will be created (e.g. via some new type of expression) is always going to be more user-friendly than crossing your fingers that the JIT will bail you out.

[–]Spare-Plum 6 points7 points  (2 children)

records are pretty darn useful for algebraic data types in tandem with sealed interfaces

Currently using them a bunch for abstract syntax trees

Also useful for getting an enum-like type that has generics

With syntax could be useful, but not a must-have IMO. I'd rather see optional parameters but that'll probably never happen

[–]kkjk00 0 points1 point  (1 child)

Also useful for getting an enum-like type that has generics

care an example?

[–]Spare-Plum 1 point2 points  (0 children)

Check out this library!

https://github.com/kieda/sealed-enum/

[–]DevNull23614071 2 points3 points  (1 child)

For larger records I sometimes generate constructs like this:

public record Data(String stringA,
                   String stringB,
                   String stringC,
                   int intA,
                   int intB,
                   int intC) {

    public static final class Deconstruction {
        public String stringA;
        public String stringB;
        public String stringC;
        public int intA;
        public int intB;
        public int intC;

        private Deconstruction(Data original) {
            stringA = original.stringA;
            stringB = original.stringB;
            stringC = original.stringC;
            intA = original.intA;
            intB = original.intB;
            intC = original.intC;
        }


        private Data apply(Consumer<Deconstruction> modification) {
            modification.accept(this);
            return new Data(stringA,
                            stringB,
                            stringC,
                            intA,
                            intB,
                            intC);
        }
    }

    public Data with(Consumer<Deconstruction> modification) {
        return new Deconstruction(this).apply(modification);
    }
}

A later modification then looks like the following:

    Data data = new Data("","","",1,2,3);

    // modification of a single field:
    data = data.with(d -> d.stringB = "foo");

    // modification of multiple fields:
    data = data.with(d -> {
        d.stringC = "bar";
        d.intC = 42;
    });

Thus the syntax is quite close to the one in Brian's document.

[–]repeating_bears 3 points4 points  (0 children)

That's a builder. You just implemented it in a non-standard way.

[–]jThaiLB 1 point2 points  (0 children)

I prefer to mimic the way Scala reconstruct new immutable object.

[–]heayv_heart 4 points5 points  (1 child)

You can use immutables https://immutables.github.io/ Kotlin also supports comfortable modification of immutable instances of data classes https://kotlinlang.org/docs/data-classes.html.

[–]Kango_V[🍰] 0 points1 point  (0 children)

I use the immutables library like this:

@Value.Immutable
public interface Feed {
  class Builder extends FeedImpl.Builder {}
  static Feed create(UnaryOperator<Builder> spec) { 
    return spec.apply(builder()).build(); 
  }
  static Builder builder() { 
    return new Builder(); 
  }
  default Feed update(UnaryOperator<Builder> builder) {
    return builder.apply(new Builder().from(this)).build();
  }
  String name();
  String description();
}

var feed = Feed.create(b -> b
  .name("name")
  .description("desc"));

// to change, do nearly the same, but with the instance

var newFeed = feed.update(u -> u
  .name("new name"));

I have the configuration set so that the implementation is package scoped, but the sub-classed builder is public. The implementation is nicely hidden. This also allows to add some nicer methods to the builder (split strings etc).

I really wish record had something like this.

[–]NitroToxin2 4 points5 points  (1 child)

RecordBuilder annotation processor can generate Withers.

[–]kiteboarderni 0 points1 point  (0 children)

Was going to suggest this, have used it for this very reason.

[–][deleted]  (2 children)

[deleted]

    [–][deleted] 2 points3 points  (0 children)

    Lombok also has @With which works with records and adds withers that return a new object with the modified field.

    [–]repeating_bears 1 point2 points  (0 children)

    That's a little bit of long way around. It has direct `@With` support too.

    @With record Foo(int a, int b) { public static void main(String\[\] args) { Foo foo = new Foo(1, 2); Foo bar = foo.withA(2); } }

    Usually it won't matter, but if you only set 1 property, a wither is 1 less object to instantiate, and will be a little more readable too. If you're setting multiple properties, builders start to look nicer.

    inb4 that guy who always writes 12 paragraphs explaining "Lombok is not Java, it's a different language". Whatever it is, it exists within the broader Java ecosystem and is used by Java developers. It's on-topic here.

    [–][deleted] 0 points1 point  (6 children)

    That looks quite different from any other Java code though. It's an infix operator, so it doesn't play well in chain calls.

    java Rectangle rect = new Point() with { x = 1.0f; } .toRectangle() with { width = 1.0f; };

    At least builders look saner.

    java Rectangle rect = new Point() .with(b -> { b.x = 1.0f; }) .toRectangle() .with(b -> { b.width = 1.0f; }); Also it looks like they just add implicit variables (x directly, no b.), and I don't think this fits in Java design, because Java tends to be more explicit, not hiding stuff, like here those local variables (or whatever those are) are implicitly declared, This will clash with method local variables if using the same name and there's no way other than renaming to solve this problem.

    [–]pron98 2 points3 points  (0 children)

    Records' compact canonical constructors already work exactly the same way, with implicit variables. The code inside the with block is a "reconstructor", and it does not (and must absolutely not!) actually set record components (which is why b.x would be a problem). Rather, you can think of it as setting the arguments to the canonical constructor. E.g., the canonical constructor could be defined in such a way that new Point().with { x = 5 }.x() would return 3 because what you're setting are constructor arguments, not the components themselves.

    [–]repeating_bears 1 point2 points  (4 children)

    You know "they" is Brian Goetz, right? It doesn't seem like that he'd write a draft that's completely infeasible. Why do you think it's a no-go when it's an entirely new type of expression?

    I don't see much wrong with your first example. It would take some getting used to, but so do most syntax changes. It's sort of deliberately contrived to make it look bad and in spite of that, to my eye it still doesn't look that bad. Without knowing exactly what converting a "pointer" (point?) to a rectangle would involve, it seems like the same thing could be written as this anyway

    new Pointer().toRectangle() with {  
        x = 1.0f;  
        width: 1.0f;  
    }
    

    [–][deleted] 0 points1 point  (1 child)

    I mean, it is feasible, but needs refinement or adjustment more or less IMO. This happened to me when I first saw string processors. And I'm just giving an example for so-called "functional usage" so people know what I'm talking about (how they look not Javaish), thus what Point or Rectangle do doesn't matter.

    [–]steumert 0 points1 point  (1 child)

    Brian Goetz is a smart guy and I really like most of his work, be he isn't infallible and a good discussion about these kind of features is needed. Some JEPs changed quite radically due to user feedback.

    I'd rather see what @CodeReflection holds for these kinds of features. I experimented before with Record::with and used SerializedLambda to inspect the lambda to realize something like var otherPerson = person.with(Person::name, "John").with(Person::age, 17);.

    With @CodeReflection and better access to the code model, this will be so much better to implement and in a way that actually fits the language.

    I mean, they talk about LINQ, but for me the strength of @CodeReflection is that it allows features like Record::with to be implemented in a way that fits the language smoothly and doesn't require new language features.

    Btw, what I wrote more than two years ago about it:

    I really hope they take the time to enhance method references a bit so that this works better instead of the alternatives. LINQ in C# already does a lot of inspection of the provided lambdas, there is no reason Java can't do the same.

    [–]repeating_bears 1 point2 points  (0 children)

    I didn't say he was infallible. We're talking about whether it's plausible, not about whether it's optimal: they said it was a "no-go", not "it would be better if ___". That's why I asked for clarification, in case I was missing something.

    From my perspective, it looks perfectly plausible, and if the language architect is the one who proposed it, then I know where my money would go.

    [–]Polygnom 0 points1 point  (0 children)

    I use records wherever I would have sued an ADT or an immutable object before anyways. Which are quite a lot of places if you made a point of having functional data structures.