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