[QUIZ] Prototype-Based Inheritance (#214)

D

Daniel Moore

-=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-

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

-=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-

## Prototype-Based Inheritance (#214)

Ay=FA 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 =3D {:name =3D> "Pele", :sport =3D> "football", :position =3D> "Fo=
rward"}
pele_jr =3D {:name =3D> "Pele Jr."}
pele_jr.prototype =3D pele
pele_jr.name # =3D> "Pele Jr."
pele_jr.sport # =3D> "football"
pele_jr.position # =3D> "forward"

You may notice that this doesn't work: `NoMethodError: undefined
method 'prototype' for {:name =3D> "Pele", :sport =3D> "football",
:position =3D> "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 =3D starbucks
new_coffee_shop.drink_size +=3D 1

Have Fun!

[1]: http://en.wikipedia.org/wiki/Prototype-based_programming

--=20
-Daniel
http://rubyquiz.strd6.com
 
L

lith

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

What about:


class Hash

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

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

end


This would allow you to do something like:

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

David Masover

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
 
T

Thorsten Hater

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

David said:
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


--
Thorsten Hater
Institut fuer Theoretische Physik I
Ruhr-Universitaet Bochum
E-Mail: (e-mail address removed)
Tel.: 0234/32-23-441
 
M

Matthias Reitinger

Daniel said:
## 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:
ArgumentError: circular prototype chain
from ./prototype.rb:8:in `prototype='
from (irb):23
-Matthias
 
D

Daniel Moore

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!
 

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,764
Messages
2,569,566
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top