Implementing a Read-Only array

G

Gavin Kistner

Right up front, let me say that I realize that I can't prevent
modifications to objects referenced by my array - that's OK.

SUMMARY
My desire is to create an Array which is read-only from the outside,
but which my class can modify internally. #freeze is not an option,
nor is #dup.


BACKGROUND
The goal here is to mimic the NodeList class in the W3C DOM model. A
NodeList is an 'array' of items returned from various methods, which
is immutable by the external consumer but which is also 'live' -
references to the returned list of nodes will be updated as the DOM
tree is modified.


In my implementation, I'm creating an array that provides an ordered
list of children for each Node. Each Node also has #previous_sibling,
#next_sibling, #first_child, #last_child, and #parent_node
attributes, which must be properly kept in-sync.

A really nice implementation would cause direct manipulation of the
Array to update all associated attributes. In the end I may do that.
However, for now, what I want is to return an array which will not
let the user modify the array itself, but which my own class can
modify when necessary.


The code I have currently follows (minus comments and some edge
checking, for terseness). What I have works, but I'm wondering if
there is a better way than just aliasing the methods. (I know that
#instance_eval and the like means that I cannot truly lock down a
class, but security-through-obscurity seems less than ideal.)


class ReadOnlyArray < Array
alias_method :'__ro_<<', :'<<' #:nodoc:
alias_method :__ro_insert, :insert #:nodoc:
alias_method :__ro_delete_at, :delete_at #:nodoc:
affectors = %w| << []= clear concat delete delete_at delete_if
fill flatten! insert map! pack pop push reject! replace reverse!
shift slice! sort! uniq! unshift |
affectors.each{ |name| undef_method name }
end

module OrderedTreeNode
attr_reader :next_sibling, :previous_sibling, :parent_node
attr_reader :first_child, :last_child, :child_nodes

def insert_before( new_child, ref_child=nil )
kids = child_nodes

#Find the index of the ref_child, if given, or use the end
dest_slot = ref_child ? kids.index( ref_child ) : kids.length

#Shove the child into the array and update its pointers
kids.__ro_insert( dest_slot, new_child )
new_child.previous_sibling = kids[ dest_slot - 1 ]
new_child.next_sibling = kids[ dest_slot + 1 ]
new_child.parent_node = self

new_child.previous_sibling.next_sibling = new_child if
new_child.previous_sibling
new_child.next_sibling.previous_sibling = new_child if
new_child.next_sibling

new_child
end

def child_nodes #:nodoc:
@__phrogzdomorderedtreenode_childnodes ||= ReadOnlyArray.new
end

#:stopdoc:
protected
attr_writer :parent_node, :next_sibling, :previous_sibling
attr_writer :first_child, :last_child
#:startdoc:

end
 
B

Brian Schröder

Right up front, let me say that I realize that I can't prevent
modifications to objects referenced by my array - that's OK.
=20
SUMMARY
My desire is to create an Array which is read-only from the outside,
but which my class can modify internally. #freeze is not an option,
nor is #dup.
=20
BACKGROUND
The goal here is to mimic the NodeList class in the W3C DOM model. A
NodeList is an 'array' of items returned from various methods, which
is immutable by the external consumer but which is also 'live' -
references to the returned list of nodes will be updated as the DOM
tree is modified.
=20
In my implementation, I'm creating an array that provides an ordered
list of children for each Node. Each Node also has #previous_sibling,
#next_sibling, #first_child, #last_child, and #parent_node
attributes, which must be properly kept in-sync.
=20
A really nice implementation would cause direct manipulation of the
Array to update all associated attributes. In the end I may do that.
However, for now, what I want is to return an array which will not
let the user modify the array itself, but which my own class can
modify when necessary.
=20
The code I have currently follows (minus comments and some edge
checking, for terseness). What I have works, but I'm wondering if
there is a better way than just aliasing the methods. (I know that
#instance_eval and the like means that I cannot truly lock down a
class, but security-through-obscurity seems less than ideal.)
=20
class ReadOnlyArray < Array
alias_method :'__ro_<<', :'<<' #:nodoc:
alias_method :__ro_insert, :insert #:nodoc:
alias_method :__ro_delete_at, :delete_at #:nodoc:
affectors =3D %w| << []=3D clear concat delete delete_at delete_if
fill flatten! insert map! pack pop push reject! replace reverse!
shift slice! sort! uniq! unshift |
affectors.each{ |name| undef_method name }
end
=20
module OrderedTreeNode
attr_reader :next_sibling, :previous_sibling, :parent_node
attr_reader :first_child, :last_child, :child_nodes
=20
def insert_before( new_child, ref_child=3Dnil )
kids =3D child_nodes
=20
#Find the index of the ref_child, if given, or use the end
dest_slot =3D ref_child ? kids.index( ref_child ) : kids.length
=20
#Shove the child into the array and update its pointers
kids.__ro_insert( dest_slot, new_child )
new_child.previous_sibling =3D kids[ dest_slot - 1 ]
new_child.next_sibling =3D kids[ dest_slot + 1 ]
new_child.parent_node =3D self
=20
new_child.previous_sibling.next_sibling =3D new_child if
new_child.previous_sibling
new_child.next_sibling.previous_sibling =3D new_child if
new_child.next_sibling
=20
new_child
end
=20
def child_nodes #:nodoc:
@__phrogzdomorderedtreenode_childnodes ||=3D ReadOnlyArray.new
end
=20
#:stopdoc:
protected
attr_writer :parent_node, :next_sibling, :previous_sibling
attr_writer :first_child, :last_child
#:startdoc:
=20
end
=20
=20

