Immutables, Part 2

Immutables, Part 2

July 8, 2019

Last time, we took a first look at the Immutables library, focusing on the basics of how to generate and use an immutable object. Here, we’ll take a look at how we can use some useful features to make our immutables a bit nicer to work with!

Basic Usage

In part 1, we used a toy example of an image editing app to highlight where our code would benefit from immutability. Using Immutables, we ended up with this Color class, which is used to generate an ImmutableColor class:

@Value.Immutable
public abstract class Color {
    public abstract String rgb();

    public Color lighten() {
        return ImmutableColor.builder()
            .rgb(/* math */)
            .build();
    }
}

This works perfectly fine, but we’d like to have some more robust and easier-to-use models.

Factory Methods

First, it’s a little annoying to actually construct our Color objects. That’s because we’re forced to use a builder—builders are nice when there’s a lot of arguments or configuration, but we only have a single RGB value.

We can generate a static factory method as an alternative way to create ImmutableColor instances. To do this, we tag the attributes that we care about with a @Value.Parameter annotation:

@Value.Immutable
public abstract class Color {
    @Value.Parameter
    public abstract String rgb();

    // ...
}

This tells the annotation processor to create a static factory method, named of, on the generated class. We now have a second way to create an ImmutableColor instance:

Color black = ImmutableColor.of("000000");

We can use our new factory method inside lighten:

@Value.Immutable
public abstract class Color {
    public abstract String rgb();

    public Color lighten() {
        return ImmutableColor.of(/* math */);
    }
}

Derived Attributes

Right now, our colors are represented as RGB values. There are alternate representations as well, such as HSV—hue, saturation, value. HSV is a bit nicer to work with for color manipulation, so let’s represent our color in both RGB and HSV.

To do this we apply some math to our RGB value and get our three HSV values.

@Value.Immutable
public abstract class Color {
    @Value.Parameter
    public abstract String rgb();

    public List<Double> hsv() {
        return /* math */
    }

    // ...
}

We can now represent our colors as HSV values! Let’s now make our hsv method an attribute.

Last time, we said that abstract, zero-argument methods with a non-void return type (such as rgb) are attributes. We can also have attributes that are non-abstract methods. However, these non-abstract methods require an annotation to distinguish them from methods that aren’t intended to be attributes (such as toString).

In this case, we always want to compute the HSV value for our color—we want a derived attribute. We can make one with the @Value.Derived annotation. Each time we create an ImmutableColor instance, that instance will compute and memoize its hsv value.

@Value.Immutable
public abstract class Color {
    @Value.Parameter
    public abstract String rgb();

    @Value.Derived
    public List<Double> hsv() {
        return /* math */
    }

    // ...
}

Lazy Attributes

We’ve now defined three Color methods:

  • rgb, required to create an ImmutableColor instance
  • hsv, computed once from rgb and stored upon creation
  • lighten, computes a color that is 10% lighter

hsv is always the same value for a Color instance, so it’s natural to turn it into an attribute. lighten is also always the same value for a Color instance, so let’s turn that into an attribute as well!

Unlike hsv, however, we don’t want to compute and memoize lighten each time we create a Color.

One, we won’t call lighten very often, so it’s not necessary to precompute it.

Two, we’ll actually end up in an infinite loop! Because lighten returns a Color, the computed value will also need to need to compute and memoize its own lighten value, and so on.

Instead of a derived attribute, we want a lazy attribute. This is done with the @Value.Lazy annotation. The method will compute its value when it gets called for the first time, then memoize the result for future calls. Lazy attributes are great for expensive computations.

@Value.Immutable
public abstract class Color {
    @Value.Parameter
    public abstract String rgb();

    @Value.Derived
    public List<Double> hsv() {
        return /* math */
    }

    @Value.Lazy
    public Color lighten() {
        return ImmutableColor.of(/* math */);
    }
}

As seen here, attributes can also be collections (and arrays). Immutables supports List, Map, and Set, as well as a few more types. By default, collections are backed by unmodifiable collections. If you’re using Guava, they’ll be backed by Guava’s immutable collections instead.

Default Attributes

Let’s add an alpha channel to our colors. Normally, we want our colors to have an alpha value of 1—fully opaque. However, when desired, we should be able to create colors with other alpha values. To do this, we make alpha a default attribute.

@Value.Immutable
public abstract class Color {
    @Value.Parameter
    public abstract String rgb();

    @Value.Derived
    public List<Double> hsv() {
        return /* math */
    }

    @Value.Default
    public double alpha() {
        return 1;
    }

    @Value.Lazy
    public Color lighten() {
        return ImmutableColor.of(/* math */);
    }
}

Copying Immutables

Sometimes, we want to create a copy of an immutable object with a few attributes changed. For example, given an arbitrary color (with an arbitrary alpha value), we might want to determine its base, fully-opaque color.

We could do this by creating an entirely new object from scratch. This isn’t so bad for our Color class, but it could get quite tedious if we had a lot of attributes to copy. This code would also become brittle if we add additional attributes to Color in the future.

// This is brittle!
Color color = // an arbitrary color
Color opaqueColor =
    ImmutableColor.builder()
        .rgb(color.rgb())
        .alpha(1)
        .build();

Instead, builders have a from method that copies all values from our desired object. We can then change any attributes that we want.

Color color = // an arbitrary color
Color opaqueColor =
    ImmutableColor.builder()
        .from(color)
        .alpha(1)
        .build();

A final way of copying immutables is via wither methods. For each settable attribute, our generated immutable objects (e.g. ImmutableColor) have a with* method. Coupled with copyOf, we have another way of copying our immutables.

Color color = // an arbitrary color
Color opaqueColor = 
    ImmutableColor.copyOf(color).withAlpha(1);

These wither methods attempt to copy with structural sharing.

We’ve now taken a look at a few basic features of the Immutables library, but there are lots more. In part 3, we’ll take a look at some more advanced features that allow us to streamline our immutables even more!

comments powered by Disqus