What’s all this “immutable date” stuff, anyway?

A simple but real-world example of the difference between a mutable and immutable object, and why you should care…

If you hang out on any sort of programming forums you have no doubt encountered “The Great Mutable vs. Immutable Debate”. While I’m sure you know what the words mean, if you are new to programming or don’t have a strong Computer Science background it might not be obvious to you whether this is an important concept to be concerned with, or just more “architecture astronaut” purists arguing some obscure fine point.

To help you answer that for yourselves, I’m going to show you the difference between the two using two popular Php DateTime libraries — Carbon and Chronos, and then demonstrate the danger of using the mutable one of those.

You have probably used Carbon — it is a wonderful library put together by Brian Nesbitt that takes all the pain out of working with dates. It has got one “short-coming”, if you will — it is built on top of the DateTime object. Because of the mutability of DateTime and the way Carbon is coded, you have the potential for this tricky bug:

$now = \Carbon\Carbon::createFromDate(2017, 6, 13);
echo $now; // 2017-06-13 04:11:44
$twomonthslater = $now->addMonths(2);
echo $twomonthslater; // 2017-08-13 04:11:44
echo $now; //   2017-08-13 04:11:44  Uh-oh!!

There’s the problem, in a nutshell. Carbon allows you to change the value of your instance “in situ”, meaning that if you lose track of changes you’ve made, you end up working with what is essentially a different instance than what you started with.

Stated more directly— you are looking at a variable called $now, but $now isn’t “now” anymore, and you may have absolutely no idea that it changed. If your Carbon instance is getting passed in and out of functions it may be getting changed completely out of sight from the main code you are working with. That will kill a few hours of your time tracking it down — assuming you even realize the bug is happening.

Compare the code above to the same thing with Chronos:

$chronosNow = Cake\Chronos\Chronos::create(2017,06,13);
echo $chronosNow; // 2017-06-13 04:11:44

$chronosTwomonthslater = $chronosNow->modify('+2 months');
echo $chronosTwomonthslater; // 2017-08-13 04:11:44

echo $chronosNow; // 2017-06-13 04:11:44

You see that a Chronos instance can’t be changed unless you do it explicitly with $chronosNow = $chronosNow->modify(‘+2 months’); because it is based on DateTimeImmutable. This prevents you from accidentally changing your values while running your code where you don’t want to.

When you read “immutability helps reduce bugs”, this is what people are talking about.

That’s helpful to know if you’ve been working with Carbon a lot and didn’t realize it worked like that, but let’s think for a moment about the code we write ourselves. Here’s a mockup of some common code (not from any particular framework, but easy to follow) that was probably quite straightforward and clean when it start, but change requests over time have rendered it quite “smelly”:

$user = UserModel::new(['first_name' => 'Jeff', 'last_name' => 'Madsen']);

$user->cleanName();
echo $user->first_name; // null or error

// UserModel.php
function cleanName(){
$this->full_name = $this->first_name . ' ' . $this->last_name;
$this->save();
}

function save(){
unset($this->first_name, $this->last_name);
parent::save();
}

I’ve created an instance of a User “Jeff Madsen” but then during the normal process of preparing this user, I’ve changed my values and no longer have the instance I started with. There is not much we can do about mutability in Php, but if we at least follow the same patterns we would use in an immutable language we can avoid common pitfalls. Compare:

$user = UserModel::new(['first_name' => 'Jeff', 'last_name' => 'Madsen']);
$user->full_name = $user->createFullName();
$user->cleanInitialValues();
echo $user->first_name; // null or error, of course
$user->save();

// UserModel.php
function createFullName(){
return $this->first_name . ' ' . $this->last_name;
}

function cleanInitialValues(){
unset($this->first_name, $this->last_name);
}

Yes, it still throw null or error, but now you’d have to be a pretty big doofus to not spot immediately *why* it is failing, and quickly fix it. Of course this is a very contrived example, the concept behind it is simple and can be extended to real code.

You’ll find that a lot of the code you write that causes mutability problems also tends to violate the Single responsibility principle and makes it very difficult to test, two more signs that you probably have code smells and complications you would do better to clear out. Have a look at one of your messy services and see if you can’t spot some places where this is at the root of your troubles.

Hope that helps!