DEV Community

Cover image for Use Immutable Objects
Ovid
Ovid

Posted on

Use Immutable Objects

Immutable Objects

I’ve been spending time designing Corinna, a new object system to be shipped with the Perl language. Amongst its many features, it’s designed to make it easier to create immutable objects, but not everyone is happy with that. For example, consider the following class:

class Box {
    has ($height, $width, $depth) :reader :new;
    has $volume :reader = $width * $height * $depth;
}

my $original_box = Box->new(height=>1, width=>2, depth=>3);
my $updated_box  = $original_box->clone(depth=>9);  # h=1, w=2, d=9
Enter fullscreen mode Exit fullscreen mode

Because none of the slots have a :writer attribute, there is no way to mutate this object. Instead you call a clone method, supplying an overriding value for the constructor argument you need to change. The $volume argument doesn’t get copied over because it’s derived from the constructor arguments.

But not everyone is happy with this approach. Aside from arguments about utility of the clone method, the notion that objects should be immutable by default has frustrated some developers reading the Corinna proposal. Even when I point out just adding a :writer attribute is all you need to do to get your mutability, people still object. So let’s have a brief discussion about immutability and why it’s useful.

But first, here’s my last 2020 Perl Conference presentation on Corinna (at the time, called "Cor").

The Problem

Imagine, for example, that you have a very simple Customer object:

my $customer = Customer->new(
    name      => "Ovid", 
    birthdate => DateTime->new( ... ),
);
Enter fullscreen mode Exit fullscreen mode

In the code above, we’ll assume the $customer can give us useful information about the state of that object. For example, we have a section of code guarded by a check to see if they are old enough to drink alcohol:

if ( $ovid->old_enough_to_drink_alcohol ) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

The above looks innocent enough and it’s the sort of thing we regularly see in code. But then this happens:

if ( $ovid->old_enough_to_drink_alcohol ) {
    my $date = $ovid->birthdate;
    ...
    # deep in the bowels of your code
    my $cutoff_date = $date->set( year => $last_year ); # oops!
    ...
}
Enter fullscreen mode Exit fullscreen mode

We had a guard to ensure that this code would not be executed if the customer wasn’t old enough to drink, but now in the middle of that code, due to how DateTime is designed, someone’s set the customer birth date to last year! The code, at this point, is probably in an invalid state and its behavior can no longer be considered correct.

But clearly no one would do something so silly, would they?

Global State

We’ve known about the dangers of global state for a long time. For example, if I call the following subroutine, will the program halt or not?

sub next ($number) {
    if ( $ENV{BLESS_ME_LARRY_FOR_I_HAVE_SINNED} ) {
        die "This was a bad idea.";
    }
    return $number++;
}
Enter fullscreen mode Exit fullscreen mode

You literally cannot inspect the above code and tell me if it will die when called because you cannot know, by inspection, what the BLESS_ME_LARRY_FOR_I_HAVE_SINNED environment variable is set to. This is one of the reasons why global environment variables are discouraged.

But here we’re talking about mutable state. You don’t want the above code to die, so you do this:

$ENV{BLESS_ME_LARRY_FOR_I_HAVE_SINNED} = 0;
say next(4);
Enter fullscreen mode Exit fullscreen mode

Except that now you’ve altered that mutable state and anything else which relies on that environment variable being set is unpredicatable. So we need to use local to safely change that in the local scope:

{
    local $ENV{BLESS_ME_LARRY_FOR_I_HAVE_SINNED} = 0;
    say next(4);
}
Enter fullscreen mode Exit fullscreen mode

Even that is not good because there’s no indication of why we’re doing this but at least you can see how we can safely change that global variable in our local scope.

ORMs

And I can hear your objection now:

“But Ovid, the DateTime object in your first example isn’t global!”

That’s true. What we had was this:

if ( $ovid->old_enough_to_drink_alcohol ) {
    my $date = $ovid->birthdate;
    ...
    # deep in the bowels of your code
    my $cutoff_date = $date->set( year => $last_year ); # oops!
    ...
}
Enter fullscreen mode Exit fullscreen mode

But the offending line should have been this:

    # note the clone().
    my $cutoff_date = $date->clone->set( year => $last_year );
Enter fullscreen mode Exit fullscreen mode

This is because the set method mutates the object in place, causing everything holding a reference to that object to silently change. It’s not global in the normal sense, but this action at a distance is a source of very real bugs.

It’s a serious enough problem that DateTime::Moonpig and DateTimeX::Immutable have both been written to provide immutable DateTime objects, and that brings me to DBIx::Class, an excellent ORM for Perl.

As of this writing, it’s been around for about 15 years and provides a component called DBIx::Class::InflateColumn::DateTime. This allows you to do things like this:

package Event;
use base 'DBIx::Class::Core';

__PACKAGE__->load_components(qw/InflateColumn::DateTime/);
__PACKAGE__->add_columns(
  starts_when => { data_type => 'datetime' }
  create_date => { data_type => 'date' }
);
Enter fullscreen mode Exit fullscreen mode

Now, whenever you call starts_when or create_date on an Event instance, you’ll get a DateTime object instead of just the raw string from the database. Further, you can set a DateTime object and not worry about your particular database’s date syntax. It just works.

Except that the object is mutable and we don’t want that. You can fix this by writing your own DBIx::Class component to use immutable DateTime objects.

package My::Schema::Component::ImmutableDateTime;

use DateTimeX::Immutable;
use parent 'DBIx::Class::InflateColumn::DateTime';

sub _post_inflate_datetime {
    my ( $self, @args ) = @_;
    my $dt = $self->next::method(@args);
    return DateTimeX::Immutable->from_object( object => $dt );
}

1;
Enter fullscreen mode Exit fullscreen mode

And then load this component:

__PACKAGE__->load_components(
    qw/+My::Schema::Component::ImmutableDateTime/
);
Enter fullscreen mode Exit fullscreen mode

And now, when you fetch your objects from the database, you get nice, immutable DateTimes. And it will be interesting to see where your codebase fails!

Does all of this mean we should never use mutable objects? Of course not. Imagine creating an immutable cache where, if you wanted to add or delete an entry, you had to clone the entire cache to set the new state. That would likely defeat the main purpose of a cache: speeding things up. But in general, immutability is a good thing and is something to strive for. Trying to debug why code far, far away from your code has reset your data is not fun.

Top comments (1)

Collapse
 
grinnz profile image
Dan

As I probably said back in these discussions, I see it as two distinct types of objects. Objects that represent a "thing", which are best treated as immutable, and objects that represent "state", which should be mutable. I use both of these types quite commonly, so focusing too much on one type seems counterproductive to me (though the points you bring up are very important, and why I love the Time::Moment immutable API).