README.md in myrrha-1.2.2 vs README.md in myrrha-2.0.0
- old
+ new
@@ -6,21 +6,21 @@
## 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.
+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
+### 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.
+ # 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
@@ -35,97 +35,97 @@
### 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
+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, #<Date: 2011-07-20 (...)>]
### 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
+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
-* <code>coerce(value, Domain)</code> return <code>value</code> if
+* <code>coerce(value, Domain)</code> returns <code>value</code> if
<code>belongs_to?(value, Domain)</code> is true (see last section below)
* <code>coerce(value, Domain)</code> returns <code>Domain.coerce(value)</code>
if the latter method exists.
* <code>coerce("any string", Domain)</code> returns <code>Domain.parse(value)</code>
if the latter method exists.
-The specific implemented rules are
+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
-
+ 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
+
+ # String -> Date, through Date.parse
require 'date'
- coerce("2011-07-20", Date) # => #<Date: 2011-07-20 (4911525/2,0,2299161)>
-
+ coerce("2011-07-20", Date) # => #<Date: 2011-07-20 (4911525/2,0,2299161)>
+
# 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) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
+ coerce('http://google.com', URI) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
# 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
@@ -138,101 +138,101 @@
end
def self.coerce(arg)
Foo.new(arg)
end
end
-
- Myrrha.coerce(:hello, Foo)
+
+ Myrrha.coerce(:hello, Foo)
# => #<Foo:0x869eee0 @arg=:hello>
-If <code>Foo</code> is not your code and you don't want to make core extensions
-by adding a <code>coerce</code> class method, you can simply add new rules to
+If <code>Foo</code> is not your code and you don't want to monkey patch the class
+by adding a <code>coerce</code> 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)
+
+ Myrrha.coerce(:hello, Foo)
# => #<Foo:0x8866f84 @arg=:hello>
-Now, doing so, the new coercion rule will be shared with all Myrrha users, which
+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
-
+ 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)
+
+ MyRules.apply(:hello, Foo)
# => #<Foo:0x8b7d254 @arg=:hello>
## The <code>to\_ruby\_literal()</code> feature
- Myrrha.to_ruby_literal([:anything])
+ Myrrha.to_ruby_literal([:anything])
[:anything].to_ruby_literal # with core extensions
### What for?
-<code>Object#to\_ruby\_literal</code> has a very simple specification. Given an
-object o that can be considered as a true _value_, the result of
-<code>o.to\_ruby\_literal</code> must be such that the following invariant
+<code>Object#to\_ruby\_literal</code> has a very simple specification. Given an
+object o that can be considered as a true _value_, the result of
+<code>o.to\_ruby\_literal</code> must be such that the following invariant
holds:
- Kernel.eval(o.to_ruby_literal) == o
+ 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.
+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
+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
+ # => false
# => because Date.today.inspect yields "#<Date: 2011-07-20 ...", which is a comment
### Example
-Myrrha implements a very simple set of rules for implementing
+Myrrha implements a very simple set of rules for implementing
<code>Object#to\_ruby\_literal</code> that works:
require 'date'
require 'myrrha/with_core_ext'
require 'myrrha/to_ruby_literal'
-
- 1.to_ruby_literal # => "1"
+
+ 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 <code>Marshal.load(...)</code> 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
@@ -247,25 +247,25 @@
end
def to_ruby_literal
"Foo.new(#{arg.inspect})"
end
end
-
+
Myrrha.to_ruby_literal(Foo.new(:hello))
- # => "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)"
-
+ # => "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})"
@@ -273,64 +273,64 @@
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)"
-
+ # => "Foo.new(:hello)"
+
### Limitation
As the feature fallbacks to marshaling, everything which is marshalable will
-work. As usual, <code>to\_ruby\_literal(Proc)</code> won't work.
+work. As usual, <code>to\_ruby\_literal(Proc)</code> 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)
-
+ 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 <code>Rules.apply(value, domain)</code> 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
+
+When the user invokes <code>Rules.apply(value, domain)</code> 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
<code>O(number of rules)</code>.
### Specifying converters
A converter is the third (resp. second) element specified in a coercion rules
@@ -341,77 +341,77 @@
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
+
+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 <code>belongs\_to?</code> and <code>subdomain?</code>
+ rules.coerce(12, Symbol) # => :"12.0"
-The pseudo-code given above relies on two main abstractions. Suppose the user
+### Semantics of <code>belongs\_to?</code> and <code>subdomain?</code>
+
+The pseudo-code given above relies on two main abstractions. Suppose the user
makes a call to <code>coerce(value, requested_domain)</code>:
* <code>belongs\_to?(value, SourceDomain)</code> is true iif
- * <code>SourceDomain</code> is a <code>Proc</code> of arity 2, and
+ * <code>SourceDomain</code> is a <code>Proc</code> of arity 2, and
<code>SourceDomain.call(value, requested_domain)</code> yields true
- * <code>SourceDomain</code> is a <code>Proc</code> of arity 1, and
+ * <code>SourceDomain</code> is a <code>Proc</code> of arity 1, and
<code>SourceDomain.call(value)</code> yields true
* <code>SourceDomain === value</code> yields true
* <code>subdomain?(SourceDomain,TargetDomain)</code> is true iif
* <code>SourceDomain == TargetDomain</code> yields true
- * TargetDomain respond to <code>:superdomain_of?</code> and answers true on
- SourceDomain
- * SourceDomain and TargetDomain are both classes and the latter is a super
- class of the former
-
+ * TargetDomain respond to <code>:superdomain_of?</code> 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}
+ 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)}
+ compilable = lambda{|v,rd| rd.respond_to?(:compile)}
r.upon(compilable) do |value, requested_domain|
requested_domain.compile(value)
- end
-
+ 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
@@ -420,11 +420,11 @@
* 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".
+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"
@@ -433,41 +433,37 @@
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
+ 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
-
+ 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,
+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
-
+ extend Myrrha::Domain.new(Integer, [], lambda{|i| i > 0})
+
end