Ryo implements prototype-based inheritance, in Ruby.
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]
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]
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
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::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]
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
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]
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
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]
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 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]
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
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
Thanks to @awfulcooking (mooff) for the helpful discussions and advice.
BSD Zero Clause.
See LICENSE.