Prototype-Based Inheritance (#214)

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

The three rules of Ruby Q.:

  1. Please do not post any solutions or spoiler discussion for this
    quiz until 48 hours have elapsed from the time this message was
    sent.

  2. Support Ruby Q. by submitting ideas and responses
    as often as you can!
    Visit: http://rubyquiz.strd6.com/suggestions

  3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem
helps everyone on Ruby T. follow the discussion. Please reply to
the original quiz message, if you can.

RSS Feed: http://rubyquiz.strd6.com/quizzes.rss

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

Prototype-Based Inheritance (#214)

Ayú Rubyists,

This week’s quiz is to implement prototype-based Inheritance1. In
prototype-based inheritance objects gain their properties from other
objects, not from class hierarchies. Here’s a code example to
illustrate setting a prototype explicitly:

pele = {:name => "Pele", :sport => "football", :position => 

“Forward”}
pele_jr = {:name => “Pele Jr.”}
pele_jr.prototype = pele
pele_jr.name # => “Pele Jr.”
pele_jr.sport # => “football”
pele_jr.position # => “forward”

You may notice that this doesn’t work: NoMethodError: undefined method 'prototype' for {:name => "Pele", :sport => "football", :position => "Forward"}:Hash. If it did work then there’s not much
for you to do on this quiz!

Hopefully this example illustrates some of the basics of how
prototype-based inheritance works: there is one prototypical football
player and other players defer to that player if they are queried for
an attribute that hasn’t been specified. This relates closely with our
intuitions and how we discuss things “it’s kind of like Starbucks,
except the drinks are bigger”

new_coffee_shop.prototype = starbucks
new_coffee_shop.drink_size += 1

Have Fun!

pele = {:name => "Pele", :sport => "football", :position => "Forward"}
pele_jr = {:name => "Pele Jr."}
pele_jr.prototype = pele
pele_jr.name # => "Pele Jr."
pele_jr.sport # => "football"
pele_jr.position # => "forward"

What about:

class Hash

def prototype=(prototype)
    merge!(prototype) {|k,o,n| o}
end

def method_missing(name, *args, &block)
    if has_key?(name)
        val = fetch(name)
        case val
        when Proc
            return val.call(self, *args)
        end
    end
    if name.to_s =~ /^(.*?)=$/
        meth = :[]=
        name = $1.intern
    else
        meth = :[]
    end
    margs = [name, *args]
    margs << block if block
    send(meth, *margs)
end

end

This would allow you to do something like:

pele_jr.age = 20
pele_jr.foo = lambda {|this, a| this.age + a}
pele_jr.foo(10) #=> 30

On Sunday 12 July 2009 12:49:55 pm lith wrote:

def prototype=(prototype)
    merge!(prototype) {|k,o,n| o}

I based my implementation on Javascript, and one of the nice properties
there
is that the prototype can change later. So, for example, in my
implementation:

irb(main):005:0> pele.favorite_food = ‘hamburger’
=> “hamburger”
irb(main):006:0> pele_jr.favorite_food
=> “hamburger”
irb(main):007:0> pele
=> {:name=>“Pele”, :sport=>“football”, :position=>“Forward”,
:favorite_food=>“hamburger”}
irb(main):008:0> pele_jr.favorite_food = ‘cheeseburger’
=> “cheeseburger”
irb(main):009:0> pele_jr.favorite_food
=> “cheeseburger”
irb(main):010:0> pele.favorite_food
=> “hamburger”

Of course, this is slower, but more powerful, I think.

        when Proc
            return val.call(self, *args)
        end

[snip]

This would allow you to do something like:

pele_jr.age = 20
pele_jr.foo = lambda {|this, a| this.age + a}
pele_jr.foo(10) #=> 30

Nice touch…

Here’s mine:

module Prototype
attr_accessor :prototype
def method_missing *args
prototype.public_send *args
end
end

That’s more generic than just hashes. Mix it into Object and it works on
anything. Of course, it’s not exactly convenient to have to define new
values
like this:

pele = Object.new
class << pele
attr_accessor :name
end
pele.name = ‘Pele’

It also severely limits what you can do with some of the builtin classes
like
Fixnums that can’t have a metaclass.

And of course, to make it saner for the example (and because OpenStruct
won’t
cooperate), I can do this:

class Hash
include Prototype
def method_missing name, *args
if has_key? name
self[name]
elsif name =~ /.=$/
self[name.to_s.chomp(’=’).to_sym] = args.first
else
super
end
end
end

Hi,

a simple Solution, which works for static Prototypes:

module Prototype
def method_missing name, *args
# if we have a prototype forward to it’s methods
# else propagate method missing upwards
if defined?(@proto) && @proto
@proto.send name,*args
else
super
end
end

def set_prototype pt
# clone the prototype, so we can freely modify
# it’s attributes w/o interference with others
@proto = pt.clone
end
end

As said above this is a simple solution, but it should work for all
classes
and is easily written.
But it comes at the cost of duplicating arbitrarily complex objects
(perhaps
many times) and just allows static prototypes.
I couldn’t come up with a solution which filters writing access to the
proto-
type (apart from simple regexing) but allows modification from the
parentside.
Possibly one could use observers and method_defined hooks for this, but
the
complexity would be rather high on this way.

Best regards,
Thorsten

Daniel M. wrote:

Prototype-Based Inheritance (#214)

I decided to give it a shot:

require ‘ostruct’

class Prototype < OpenStruct
attr_reader :prototype

def prototype=(new_prototype)
  if new_prototype.prototypes.include?(self)
    raise ArgumentError, "circular prototype chain"
  end
  @prototype = new_prototype
end

def prototypes
  if @prototype.nil?
    []
  else
    [@prototype] + @prototype.prototypes
  end
end

def delete_slot(name)
  name = name.to_sym
  if @table.has_key?(name)
    meta = class << self; self; end
    meta.send(:remove_method, name)
    meta.send(:remove_method, :"#{name}=")
  end
  delete_field(name)
end

def method_missing(mid, *args)
  if @prototype.nil? || mid.id2name =~ /=$/
    super
  else
    @prototype.send(mid, *args)
  end
end

end

Most of the functionality is inherited from OpenStruct. The trip down
the prototype chain happens in method_missing. A short IRB session
should explain the usage:

require ‘prototype’
true

starbucks = Prototype.new(:name => “Starbucks”, :drink_size => 3)
=> #<Prototype name=“Starbucks”, drink_size=3>

new_coffee_shop = Prototype.new(:name => “New”)
=> #

new_coffee_shop.prototype = starbucks
=> #<Prototype name=“Starbucks”, drink_size=3>

new_coffee_shop.drink_size
=> 3

new_coffee_shop.drink_size += 1
=> 4

new_coffee_shop.drink_size
=> 4

new_coffee_shop.delete_slot(:drink_size)
=> 4

new_coffee_shop.drink_size
=> 3

another_coffee_shop = Prototype.new(:name => “Another”)
=> #

another_coffee_shop.prototype = new_coffee_shop
=> #

another_coffee_shop.drink_size
=> 3

another_coffee_shop.prototypes
=> [#, #<Prototype name=“Starbucks”,
drink_size=3>]

starbucks.prototype = another_coffee_shop
ArgumentError: circular prototype chain
from ./prototype.rb:8:in `prototype=’
from (irb):23

-Matthias

Prototype based inheritance is used in many programming languages,
JavaScript being the most well known. As demonstrated in the solutions
this week there are a few different techniques that can be used.

lith provided a solution that merges the prototypes data into the
inheriting Hash when the prototype method is called. Special care is
taken to not overwrite fields in the inheriting Hash. This use of
merge! accomplishes that elegantly:

def prototype=(prototype)
  merge!(prototype) {|k,o,n| o}
end

Another feature of lith’s solution is the ability to use blocks as
values in the Hash. This allows the objects to inherit methods as well
as simple values.

starbucks = {:drink_size => 3, :cost => lambda {|this| 

this.drink_size + 1}}
new_coffee_shop = {:drink_size => 1}
new_coffee_shop.prototype = starbucks

In this case the new_coffee_shop’s cost will be 2. It inherits the
cost method from the parent.

David M.'s solution used the JavaScript style of prototype
inheritance, where rather than merging the values of the parent into
the child object, the child object maintains a reference to the
parent. An advantage of this technique is that updates in the parent
are carried through to the child, unless the child overrides that
property. There is a trade off however, maintaining the link back to
the parent means an extra method lookup. This isn’t usually an issue,
but if you are working with a deep nesting and inheriting many
attributes it can add up, so it’s important to be aware of.

Thorsten H. submitted a simple solution that maintained a reference
to a copy of the prototype object. This prevents modifications in the
prototype from being reflected in the child. The simplicity of the
implementation comes at a cost though, duplicating some objects may be
very expensive.

Matthias R. also submitted a solution this week. Matthias’s
solution extends OpenStruct to handle converting method calls into
hash keys. Like David’s solution Matthias’s solution maintains a
reference to the prototype object. In addition Matthias gives us a
prototypes method that returns an array of all the prototypes for
this object. This is used to detect a circular inheritance chain,
which almost always means an error. Matthias also adds a method to
delete a property. When a property is deleted from a child the child’s
prototype’s property will shine through.

There were several interesting takes on this week’s quiz. Some of the
decisions, like whether to have fixed prototypes by copying/merging or
dynamic prototypes by maintaining references are mutually exclusive,
but other techniques from these solutions can be combined together,
like allowing blocks, deleting properties, or maintaining a list of
prototypes. When deciding which type of inheritance best fits your
next applications needs it is important to consider the trade offs
between simplicity and efficiency as well as speed and memory.

Thank you lith, David, Thorsten and Matthias for your solutions to
this week’s quiz!

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs