• The cover of the 'Perl Hacks' book
  • The cover of the 'Beginning Perl' book
  • An image of Curtis Poe, holding some electronic equipment in front of his face.

Why Do We Want Immutable Objects?

minute read



Find me on ... Tags

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

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.

The Problem

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

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

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 ) {
    ...
}

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!
    ...
}

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++;
}

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);

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);
}

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!
    ...
}

But the offending line should have been this:

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

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' }
);

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;

And then load this component:

__PACKAGE__->load_components(
    qw/+My::Schema::Component::ImmutableDateTime/
);

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.

Please leave a comment below!

Full-size image


If you'd like top-notch consulting or training, email me and let's discuss how I can help you. Read my hire me page to learn more about my background.


Copyright © 2018-2024 by Curtis “Ovid” Poe.