Why not create a proxy class that contains an array and only forwards
the reading methods. It seems to me, that this would achieve exactly
what you want.

best regards,

Brian

--=20
http://ruby.brian-schroeder.de/

Stringed instrument chords: http://chordlist.brian-schroeder.de/
 
G

Gavin Kistner

--Apple-Mail-10--259184887
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain;
charset=ISO-8859-1;
delsp=yes;
format=flowed

class ReadOnlyArray < Array
alias_method :'__ro_<<', :'<<' #:nodoc:
alias_method :__ro_insert, :insert #:nodoc:
alias_method :__ro_delete_at, :delete_at #:nodoc:
affectors =3D %w| << []=3D clear concat delete delete_at = delete_if
fill flatten! insert map! pack pop push reject! replace reverse!
shift slice! sort! uniq! unshift |
affectors.each{ |name| undef_method name }
end
Why not create a proxy class that contains an array and only forwards
the reading methods. It seems to me, that this would achieve exactly
what you want.

That's an interesting idea. What advantage do you think that would =20
offer compared to my implementation above? How would you modify the =20
internal representation when necessary? (Perhaps the constructor for =20
the proxy class receives the array that it is supposed to wrap?)

One advantage of mine versus the proxy approach would be that by =20
actually inheriting from Array, my class supports additional methods =20
defined by the user for Array or Enumerable. (If the user has added a =20=

special Array#my_collect method, my array is extended as well.)

(I think that it's correct that it would not be possible for a custom =20=

user method to modify the array, if I'm undef'ing all core methods =20
for modifying it, yes?)

--Apple-Mail-10--259184887--
 
R

Robert Klemme

Gavin said:
class ReadOnlyArray < Array
alias_method :'__ro_<<', :'<<' #:nodoc:
alias_method :__ro_insert, :insert #:nodoc:
alias_method :__ro_delete_at, :delete_at #:nodoc:
affectors = %w| << []= clear concat delete delete_at delete_if
fill flatten! insert map! pack pop push reject! replace reverse!
shift slice! sort! uniq! unshift |
affectors.each{ |name| undef_method name }
end
Why not create a proxy class that contains an array and only forwards
the reading methods. It seems to me, that this would achieve exactly
what you want.

That's an interesting idea. What advantage do you think that would
offer compared to my implementation above? How would you modify the
internal representation when necessary? (Perhaps the constructor for
the proxy class receives the array that it is supposed to wrap?)

One advantage of mine versus the proxy approach would be that by
actually inheriting from Array, my class supports additional methods
defined by the user for Array or Enumerable. (If the user has added a
special Array#my_collect method, my array is extended as well.)

(I think that it's correct that it would not be possible for a custom
user method to modify the array, if I'm undef'ing all core methods
for modifying it, yes?)

require 'delegate'

class ImmutableArray < DelegateClass(Array)
def []=(*a) raise "Immutable!" end
def <<(*a) raise "Immutable!" end
def push(*a) raise "Immutable!" end
def unshift(*a) raise "Immutable!" end
end

?> a = [1,2,3]
=> [1, 2, 3]
ia = ImmutableArray.new a => [1, 2, 3]
ia << "bust"
RuntimeError: Immutable!
from (irb):5:in `<<'
from (irb):13

:))

Kind regards

robert
 
P

Phrogz

Well slap me with a wet noodle and call me Suzy, that is straight-up
gorgeous!

Thanks,

- Gavin (who needs to read more of Standard Lib, more thoroughly)
 
B

Brian Schröder

Gavin said:
class ReadOnlyArray < Array
alias_method :'__ro_<<', :'<<' #:nodoc:
alias_method :__ro_insert, :insert #:nodoc:
alias_method :__ro_delete_at, :delete_at #:nodoc:
affectors =3D %w| << []=3D clear concat delete delete_at delete_= if
fill flatten! insert map! pack pop push reject! replace reverse!
shift slice! sort! uniq! unshift |
affectors.each{ |name| undef_method name }
end
Why not create a proxy class that contains an array and only forwards
the reading methods. It seems to me, that this would achieve exactly
what you want.

That's an interesting idea. What advantage do you think that would
offer compared to my implementation above? How would you modify the
internal representation when necessary? (Perhaps the constructor for
the proxy class receives the array that it is supposed to wrap?)

One advantage of mine versus the proxy approach would be that by
actually inheriting from Array, my class supports additional methods
defined by the user for Array or Enumerable. (If the user has added a
special Array#my_collect method, my array is extended as well.)

(I think that it's correct that it would not be possible for a custom
user method to modify the array, if I'm undef'ing all core methods
for modifying it, yes?)
=20
require 'delegate'
=20
class ImmutableArray < DelegateClass(Array)
def []=3D(*a) raise "Immutable!" end
def <<(*a) raise "Immutable!" end
def push(*a) raise "Immutable!" end
def unshift(*a) raise "Immutable!" end
end
=20
?> a =3D [1,2,3]
=3D> [1, 2, 3]
ia =3D ImmutableArray.new a =3D> [1, 2, 3]
ia << "bust"
RuntimeError: Immutable!
from (irb):5:in `<<'
from (irb):13
=20
:))
=20
Kind regards
=20
robert
=20
=20

