On Thu, Aug 18, 2011 at 04:06:47PM +0200, Carl Mäsak wrote: > I was working on the Little Animal Farm game yesterday, and wanted to > make it totally safe against tampering from the outside. (See > <http://masak.org/carl/yapc-eu-2011-little-animal-farm/talk.pdf>.) > > I ended up implementing a custom accessor method and a sub to > deep-clone hashes, just to make sure that data belonging to readonly > attributes doesn't get changed. > > This worries me. > > Concrete example (useless output left out): > > $ perl6 > > class A { has @.numbers; method add($n) { @!numbers.push($n) } } > > my $a = A.new > > $a.add(1); $a.add(2); $a.add(3) > > $a.numbers[1] = "OH NOES" > > $a.numbers > 1 OH NOES 3 > > Notice that the attribute @.numbers is readonly (since it's not > declared 'is rw'). But that does us a fat lot of good here; it just > means that I'm not allowed to reassign the array reference itself. The > contents are just as writable as any array's. > > What worries me -- and the reason I turn to p6l for this -- is that a > lot of class authors will fail to notice this fact and write code that > isn't properly encapsulated and, depending on the circumstances, maybe > even insecure.
This is not specific to Perl 6 at all, and is a well-known problem in
object-oriented languages. Some quick playing with search engines
turns up the phrase "defensive copy".
> Quoting from S06:639, which talks about readonly routine parameters:
>
> ] By default, all parameters are readonly aliases to their corresponding
> ] arguments--the parameter is just another name for the original
> ] argument, but the argument can't be modified through it. This is
> ] vacuously true for value arguments, since they may not be modified in
> ] any case. However, the default forces any container argument to also
> ] be treated as an immutable value. This extends down only one level;
> ] an immutable container may always return an element that is mutable if
> ] it so chooses.
>
> Applying this semantics to readonly attributes would make the above
> issue go away. The problem with the semantics is that there doesn't
> seem to be a way to implement it that is both performant and free from
> surprising side effects.
To me it is obvious that non-rw attributes, non-rw parameters, and non-rw
block returns should use the same semantics. Of course that leaves the
question of what those semantics should be.
> I solved this in my code by writing my own accessor that clones the array:
>
> $ perl6
> > class A { has @!numbers; method add($n) { @!numbers.push($n); return };
> > method numbers { @!numbers.clone } }
> > my $a = A.new
> > $a.add(1); $a.add(2); $a.add(3)
> > $a.numbers[1] = "OH NOES"
> > $a.numbers
> 1 2 3
>
> It felt kinda silly to write that accessor. Note also that I had to
> explicitly 'return' from the .add method, because .push returns the
> array acted on, and that would otherwise have been return, and the
> class would again be vulnerable. So it's not just accessors that are
> the problem.
You didn't have to add that return. Method add is not declared
C<method add($n) is rw>, which means that its return value is automatically
protected against modification in the same sort of way as a non-rw
attribute.
> But it gets worse.
>
> $ perl6
> > class B { has %!info = foo => { bar => 42 }; method info { %!info.clone } }
> > my $b = B.new
> > $b.info.perl
> {"foo" => {"bar" => 42}}
>
> > $b.info<foo> = "OH NOES"
> Cannot modify readonly value
> > $b.info<baz> = 42
> > $b.info.perl
> {"foo" => {"bar" => 42}}
>
> > $b.info<foo><bar> = "OH NOES"
> > $b.info.perl
> {"foo" => {"bar" => "OH NOES"}}
>
> We've done the cloning, so we're safe from changes on the shallow
> level. But changes on the levels below go through. (Same goes for
> arrays?) Why? When we clone the hash, we copy over the keys/values to
> a new hash. The value in this case is a hash reference, so the new
> hash gets the same hash reference.
>
> I ended up writing a custom deep-cloner for hashes in order not to
> leak any information from the class:
>
> $ perl6
> > sub deepclone(%h) { hash map -> $k, $v {; $k => ($v ~~ Hash ??
> > deepclone($v) !! $v ) }, %h.kv }
> > class B { has %!info = foo => { bar => 42 }; method info {
> > deepclone(%!info) } }
> > my $b = B.new
> > $b.info<foo><bar> = "OH NOES"
> > $b.info.perl
> {"foo" => {"bar" => 42}}
>
> This solves it, but in a specialized and ad-hoc way.
>
> The whole thing leaves me with the following questions:
>
> * What do we mean when we say 'has @.a is readonly'? What do we want it to
> mean?
It means has @!a; method a() { @!a }. @!a is returned in a way that is
readonlyified in accordance with the rules of non-rw blocks.
> * Are we fine with references from readonly attributes leaking out of
> the class? Should we be?
I am, personally. It's not a problem unique to Perl 6 and I have yet to
see a solution (in any language) that fails to be worse than the original
problem.
> * What language components could be provided to put class implementors
> on the right path so that their classes end up encapsulated by
> default?
C++ has this in the form of the const system. Essentially, every reference
may be "const qualified", which forbids all mutating access; const semantics
are implicitly deep, because constness carries over to derived references
from method calls (simplified greatly).
> * Could someone restate this whole problem space in a way that doesn't
> make my head hurt?
I have thought about the "generalized Perl 6 const problem" before, mostly
in the context of non-rw subs. I don't have a solution which isn't worse
than the problem.
> Some references follow.
>
> The book "Effective Java" advises Java programmers to write their
> accessors to clone things whose internal references they don't want to
> leak out of the class. It also has basically the same wording for
> constructor parameters, since that would be a way for external
> references to "leak into" the class.
>
> <http://java.sun.com/docs/books/effective/>
>
> RFC 67 proposes a .clone method in Perl 6. It does deep cloning,
> however, and what we ended up with is shallow cloning.
>
> <http://dev.perl.org/perl6/rfc/67.html>
>
> I seem to recall at least one p6c or p6l thread talking at length
> about deep cloning, but I can't find it.
-Stefan
signature.asc
Description: Digital signature
