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:
#!/usr/bin/env ruby
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:
#!/usr/bin/env ruby
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:
#!/usr/bin/env ruby
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
:
#!/usr/bin/env ruby
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!
:
#!/usr/bin/env ruby
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
:
#!/usr/bin/env ruby
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
:
#!/usr/bin/env ruby
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:
#!/usr/bin/env ruby
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:
#!/usr/bin/env ruby
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:
#!/usr/bin/env ruby
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:
#!/usr/bin/env ruby
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 method from a Ryo property by the presence
or absence of an argument. Consider the following example,
where a property collides with the Kernel#then
method:
#!/usr/bin/env ruby
require "ryo"
ryo = Ryo::Object(then: 12)
# Resolves to Ryo property
p ryo.then # => 12
# Resolves to Kernel#then
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 that doesn't quite capture the fact that Ryo
uses duck typing: any object that implements #each_pair
is similar to a Hash, and any object that implements
#each
is similar to an Array. Note that only
Ryo.from,
Ryo::Object.from
and
Ryo::BasicObject.from
can handle Array-like objects.
The following example implements #each_pair
:
#!/usr/bin/env ruby
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
Documentation
A complete API reference is available at 0x1eef.github.io/x/ryo.rb
Install
Ryo can be installed via rubygems.org:
gem install ryo.rb
Sources
Thanks
Thanks to @awfulcooking (mooff) for the helpful discussions