Though this means that you have to override all methods that can
change the array. I'd shurely forget something in this case. You
forgot e.g. #clear. Therefore I'd propose to explicitly delegate the
needed methods for the application because this is easier to control.

best regards,

Brian

--=20
http://ruby.brian-schroeder.de/

Stringed instrument chords: http://chordlist.brian-schroeder.de/
 
G

Gavin Kistner

Gavin said:
affectors =3D %w| << []=3D clear concat delete delete_at delete_if
fill flatten! insert map! pack pop push reject! replace reverse!
shift slice! sort! uniq! unshift |

Though this means that you have to override all methods that can
change the array. I'd shurely forget something in this case. You
forgot e.g. #clear. Therefore I'd propose to explicitly delegate the
needed methods for the application because this is easier to control.

But again, that way prevents users' convenience methods for Arrays/=20
Enumerables from being available.

The 'affectors' list above is (I believe) precisely the list of =20
methods that can change the array. (Except I accidentally included =20
pack, which is non-mutating.)

The final solution that I'm currently using is:

require 'delegate'

class ReadOnlyArray < DelegateClass(Array)
mutators =3D %w| << []=3D clear concat delete delete_at delete_if =20=

fill |
mutators.concat %w| flatten! insert map! pop push reject! replace |
mutators.concat %w| reverse! shift slice! sort! uniq! unshift |
mutators.each do |name|
define_method( name ){ raise "#{self} is read-only!" }
end
end

a =3D [1,2,3]
ro =3D ReadOnlyArray.new( a )

a << 4
p ro
#=3D> [1, 2, 3, 4]

p ro.reverse
#=3D> [4, 3, 2, 1]

p ro.reverse!

/Users/gkistner/Desktop/tmp.rb:8:in `reverse!': 1234 is read-only! =20
(RuntimeError)
from /Users/gkistner/Desktop/tmp.rb:8:in `reverse!'
from /Users/gkistner/Desktop/tmp.rb:18
 
R

Robert Klemme

Gavin said:
Gavin Kistner wrote:
affectors = %w| << []= clear concat delete delete_at delete_if
fill flatten! insert map! pack pop push reject! replace reverse!
shift slice! sort! uniq! unshift |

Though this means that you have to override all methods that can
change the array. I'd shurely forget something in this case. You
forgot e.g. #clear. Therefore I'd propose to explicitly delegate the
needed methods for the application because this is easier to control.

