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

Attachment: signature.asc
Description: Digital signature

Reply via email to