Implementing a Read-Only array

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

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

···

On 01/06/05, Gavin Kistner <gavin@refinery.com> wrote:

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

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

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

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?)

···

On Jun 1, 2005, at 7:49 AM, Brian Schröder wrote:

On 01/06/05, Gavin Kistner <gavin@refinery.com> wrote:

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.

Gavin Kistner wrote:

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

···

On Jun 1, 2005, at 7:49 AM, Brian Schröder wrote:

On 01/06/05, Gavin Kistner <gavin@refinery.com> wrote:

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)

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

···

On 01/06/05, Robert Klemme <bob.news@gmx.net> wrote:

Gavin Kistner wrote:
> On Jun 1, 2005, at 7:49 AM, Brian Schröder wrote:
>> On 01/06/05, Gavin Kistner <gavin@refinery.com> wrote:
>>> 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

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

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

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

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

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

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

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

p ro.reverse!

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

···

On Jun 1, 2005, at 4:11 PM, Brian Schröder wrote:

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.

Gavin Kistner wrote:

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

···

On Jun 1, 2005, at 4:11 PM, Brian Schröder wrote:

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

···

On Jun 2, 2005, at 3:55 AM, Robert Klemme wrote:

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

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[i] = 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 :wink: If it breaks, you will know it and if
it does not it's also good.

best regards,

Brian

···

On 02/06/05, Robert Klemme <bob.news@gmx.net> wrote:

Gavin Kistner wrote:
> On Jun 1, 2005, at 4:11 PM, Brian Schröder wrote:
>>> 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.

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

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

Gavin Kistner wrote:

···

On Jun 2, 2005, at 3:55 AM, Robert Klemme wrote:

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

Duly noted and understood.

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

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

irb(main):004:0> a =

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

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

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

···

On Jun 2, 2005, at 11:22 AM, Brian Schröder wrote:

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[i] = v
  end
end

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

-a

···

On Fri, 3 Jun 2005, Gavin Kistner wrote:

On Jun 2, 2005, at 11:22 AM, Brian Schröder wrote:

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[i] = v
  end
end

Duly noted and understood.

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

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

irb(main):004:0> a =

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

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

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

--

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

===============================================================================

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

···

On Jun 2, 2005, at 4:54 PM, Ara.T.Howard wrote:

On Fri, 3 Jun 2005, Gavin Kistner wrote:

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?