But again, that way prevents users' convenience methods for Arrays/
Enumerables from being available.

Yeah, I prefer the method to explicitely exclude mutators, too. The
reason being that IMHO there are more often non modifying convenience
methods defined on Array than modifying methods.
The 'affectors' list above is (I believe) precisely the list of
methods that can change the array. (Except I accidentally included
pack, which is non-mutating.)

The final solution that I'm currently using is:

require 'delegate'

class ReadOnlyArray < DelegateClass(Array)
mutators = %w| << []= clear concat delete delete_at delete_if
fill |
mutators.concat %w| flatten! insert map! pop push reject!
replace | mutators.concat %w| reverse! shift slice! sort! uniq!
unshift | mutators.each do |name|
define_method( name ){ raise "#{self} is read-only!" }
end
end

You don't even need the variable and the ugly concat

require 'delegate'

class ReadOnlyArray < DelegateClass(Array)
%w{
<< []= clear concat delete delete_at delete_if fill
flatten! insert map! pop push reject! replace
reverse! shift slice! sort! uniq! unshift
}.each do |name|
define_method( name ){ raise "#{self} is read-only!" }
end
end

Kind regards

robert
 
G

Gavin Kistner

--Apple-Mail-1--170027547
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed

You don't even need the variable and the ugly concat
[...]

Prettier, to be sure.


Now I'm bothered by the fact that my delegate array thinks that it
responds to methods which only exist to raise an error. I've hacked
the delegate.rb code to add an #undelegate_method method. Comments on
the code I used (and a cleaner way to do it) would be appreciated.
Does this (feature, if not implementation) seem like it might be a
good addition to the standard lib code?



[Slim:~/Desktop] gavinkis% diff /usr/local/lib/ruby/1.8/delegate.rb /
usr/local/src/ruby-1.8.2/lib/delegate.rb
100,110d99
< def self._ignored_methods
< @_ignored_methods ||= {}
< end
< def self.undelegate_methods( *methods )
< ignore = _ignored_methods
< methods.flatten.each do |m|
< m = m.to_s.intern
< undef_method( m )
< ignore[ m ] = true
< end
< end
115d103
< super if self.class._ignored_methods[ m ] #raise
StandardError, "No such method: #{name}"

[Slim:~/Desktop] gavinkis% cat readonlyarray.rb
require 'delegate'
class ReadOnlyArray < DelegateClass( Array )
undelegate_methods( %w{
<< []= clear concat delete delete_at delete_if fill
flatten! insert map! pop push reject! replace
reverse! shift slice! sort! uniq! unshift
} )
end

a = [1,2]
ro = ReadOnlyArray.new( a )
a << 3

puts a.respond_to?( :concat )
puts ro.respond_to?( :concat )
a.concat( [4,5,6] )
ro.concat( [7,8,9] )

[Slim:~/Desktop] gavinkis% ruby readonlyarray.rb
true
false
/usr/local/lib/ruby/1.8/delegate.rb:115:in `method_missing':
undefined method `concat' for [1, 2, 3, 4, 5, 6]:ReadOnlyArray
(NoMethodError)
from readonlyarray.rb:17


--Apple-Mail-1--170027547--
 
R

Robert Klemme

Gavin said:
You don't even need the variable and the ugly concat
[...]

Prettier, to be sure.


Now I'm bothered by the fact that my delegate array thinks that it
responds to methods which only exist to raise an error. I've hacked
the delegate.rb code to add an #undelegate_method method. Comments on
the code I used (and a cleaner way to do it) would be appreciated.
Does this (feature, if not implementation) seem like it might be a
good addition to the standard lib code?

Certainly. Did you try using remove_method - that might save you the
Hash.
http://www.ruby-doc.org/core/classes/Module.html#M000698

Kind regards

robert
 
B

Brian Schröder

Gavin said:
Gavin Kistner wrote:
affectors =3D %w| << []=3D clear concat delete delete_at delete_if
fill flatten! insert map! pack pop push reject! replace reverse!
shift slice! sort! uniq! unshift |

Though this means that you have to override all methods that can
change the array. I'd shurely forget something in this case. You
forgot e.g. #clear. Therefore I'd propose to explicitly delegate the
needed methods for the application because this is easier to control.

