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.

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.

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.

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?

* Are we fine with references from readonly attributes leaking out of
the class? Should we be?

* What language components could be provided to put class implementors
on the right path so that their classes end up encapsulated by
default?

* Could someone restate this whole problem space in a way that doesn't
make my head hurt?

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.

// Carl

Reply via email to