About
Ryo implements prototype-based inheritance, in Ruby.
Examples
Prototypes
Point object
The following example demonstrates how prototype-based inheritance is implemented in Ryo. The example introduces three objects to form a single point object with the properties, "x" and "y". The Ryo() method seen in the example returns an instance of Ryo::Object:
require "ryo"
point_x = Ryo(x: 5)
point_y = Ryo({y: 10}, point_x)
point = Ryo({}, point_y)
p [point.x, point.y]
##
# [5, 10]
Ryo.fn
The following example demonstrates a Ryo function.
Ryo.fn
will bind its self
to the Ryo object it is assigned to, and when the function
is called it will have access to the properties of the Ryo object:
require "ryo"
point_x = Ryo(x: 5)
point_y = Ryo({y: 10}, point_x)
point = Ryo({
multiply: Ryo.fn { |m| [x * m, y * m] }
}, point_y)
p point.multiply.call(2)
##
# [10, 20]
Ryo.memo
The following example demonstrates
Ryo.memo
.
Ryo.memo
returns a value that becomes memoized after a property is
accessed for the first time. It is similar to a Ryo function:
require "ryo"
point_x = Ryo(x: Ryo.memo { 5 })
point_y = Ryo({y: Ryo.memo { 10 }}, point_x)
point = Ryo({sum: Ryo.memo { x + y }}, point_y)
print "point.x = ", point.x, "\n"
print "point.y = ", point.y, "\n"
print "point.sum = ", point.sum, "\n"
##
# point.x = 5
# point.y = 10
# point.sum = 15
Iteration
Ryo.each
The
Ryo.each
method can iterate through the properties of a Ryo object, and
its prototype(s). Ryo is designed to not mix its implementation
with the objects it creates - that's why
Ryo.each
is not implemented directly on a Ryo object.
A demonstration of Ryo.each
:
require "ryo"
point = Ryo(x: 10, y: 20)
Ryo.each(point) do |key, value|
p [key, value]
end
##
# ["x", 10]
# ["y", 20]
Ryo.map!
Ryo::Enumerable
methods can return a new copy of a Ryo object and its prototypes, or mutate
a Ryo object and its prototypes in-place. The following example demonstrates
an in-place map operation on a Ryo object with
Ryo.map!
.
The counterpart of
Ryo.map!
is
Ryo.map
,
and it returns a new copy of a Ryo object and its prototypes.
A demonstration of Ryo.map!
:
require "ryo"
point_x = Ryo(x: 2)
point_y = Ryo({y: 4}, point_x)
point = Ryo({}, point_y)
Ryo.map!(point) { |key, value| value * 2 }
p [point.x, point.y]
p [point_x.x, point_y.y]
##
# [4, 8]
# [4, 8]
Ancestors
All Ryo::Enumerable
methods support an optional ancestors
option.
ancestors
is an integer that determines how far up the prototype chain a
Ryo::Enumerable
method can go. 0 covers a Ryo object, and none of the prototypes in its
prototype chain. 1 covers a Ryo object, and one of the prototypes in its
prototype chain - and so on.
When the ancestors
option is not provided, the default behavior of
Ryo::Enumerable
methods is to traverse the entire prototype chain. The following example
demonstrates using the ancestors
option with
Ryo.find
:
require "ryo"
point_x = Ryo(x: 5)
point_y = Ryo({y: 10}, point_x)
point = Ryo({}, point_y)
p Ryo.find(point, ancestors: 0) { |k,v| v == 5 } # => nil
p Ryo.find(point, ancestors: 1) { |k,v| v == 5 } # => nil
p Ryo.find(point, ancestors: 2) { |k,v| v == 5 }.x # => point_x.x
p Ryo.find(point) { |k,v| v == 5 }.x # => point_x.x
Recursion
Ryo.from
The Ryo.from
method has
the same interface as the Ryo
method, but it is implemented to recursively walk a Hash object and create Ryo objects
from other Hash objects found along the way. Recursion is not the default behavior
because it has the potential to be slow when given a complex Hash object that's
very large - otherwise there shouldn't be a noticeable performance impact.
The following example demonstrates Ryo.from
:
require "ryo"
point = Ryo.from({
x: {to_i: 0},
y: {to_i: 10}
})
p [point.x.to_i, point.y.to_i]
##
# [0, 10]
Ryo.from with an Array
The Ryo.from
method can
walk an Array object, and create Ryo objects from Hash objects found along the way.
An object that can't be turned into a Ryo object is left as-is. The following
example demonstrates how that works in practice:
require "ryo"
points = Ryo.from([
{x: {to_i: 2}},
"foobar",
{y: {to_i: 4}}
])
p points[0].x.to_i
p points[1]
p points[2].y.to_i
##
# 2
# "foobar"
# 4
Ryo.from with OpenStruct
All methods that can create Ryo objects support turning a Struct, or OpenStruct object
into a Ryo object. The following example demonstrates how
Ryo.from
can recursively turn an OpenStruct object into Ryo objects. The example also assigns
a prototype to the Ryo object created from the OpenStruct:
require "ryo"
require "ostruct"
point = Ryo.from(
OpenStruct.new(x: {to_i: 5}),
Ryo.from(y: {to_i: 10})
)
p [point.x.to_i, point.y.to_i]
##
# [5, 10]
BasicObject
Ryo::BasicObject
All of the previous examples have been working with instances of Ryo::Object, a subclass of Ruby's Object class. In comparison, Ryo::BasicObject - a subclass of Ruby's BasicObject class, provides an object with fewer methods. The following example demonstrates how to create an instance of Ryo::BasicObject:
require "ryo"
point_x = Ryo::BasicObject(x: 0)
point_y = Ryo::BasicObject({y: 0}, point_x)
point = Ryo::BasicObject({}, point_y)
p [point.x, point.y]
##
# [0, 0]
Ryo::BasicObject.from
Ryo::BasicObject.from is identical to Ryo.from but rather than returning instance(s) of Ryo::Object it returns instance(s) of Ryo::BasicObject instead:
require "ryo"
point = Ryo::BasicObject.from({
x: {to_i: 2},
y: {to_i: 4}
})
p [point.x.to_i, point.y.to_i]
##
# [2, 4]
Collisions
Resolution strategy
When a property and method collide, Ryo tries to find the best resolution. Since Ryo properties don't accept arguments, and methods can - we are able to distinguish a property from a method in many cases.
Consider this example, where a property collides with the Kernel#then
method. This example
would work the same for other methods that accept a block and/or arguments:
require "ryo"
ryo = Ryo::Object(then: 12)
p ryo.then # => 12
p ryo.then { 34 } # => 34
Beyond Hash objects
Duck typing
The documentation has used simple terms to describe the objects that Ryo works
with: Hash and Array objects. But in reality, Ryo uses duck typing, so any object
that implements #each_pair
can be treated as a Hash object, and any object that
implements #each
can be treated as an Array object. Note that only
Ryo.from,
Ryo::Object.from
and
Ryo::BasicObject.from
can handle Array/#each objects.
Here's an example of how to turn your own custom object, which implements
#each_pair
, into a Ryo object:
require "ryo"
class Point
def initialize
@x = 5
@y = 10
end
def each_pair
yield("x", @x)
yield("y", @y)
end
end
point = Ryo(Point.new)
p point.x # => 5
p point.y # => 10
Rubygems.org
Ryo can be installed via rubygems.org.
gem install ryo.rb
Sources
Thanks
Thanks to @awfulcooking (mooff) for the helpful discussions and advice.
License
BSD Zero Clause.
See LICENSE.