But again, that way prevents users' convenience methods for Arrays/
Enumerables from being available.
=20
Yeah, I prefer the method to explicitely exclude mutators, too. The
reason being that IMHO there are more often non modifying convenience
methods defined on Array than modifying methods.
=20

Just playing devils advocate here, but if you care about user added
methods, the user may also add something that acts in effekt like

class Array
def mutate(i, v)
self =3D v
end
end

which you certainly would like to remove. Though I have to disclaim
that I personally would simply stick with a simple array and a note
not to change its contents ;-) If it breaks, you will know it and if
it does not it's also good.

best regards,

Brian


--=20
http://ruby.brian-schroeder.de/

Stringed instrument chords: http://chordlist.brian-schroeder.de/
 
G

Gavin Kistner

Just playing devils advocate here, but if you care about user added
methods, the user may also add something that acts in effekt like

class Array
def mutate(i, v)
self =3D v
end
end


Duly noted and understood.

However...if I've undef'd #[]=3D, then that won't work, right?


irb(main):003:0> class Array; def mutate(i,v); self=3Dv; end; end

irb(main):004:0> a =3D []

irb(main):005:0> class << a; undef_method( :'[]=3D' ); end

irb(main):008:0> a[0] =3D 0
NoMethodError: undefined method `[]=3D' for []:Array
from (irb):8

irb(main):009:0> a.mutate(0,0)
NoMethodError: undefined method `[]=3D' for []:Array
from (irb):2:in `mutate'
from (irb):9
 
A

Ara.T.Howard

--8323328-1709284869-1117752889=:2568
Content-Type: MULTIPART/MIXED; BOUNDARY="8323328-1709284869-1117752889=:2568"

This message is in MIME format. The first part should be readable text,
while the remaining parts are likely unreadable without MIME-aware tools.

--8323328-1709284869-1117752889=:2568
Content-Type: TEXT/PLAIN; charset=X-UNKNOWN; format=flowed
Content-Transfer-Encoding: QUOTED-PRINTABLE

Just playing devils advocate here, but if you care about user added
methods, the user may also add something that acts in effekt like
=20
class Array
def mutate(i, v)
self =3D v
end
end


Duly noted and understood.

However...if I've undef'd #[]=3D, then that won't work, right?


irb(main):003:0> class Array; def mutate(i,v); self=3Dv; end; end

irb(main):004:0> a =3D []

irb(main):005:0> class << a; undef_method( :'[]=3D' ); end

irb(main):008:0> a[0] =3D 0
NoMethodError: undefined method `[]=3D' for []:Array
from (irb):8

irb(main):009:0> a.mutate(0,0)
NoMethodError: undefined method `[]=3D' for []:Array
from (irb):2:in `mutate'
from (irb):9


but that means that not even you can mutate it. if not even you can mutate=
it
why not just use Object#freeze?

-a
--=20
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D
| email :: ara [dot] t [dot] howard [at] noaa [dot] gov
| phone :: 303.497.6469
| My religion is very simple. My religion is kindness.
| --Tenzin Gyatso
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D

--8323328-1709284869-1117752889=:2568--
--8323328-1709284869-1117752889=:2568--
 
G

Gavin Kistner

--Apple-Mail-1--124546398
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed

However...if I've undef'd #[]=, then that won't work, right?
but that means that not even you can mutate it. if not even you
can mutate it
why not just use Object#freeze?

Well, if I alias it first I can. (This is using my original non-
delegated method, since the delegator technique (as I've currently
implemented it) does allow the custom mutator method to affect the
wrapped object directly.

class Array
def mutate(k,v); self[k]=v; end
end

class ImmutableArray < Array
alias_method :'__ro_[]=', :'[]='
%w{
<< []= clear concat delete delete_at delete_if fill
flatten! insert map! pop push reject! replace
reverse! shift slice! sort! uniq! unshift
}.each{ |m|
undef_method( m )
}
end

ro = ImmutableArray.new( [0,1,2] )

ro.send( :'__ro_[]=', 1, 'one' )
p ro
#=> [0, "one", 2]

ro.mutate( 2, 'two' )
#=> /Users/gavinkistner/Desktop/readonlyarray.rb:11:in `mutate':
undefined method `[]=' for [0, "one", 2]:ImmutableArray (NoMethodError)
from /Users/gavinkistner/Desktop/readonlyarray.rb:44


--Apple-Mail-1--124546398--
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,780
Messages
2,569,611
Members
45,277
Latest member
VytoKetoReview

Latest Threads

Top