Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 20, 2021 06:54 am GMT

Use Immutable Objects

Immutable Objects

Ive been spending time designing Corinna, a new object system to be shipped with the Perl language. Amongst its many features, its 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 doesnt get copied over because its 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 lets have a brief discussion about immutability and why its useful.

But first, heres 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( ... ),);

In the code above, well 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 its 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 wasnt old enough to drink, but now in the middle of that code, due to how DateTime is designed, someones 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

Weve 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 were talking about mutable state. You dont 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 youve 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 theres no indication of why were 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 isnt global!

Thats 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. Its not global in the normal sense, but this action at a distance is a source of very real bugs.

Its 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, its 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, youll 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 databases date syntax. It just works.

Except that the object is mutable and we dont 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.


Original Link: https://dev.to/ovid/use-immutable-objects-4pbl

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To