Project

ryo.rb

0.0
The project is in a healthy, maintained state
Ryo implements prototype-based inheritance, in Ruby.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 3.5
~> 3.10
~> 1.9
~> 0.9
 Project Readme

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.