# Myrrha (v1.2.2)
[![Build Status](https://secure.travis-ci.org/blambeau/myrrha.png)](http://travis-ci.org/blambeau/myrrha)
[![Dependency Status](https://gemnasium.com/blambeau/myrrha.png)](https://gemnasium.com/blambeau/myrrha)
## Description
Myrrha provides the coercion framework which is missing to Ruby, IMHO. Coercions
are simply defined as a set of rules for converting values from source to target
domains (in an abstract sense). As a typical and useful example, it comes bundled
with a coerce() method providing a unique entry point for converting a string to
a numeric, a boolean, a date, a time, an URI, and so on.
### Install
% [sudo] gem install myrrha
### Bundler & Require
# Bug fixes (tiny) do not even add new default rules to coerce and
# to\_ruby\_literal. Minor version can, which could break your code.
# Therefore, please always use:
gem "myrrha", "~> 1.2.2"
## Links
* http://www.rubydoc.info/gems/myrrha/1.1.0/file/README.md (read this file there!)
* http://github.com/blambeau/myrrha (source code)
* http://rubygems.org/gems/myrrha (download)
## The coerce()
feature
Myrrha.coerce(:anything, Domain)
coerce(:anything, Domain) # with core extensions
### What for?
Having a single entry point for coercing values from one data-type (typically
a String) to another one is very useful. Unfortunately, Ruby does not provide
such a unique entry point... Thanks to Myrrah, the following scenario is
possible and even straightforward:
require 'myrrha/with_core_ext'
require 'myrrha/coerce'
require 'date'
values = ["12", "true", "2011-07-20"]
types = [Integer, Boolean, Date]
values.zip(types).collect do |value,domain|
coerce(value, domain)
end
# => [12, true, #]
### Implemented coercions
Implemented coercions are somewhat conservative, and only use a subset of what
ruby provides here and there. This is to avoid strangeness ala PHP... The
general philosophy is to provide the natural coercions we apply everyday.
The master rules are
* coerce(value, Domain)
return value
if
belongs_to?(value, Domain)
is true (see last section below)
* coerce(value, Domain)
returns Domain.coerce(value)
if the latter method exists.
* coerce("any string", Domain)
returns Domain.parse(value)
if the latter method exists.
The specific implemented rules are
require 'myrrha/with_core_ext'
require 'myrrha/coerce'
# NilClass -> _Anything_ returns nil, always
coerce(nil, Integer) # => nil
# Object -> String, via ruby's String()
coerce("hello", String) # => "hello"
coerce(:hello, String) # => "hello"
# String -> Numeric, through ruby's Integer() and Float()
coerce("12", Integer) # => 12
coerce("12.0", Float) # => 12.0
# String -> Numeric is smart enough:
coerce("12", Numeric) # => 12 (Integer)
coerce("12.0", Numeric) # => 12.0 (Float)
# String -> Regexp, through Regexp.compile
coerce("[a-z]+", Regexp) # => /[a-z]+/
# String -> Symbol, through to_sym
coerce("hello", Symbol) # => :hello
# String -> Boolean (hum, sorry Matz!)
coerce("true", Boolean) # => true
coerce("false", Boolean) # => false
coerce("true", TrueClass) # => true
coerce("false", FalseClass) # => false
# String -> Date, through Date.parse
require 'date'
coerce("2011-07-20", Date) # => #
# String -> Time, through Time.parse (just in time issuing of require('time'))
coerce("2011-07-20 10:57", Time) # => 2011-07-20 10:57:00 +0200
# String -> URI, through URI.parse
require 'uri'
coerce('http://google.com', URI) # => #
# String -> Class and Module through constant lookup
coerce("Integer", Class) # => Integer
coerce("Myrrha::Version", Module) # => Myrrha::Version
# Symbol -> Class and Module through constant lookup
coerce(:Integer, Class) # => Integer
coerce(:Enumerable, Module) # => Enumerable
### No core extension? no problem!
require 'myrrha/coerce'
Myrrha.coerce("12", Integer) # => 12
Myrrha.coerce("12.0", Float) # => 12.0
Myrrha.coerce("true", Myrrha::Boolean) # => true
# [... and so on ...]
### Adding your own coercions
The easiest way to add additional coercions is to implement a coerce
method on you class; it will be used in priority.
class Foo
def initialize(arg)
@arg = arg
end
def self.coerce(arg)
Foo.new(arg)
end
end
Myrrha.coerce(:hello, Foo)
# => #
If Foo
is not your code and you don't want to make core extensions
by adding a coerce
class method, you can simply add new rules to
Myrrha itself:
Myrrha::Coerce.append do |r|
r.coercion(Symbol, Foo) do |value, _|
Foo.new(value)
end
end
Myrrha.coerce(:hello, Foo)
# => #
Now, doing so, the new coercion rule will be shared with all Myrrha users, which
might be intrusive. Why not using your own set of coercion rules?
MyRules = Myrrha::Coerce.dup.append do |r|
r.coercion(Symbol, Foo) do |value, _|
Foo.new(value)
end
end
# Myrrha.coerce is actually a shortcut for:
Myrrha::Coerce.apply(:hello, Foo)
# => Myrrha::Error: Unable to coerce `hello` to Foo
MyRules.apply(:hello, Foo)
# => #
## The to\_ruby\_literal()
feature
Myrrha.to_ruby_literal([:anything])
[:anything].to_ruby_literal # with core extensions
### What for?
Object#to\_ruby\_literal
has a very simple specification. Given an
object o that can be considered as a true _value_, the result of
o.to\_ruby\_literal
must be such that the following invariant
holds:
Kernel.eval(o.to_ruby_literal) == o
That is, parsing & evaluating the literal yields the same value. When generating
(human-readable) ruby code, having a unique entry point that respects the
specification is very useful.
For almost all ruby classes, but not all, using o.inspect respects the
invariant. For example, the following is true:
Kernel.eval("hello".inspect) == "hello" # => true
Kernel.eval([1, 2, 3].inspect) == [1, 2, 3] # => true
Kernel.eval({:key => :value}.inspect) == {:key => :value} # => true
# => true
Unfortunately, this is not always the case:
Kernel.eval(Date.today.inspect) == Date.today
# => false
# => because Date.today.inspect yields "#Object#to\_ruby\_literal that works:
require 'date'
require 'myrrha/with_core_ext'
require 'myrrha/to_ruby_literal'
1.to_ruby_literal # => "1"
Date.today.to_ruby_literal # => "Marshal.load('...')"
["hello", Date.today].to_ruby_literal # => "['hello', Marshal.load('...')]"
Myrrha implements a best-effort strategy to return a human readable string. It
simply fallbacks to Marshal.load(...)
when the strategy fails:
(1..10).to_ruby_literal # => "1..10"
today = Date.today
(today..today+1).to_ruby_literal # => "Marshal.load('...')"
### No core extension? no problem!
require 'date'
require 'myrrha/to_ruby_literal'
Myrrha.to_ruby_literal(1) # => 1
Myrrha.to_ruby_literal(Date.today) # => Marshal.load("...")
# [... and so on ...]
### Adding your own rules
The easiest way is simply to override to\_ruby\_literal
in your
class
class Foo
attr_reader :arg
def initialize(arg)
@arg = arg
end
def to_ruby_literal
"Foo.new(#{arg.inspect})"
end
end
Myrrha.to_ruby_literal(Foo.new(:hello))
# => "Foo.new(:hello)"
As with coerce, contributing your own rule to Myrrha is possible:
Myrrha::ToRubyLiteral.append do |r|
r.coercion(Foo) do |foo, _|
"Foo.new(#{foo.arg.inspect})"
end
end
Myrrha.to_ruby_literal(Foo.new(:hello))
# => "Foo.new(:hello)"
And building your own set of rules is possible as well:
MyRules = Myrrha::ToRubyLiteral.dup.append do |r|
r.coercion(Foo) do |foo, _|
"Foo.new(#{foo.arg.inspect})"
end
end
# Myrrha.to_ruby_literal is actually a shortcut for:
Myrrha::ToRubyLiteral.apply(Foo.new(:hello))
# => "Marshal.load('...')"
MyRules.apply(Foo.new(:hello))
# => "Foo.new(:hello)"
### Limitation
As the feature fallbacks to marshaling, everything which is marshalable will
work. As usual, to\_ruby\_literal(Proc)
won't work.
## The general coercion framework
A set of coercion rules can simply be created from scratch as follows:
Rules = Myrrha.coercions do |r|
# `upon` rules are tried in priority if PRE holds
r.upon(SourceDomain) do |value, requested_domain|
# PRE: - user wants to coerce `value` to a requested_domain
# - belongs_to?(value, SourceDomain)
# implement the coercion or throw(:newrule)
returned_value = something(value)
# POST: belongs_to?(returned_value, requested_domain)
end
# `coercion` rules are then tried in order if PRE holds
r.coercion(SourceDomain, TargetDomain) do |value, requested_domain|
# PRE: - user wants to coerce `value` to a requested_domain
# - belongs_to?(value, SourceDomain)
# - subdomain?(TargetDomain, requested_domain)
# implement the coercion or throw(:newrule)
returned_value = something(value)
# POST: returned_value belongs to requested_domain
end
# fallback rules are tried if everything else has failed
r.fallback(SourceDomain) do |value, requested_domain|
# exactly the same as upon rules
end
end
When the user invokes Rules.apply(value, domain)
all rules for
which PRE holds are executed in order, until one succeed (chain of
responsibility design pattern). This means that coercions always execute in
O(number of rules)
.
### Specifying converters
A converter is the third (resp. second) element specified in a coercion rules
(resp. an upon or fallback rule). A converter is generally a Proc of arity 2,
which is passed the source value and requested target domain.
Myrrha.coercions do |r|
r.coercion String, Numeric, lambda{|value,requested_domain|
# this is converter code
}
end
convert("12", Integer)
A converter may also be specified as an array of domains. In this case, it is
assumed that they for a path inside the convertion graph. Consider for example
the following coercion rules (contrived example)
rules = Myrrha.coercions do |r|
r.coercion String, Symbol, lambda{|s,t| s.to_sym } # 1
r.coercion Float, String, lambda{|s,t| s.to_s } # 2
r.coercion Integer, Float, lambda{|s,t| Float(s) } # 3
r.coercion Integer, Symbol, [Float, String] # 4
end
The last rule specifies a convertion path, through intermediate domains. The
complete rule specifies that applying the following path will work
Integer -> Float -> String -> Symbol
#3 #2 #1
Indeed,
rules.coerce(12, Symbol) # => :"12.0"
### Semantics of belongs\_to?
and subdomain?
The pseudo-code given above relies on two main abstractions. Suppose the user
makes a call to coerce(value, requested_domain)
:
* belongs\_to?(value, SourceDomain)
is true iif
* SourceDomain
is a Proc
of arity 2, and
SourceDomain.call(value, requested_domain)
yields true
* SourceDomain
is a Proc
of arity 1, and
SourceDomain.call(value)
yields true
* SourceDomain === value
yields true
* subdomain?(SourceDomain,TargetDomain)
is true iif
* SourceDomain == TargetDomain
yields true
* TargetDomain respond to :superdomain_of?
and answers true on
SourceDomain
* SourceDomain and TargetDomain are both classes and the latter is a super
class of the former
### Advanced rule examples
Rules = Myrrha.coercions do |r|
# A 'catch-all' upon rule, always fired
catch_all = lambda{|v,rd| true}
r.upon(catch_all) do |value, requested_domain|
if you_can_coerce?(value)
# then do it!
else
throw(:next_rule)
end
end
# Delegate every call to the requested domain if it responds to compile
compilable = lambda{|v,rd| rd.respond_to?(:compile)}
r.upon(compilable) do |value, requested_domain|
requested_domain.compile(value)
end
# A fallback strategy if everything else fails
r.fallback(Object) do |value, requested_domain|
# always fired after everything else
# this is your last change, an Myrrha::Error will be raised if you fail
end
end
### Factoring domains through specialization by constraint
Specialization by constraint (SByC) is a theory of types for which the following
rules hold:
* A type (aka domain) is a set of values
* A sub-type is a subset
* A sub-type can therefore be specified through a predicate on the super domain
For example, "positive integers" is a sub type of "integers" where the predicate
is "value > 0".
Myrrha comes with a small feature allowing you to create types 'ala' SByC:
PosInt = Myrrha.domain(Integer){|i| i > 0}
PosInt.name # => "PosInt"
PosInt.class # => Class
PosInt.superclass # => Integer
PosInt.ancestors # => [PosInt, Integer, Numeric, Comparable, Object, Kernel, BasicObject]
PosInt === 10 # => true
PosInt === -1 # => false
PosInt.new(10) # => 10
PosInt.new(-10) # => ArgumentError, "Invalid value -10 for PosInt"
Note that the feature is very limited, and is not intended to provide a truly
coherent typing framework. For example:
10.is_a?(PosInt) # => false
10.kind_of?(PosInt) # => false
Instead, Myrrha domains are only provided as an helper to build sound coercions
rules easily while 1) keeping a Class-based approach to source and target
domains and 2) having friendly error messages 3) really supporting true
reasoning on types and value:
# Only a rule that converts String to Integer
rules = Myrrha.coercions do |r|
r.coercion String, Integer, lambda{|s,t| Integer(s)}
end
# it succeeds on both integers and positive integers
rules.coerce("12", Integer) # => 12
rules.coerce("12", PosInt) # => 12
# and correctly fails in each case!
rules.coerce("-12", Integer) # => -12
rules.coerce("-12", PosInt) # => ArgumentError, "Invalid value -12 for PosInt"
Note that if you want to provide additional tooling to your factored domain,
the following way of creating them also works:
class PosInt < Integer
extend Myrrha::Domain
def self.predicate
@predicate ||= lambda{|i| i > 0}
end
end