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