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

License

BSD Zero Clause
See LICENSE