New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@Builder should get a @Default companion annotation. #1201

Closed
ewirch opened this Issue Sep 26, 2016 · 9 comments

Comments

Projects
None yet
3 participants
@ewirch

ewirch commented Sep 26, 2016

I want final fields to receive a default value, if they not have been set at the builder. Let's say I'd like to have a nonnull enum field. Example:

@Data
@Builder
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class Configuration {
    @Default(Automation.NONE)
    Automation automation;

    public enum Automation {
        INTERNAL,
        EXTERNAL,
        NONE
    }
}

assert Configuration.builder().build().getAutomation() == Configuration.Automation.NONE;
@Maaartinus

This comment has been minimized.

Contributor

Maaartinus commented Sep 26, 2016

Your case requires something like

public @interface Default {
    Configuration.Automation value();
}

There's no way to specify an annotation taking an arbitrary object (and there are primitives, too). Lombok might be able to hack its way around it, but it's surely not easy and not nice. See this workaround for a simple solution.

@ewirch

This comment has been minimized.

ewirch commented Sep 26, 2016

I see your point. That's a nice workaround. But it does not work for immutable classes.

@Maaartinus

This comment has been minimized.

Contributor

Maaartinus commented Sep 26, 2016

You're right. And immutable classes are the main target for Builder. This might work as lombok should complete the class with missing stuff:

@Value
@Builder(builderClassName="Builder")
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class Configuration {
    public static class Builder {
        Automation automation = Automation.NONE;
    }

    Automation automation;

    public enum Automation {
        INTERNAL,
        EXTERNAL,
        NONE
    }
}

In case it doesn't, @FieldDefaults might be the culprit (do they influence the nested class?).

@ewirch

This comment has been minimized.

ewirch commented Sep 26, 2016

Ah, nice, but it's easy to misspell the field name in the builder, or forget to rename it when you rename the parent field. Nothing will warn you.

This is my current minimal, working, compiler checked workaround (not using @builder):

@RequiredArgsConstructor
@Data
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
class Configuration {
    Automation automation;

     public enum Automation {
        INTERNAL,
        EXTERNAL,
        NONE
    }

    static Builder builder() {
        return new Builder();
    }

    @Accessors(fluent = true)
    @Setter
    @FieldDefaults(level = AccessLevel.PRIVATE)
    public static class Builder {
        Configuration.Automation automation = Configuration.Automation.NONE;

        GlobalConfiguration build() {
            return new Configuration(automation);
        }
    }
}
@ewirch

This comment has been minimized.

ewirch commented Sep 27, 2016

Lombok does byte code rewriting, doesn't it? So how about you remove final field initialization, if the field is annotated @DefaultVale?

Let's pick up the example from previously:

@Builder
class Configuration {
    private final int CONSTANT = 3;
    @DefaultValue
    private final Automation automation = Automation.NONE;
}

The decompiled bytecode would look similar to:

class Configuration {
    private final int CONSTANT = 3;
    private final Automation automation;

    static class ConfigurationBuilder {
        private final Automation automation = Configuration.Automation.NONE;

        // setters, and build method        
    }

    // constructor
}

Will this work?

@Maaartinus

This comment has been minimized.

Contributor

Maaartinus commented Sep 27, 2016

Ah, nice, but it's easy to misspell the field name in the builder, or forget to rename it when you rename the parent field. Nothing will warn you.

It should become an unused private field. But I agree that it's neither fool-proof nor nice.

Lombok does byte code rewriting, doesn't it?

No, it does AST manipulations.

So how about you remove final field initialization, if the field is annotated @DefaultVale?

Not me (I'm not a project owner and I don't have enough spare time to do this). But yes, moving the initialization to the builder is the right thing.

Will this work?

Apart from that the field shouldn't be final in the builder, it sounds good.

I'm not sure, if the annotation is needed. You obviously want to keep CONSTANT constant and move the annotated initialization to the builder. But a constant should actually be static final which excludes it from the builder already. There are uses for a non-static final field with an initializer expression, but there are too rare to care about. IMHO what your annotation does should be the default behavior. For the current behavior, I'd suggest @Builder.Exclude.

@ewirch

This comment has been minimized.

ewirch commented Sep 28, 2016

I agree. @Builder.Exclude will be the better approach.

@rzwitserloot

This comment has been minimized.

Owner

rzwitserloot commented Feb 20, 2017

Moving the initialization to the builder has all sorts of hairy issues because you're messing with scope. What if the initialization refers to another field, for example?

I think the best way forward is to either silently take the initializer, or possibly only if there's some sort of @Default annotation, and move it to a sibling constant (a static final field), and then have the builder refer to that.

@rzwitserloot

This comment has been minimized.

Owner

rzwitserloot commented Feb 20, 2017

Okay, here's the plan:

we turn this:

@Builder @Value
public class Whatever {
    long created = System.currentTimeMillis();
    String name = "anonymous";
}

into:

public class Whatever {
    static final long $default$created() {
        return /*moved expr */System.currentTimeMillis();
    }

    static final String $default$name() {
        return /* moved expr */ "anonymous";
    }

    private final long created = $default$created();
    private final String name = $default$name();

    public static class WhateverBuilder {
        private long created;
        private boolean created$set;
        private String name;
        private boolean name$set;

        public created(long c) {
            created = c;
            created$set = true;
        }
        // all the other builder stuff

        public Whatever build() {
            return new Whatever(created$set ? created : $default$created(), name$set ? name : $default$name());
    }
    //builder() method here.

}

By using a static method we do NOT mess with invocation order/time based stuff such as initializing a 'created' value.

Open issues:

  • What about intentional constant stuff? For example, in the above snippet, what if there shouldn't even BE a created(long) method in the builder, it should remain a final field whose expression is initialized by its initializer expression during construction and that's that, it's not otherwise settable?

We could make an annotation for that. Something like @Builder.Constant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment