README.md in myrrha-1.0.0 vs README.md in myrrha-1.1.0
- old
+ new
@@ -1,6 +1,6 @@
-# Myrrha
+# Myrrha (v1.1.0)
## 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
@@ -15,19 +15,19 @@
### 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 "alf", "~> 1.0.0"
+ gem "myrrha", "~> 1.0.0"
## Links
-* http://rubydoc.info/github/blambeau/myrrha/master/frames (read this file there!)
+* 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 missing <code>coerce()</code>
+## The <code>coerce()</code> feature
Myrrha.coerce(:anything, Domain)
coerce(:anything, Domain) # with core extensions
### What for?
@@ -46,38 +46,75 @@
values.zip(types).collect do |value,domain|
coerce(value, domain)
end
# => [12, true, #<Date: 2011-07-20 (...)>]
-### Example
+### 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
+
+* <code>coerce(value, Domain)</code> return <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
+
require 'myrrha/with_core_ext'
require 'myrrha/coerce'
- # it works on numerics
+ # 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
- # but also on regexp (through Regexp.compile)
+ # 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]+/
- # and, yes, on Boolean (sorry Matz!)
+ # 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
- # and on date and time (through Date/Time.parse)
+ # String -> Date, through Date.parse
require 'date'
- require 'time'
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
- # why not on URI?
+ # String -> URI, through URI.parse
require 'uri'
coerce('http://google.com', URI) # => #<URI::HTTP:0x8281ce0 URL:http://google.com>
- # on nil, it always returns nil
- coerce(nil, Integer) # => nil
+ # 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'
@@ -131,11 +168,11 @@
# => Myrrha::Error: Unable to coerce `hello` to Foo
MyRules.apply(:hello, Foo)
# => #<Foo:0x8b7d254 @arg=:hello>
-## The missing <code>to\_ruby\_literal()</code>
+## The <code>to\_ruby\_literal()</code> feature
Myrrha.to_ruby_literal([:anything])
[:anything].to_ruby_literal # with core extensions
### What for?
@@ -289,12 +326,46 @@
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>.
-### <code>belongs\_to?</code> and <code>subdomain?</code>
+### 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 <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
@@ -303,10 +374,12 @@
<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
### Advanced rule examples
@@ -332,6 +405,57 @@
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
+ 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"
+
+
+
\ No newline at end of file