Immutability with Moo(se)
Would it be doable, to have a module to activate immutability with Moo(se)? Something like
with MooX::Immutable;
which would make all the objects from the class immutable.
As a short introduction, immutability is when objects never change: instead of modifying data in place, you must make a copy of it and override the values you want to change. Thus when you use an object at any point in your code, it will always be exactly the same: you are never taking the chance that some method modified it without you realizing.
I've come across the concept of immutability a fair few times recently:
- chromatic explains that it "reduces the possibility that someone will manipulate the object and make it invalid" and thus "you get to remove a lot of error checking code because wtihin the API you can assume that the object is always in a valid state"
- DateTime::Moonpig is immutable because mutability is dangerous.
- Tobyink says "Immutable objects should be favoured over mutable ones."
- nothingmuch wrote (in 2009) three excellent articles about immutability, where s.he explains various reasons why immutability is good, and indicates ways to achieve it with Perl...
...but now we have Moo(se) so I'm hoping there can be a simpler way.
If I understood well, MooX::Immutable would need to clone every input and output:
- input: all attributes that are passed as parameters to the contructor
- output: all accessors return a clone of the corresponding attribute
And I think that... that'd be it? Am I missing something?
Would that be something doable? useful?
PS: after a byterock's first comment, I thought I should clarify what immutability is and what a reasonable goal could be.
Immutability is a way of programming, where you never modify a variable that has been assigned nor an object that has been created. It's generally considered a "good practice". Some languages like Haskell and Erlang are immutable by default: once a variable has some value it cannot change at all anymore. Clojure provides immutable data structures too, so it's easy to keep everything immutable. To understand why that's good, read some of the links I indicated above.
In Perl it's not that simple, because Perl is flexible. In the same way that Moose made it much easier to produce standard and clean object-oriented code, I'm hoping that we can come up with some tool(s) to facilitate the production of immutable code.
But the difficulty is that to produce immutable code, objects need to have all attributes immutable. And CPAN makes this difficult because not many modules will guarantee that they do everything immutable (for instance DateTime is not immutable; DateTime::Moonpig is the immutable version of DateTime).
Moose had a similar problem: how do you subclass a non-Moose class? And they came up with a solution: MooseX::NonMoose. Can we come up with solutions for immutability?
There seem to be two ways to go about it. Two solutions:
1. Try to only use immutable modules from CPAN... or use them, but don't use the methods that mutate state (if you can figure out and then remember which ones they are).
2. Clone all object inputs (constructor parameters) and outputs (accessor return values) so that the object cannot be modified (in theory that's perfect).
Toby pointed out the problem with solution 2:
- not all objects can be cloned easily (but most can - in the same way that not all classes can be used with MooseX::NonMoose, but most can)
- what happens when someone uses a mutable method on a DateTime attribute?
Well, it mutates their clone of the attribute but not the attribute itself. Which is what we want to protect immutability, but it could be confusing for the user. I'm still thinking though, that if you advertise your class as being immutable, as in "You won't be able to modify this object - accessors will only return clones of the attributes", users should be able to figure it out. But yeah, it's no ideal...
For solution 1 even though it's not perfect we could build some tools to help, like:
- a Moo(se)X module that makes ALL scalar, hashref and arraref attributes immutables (with Sub::Trigger::Lock)
- a module that makes it easy to test for immutability, so more modules can guarantee immutability on CPAN.
In an ideal world we could "immutabilize" an object, so that any attempt to modify it throws an exception. For example "$datetime->add_duration()" would throw an exception.
I have no idea whether this is possible...
If you make the assumption that all access to attributes is via accessor methods, and nobody is going to sneakily look at
$instance->{"attr"}
, then immutability is not that difficult to achieve.Firstly, make sure all attributes are declared as
is => "ro"
. That's half the battle already; attributes which are strings, numbers, booleans, etc will now be reasonably protected.The remaining gap is references. Although the reference itself will be read-only, the thing being referred to (probably an array or hash) is still mutable. For example, for a read-only attribute which is an arrayref, we can still do this:
Note that this is exactly where the "always in a valid state" problem that chromatic mentions comes in. Perhaps we validated that
attr
was an arrayref of integers in the constructor; thatpush
statement could be adding any old data onto the array.The Perl internals have the ability to mark an array or hash as immutable, though this is not really documented. Const::Fast is one module that exposes this functionality, though the API of Const::Fast is not especially convenient for applying read-onlyness to Moose/Moo object attributes. Sub::Trigger::Lock is a module I wrote that is perhaps a little more convenient for use with object attributes. (I perhaps need to better document the gotcha that triggers aren't executed when an attribute value is provided by the default/builder sub.)
Another approach would be to simply not have any attributes which are hashrefs or arrayrefs. Instead, make the attributes be some kind of blessed set/collection/bag objects which provide their own accessor methods. List::Objects::WithUtils is a good place to start if you want to pursue this approach. List::Objects::Types even provides some nice coercions to List::Objects::WithUtils objects from plain old arrayrefs and hashrefs.
Oh, that's another way to go about it: making attributes read-only. But that means that you cannot use any object that's mutable. For instance, don't use DateTime objects as attributes because they are mutable. That might restrict us a bit. Plus it means you have to check whether all the attributes you use are mutable, and most documentation doesn't state that clearly.
By cloning the attributes deeply (for instance with MooseX::Clone), you ensure that no mutability can happen. If the user does
$object->attribute->some_method_that_mutates_the_attribute()
then they have only modified the clone, not the attribute itself.
Is that a better approach?
There's also the problem of the inputs. If the user gives you some DateTime attribute and then they modify it, there's a problem. Example:
So we would need to also clone the constructor parameters.
Right?
PS: I assume that users use accessors and not $obj->{attribute}
Or can Sub::Trigger::Lock make a DateTime object immutable?? (throw an exception on method calls that would mutate the state)
I don't think cloning is necessarily the best way to approach this. Yes, it's potentially a pretty thorough way to prevent people from tampering, but it has its own set of problems.
If all objects are plain old blessed hashrefs, cloning everything seems a simple enough proposition. Cloning
$o
is really just$clone = bless({%$o}, ref($o))
. However, given that objects (even hashref-based ones) may store some of their attributes inside-out, this isn't a generic solution - it requires some co-operation from the object; it must provide some kind of$o->clone
method for you to call.Co-operative immutability is perhaps a better solution. If I have an attribute which takes DateTime objects, and it's documented as read-only, then I probably don't want you calling mutator methods on it. Perhaps you technically can, but I'd rather you didn't, the same way I'd rather you didn't access my attributes like
$o->{"attr"}
.There are styles of programming that encourage people to co-operate. In your example where you create a DateTime object, pass it to my constructor, and then manipulate your original DateTime object, that could be combated by documenting that my constructor accepts an ISO-8601-formatted datetime string, and then coercing it to a DateTime object internally.
In your example of
$object->attribute->some_mutator()
, if$object->attribute
just returned a clone of the attribute, yes, it would mean that$object
has been made safe from mutation, but it also means that the call tosome_mutator()
has silently succeeded, but effectively probably done nothing, which leads to a counter-intuitive interface. I'd rather that call threw an exception. And if an exception is not a possibility, then I'd rather it just properly succeeded, mutating the object, but in documentation discourage callers from doing this sort of thing. (See also: the Law of Demeter, which if followed would probably forbid such a method call.)Immutable instances require "buy-in" from across the entire application. Yes, I can write a class in such a way that it encourages immutable usage (e.g. making accessors read-only). However, this class is only a small part of the jigsaw that makes up the entire application. Perhaps some of the other classes in the application (like DateTime) might have mutable instances - the solution is to just agree to not call the methods that mutate instances - especially not on objects which might have other objects pointing to them and thus result in action at a distance. (If I create a DateTime object in a method, and don't pass it to anybody else, then I can probably safely call mutators on it within the privacy of my own homeHHHH lexical scope.)
@mascip: DateTime being mutable is a bad design. Just don't use mutable classes for the properties of the class you want to be immutable.
Yes, I know this is good in theory, but hard is practice due to the DateTime object we have in Perl 5.
I agree toby and dolmen, ultimately the goal is to avoid using mutable classes and methods. The problem is:
We don't always realize that a method mutates an object.
For instance here RJBS didn't realize that DateTime->add_duration() mutated the object. If he had cloned his attributes, he wouldn't have had this problem.
Buy I agree with your analysis about cloning in the end:
- for inputs, some of them cannot be cloned.
- for outputs, it might be better to let them be mutated, to not confuse the user.
Arg.
In an imperfect environment (in terms of immutability) we can't get a perfect result.
What about having a Moo(se) role that enables Sub::Trigger::Lock on all array and hash references? That could be a good help already.
And what about p5-mop? Could p5-mop objects have locked attributes? (I'm aware that's a totally different question, and it's probably Stevan i should ask)
I've also been thinking about testing for immutability. Basically it's
my $obj = Some::Class->new;
my $copy = $obj->clone;
$copy->some_method(\@params);
is_deeply($copy, $obj, '->some_method(\@params) did not mutate the object');
which could be embedded in a function:
doesnt_mutate($obj, 'some_method', \@params);
Is this a reasonable approach?
I wonder what you are looking for is something like how DBI works (on the inside).
For example when you prepare a statement you get a new copy of an 'Statement' object, or handle to use DBI-ish, while the original statement object stays unchanged from when you start.
Is this what you are looking for??
Hi byterock :-) Apparently DBI delivers the Statement object in an immutable way, then.
Apparently my goal is not clear. I've added a Post Scriptum to my original post to clarify it. Let me know if that worked.
DBI::st objects are not conceptually immutable. If you call, say
fetchrow_array
on one, you get back a different row each time. This implies that each call is changing the internal state of the object.DBI is actually an example of where mutable objects are perfectly sensible: as iterators.
Immutable objects are great because they are easier to reason about, reduce the possibilities for action at a distance, and make testing simpler. But they are not the perfect fit for every problem. Sometimes you need to be prepared to forego the benefits of immutable objects, in favour of coming up with a class design that makes the most sense for the type of data and processes you're modelling.
I don't have a clear opinion about whether iterators should be mutable or immutable. But I would like tools to help me get some level of immutability, without me needing to work on it too hard.
It would be nice to get most of my attributes to be Sub::Trigger::Lock-ed without needing to write it every time (I could probably try to write a MooX module for that - I'd have to read the relevant Moo documentation). And I'd like to have some functions to test for immutability (I might try it at home).
Some online people's opinion about whether iterators should be mutable or immutable:In nothingmuch's article (in my first post), he says: "there's usually no good reason to prefer mtability". And Clojure and Erlang don't seem to suffer from defaulting to immutability.
I'm not proposing to enforce immutability, especially in an environment where so many objects are mutable already. Just thinking about tools to make it easier to produce immutable classes. In the same way that Moose doesn't enforce Object Oriented design: it facilitates it.
"DBI::st objects are not conceptually immutable. If you call, say fetchrow_array on one, you get back a different row each time"
Yes of course as you are using an instance of a statement handle. The Original object that was loaded by DBI is unchanged (though I suppose you could changed it as it is only the inner part of a tied hash)
I guess what I was trying to say is that DBI keeps an (hopefully) Immutable copy of the DBI,Driver,and Statement around that it clones from and it would be useful if it could be made immutable