Forum: Ruby Prototype-Based Inheritance (#214)

33117162fff8a9cf50544a604f60c045?d=identicon&s=25 Daniel X Moore (yahivin)
on 2009-07-10 23:25
(Received via mailing list)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

The three rules of Ruby Quiz:

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 Quiz 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 Talk 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 Inheritance[1]. 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!

[1]: http://en.wikipedia.org/wiki/Prototype-based_programming
87ef5d1e14b148eb596433bc17ffe690?d=identicon&s=25 lith (Guest)
on 2009-07-12 19:50
(Received via mailing list)
>     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
1bc63d01bd3fcccc36fb030a62039352?d=identicon&s=25 David Masover (Guest)
on 2009-07-13 17:29
(Received via mailing list)
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
D91f9adf63acfa73b50ebab43f10d7ee?d=identicon&s=25 Thorsten Hater (Guest)
on 2009-07-14 16:05
(Received via mailing list)
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
29194588628f0c08917c93d1b5793d3a?d=identicon&s=25 Matthias Reitinger (Guest)
on 2009-07-20 05:06
(Received via mailing list)
Daniel Moore 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")
  => #<Prototype 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")
  => #<Prototype name="Another">
  >> another_coffee_shop.prototype = new_coffee_shop
  => #<Prototype name="New">
  >> another_coffee_shop.drink_size
  => 3
  >> another_coffee_shop.prototypes
  => [#<Prototype name="New">, #<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
33117162fff8a9cf50544a604f60c045?d=identicon&s=25 Daniel X Moore (yahivin)
on 2009-07-26 20:21
(Received via mailing list)
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 Masover'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 Hater 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 Reitinger 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!
Please log in before posting. Registration is free and takes only a minute.
Existing account

NEW: Do you have a Google/GoogleMail, Yahoo or Facebook account? No registration required!
Log in with Google account | Log in with Yahoo account | Log in with Facebook account
No account? Register here.