From: "Victor \"Zverok\" Shepelev" Date: 2006-11-12T02:27:45+09:00 Subject: Pattern-matching for method arguments in Ruby Hi all. I think, pattern-matching for function argument is a rather good idea. I know, it it someway embodied into Multiple Dispatch Library[1], but it has some major drawbacks: 1. it is not Ruby: #normal method: def reverse(array) end #multiple dispatched method: multi(:reverse, []){} multi(:reverse, Array){} 2. it is checking for argument *types*, not their "shape" (read further to understand it) 3. it does only arguments checking, not their conversion, for ex., lists can't be divided into "head" and "tail" (therefore my note about "arguments shape": you can't say something like "argument 1 should be array with first element being hash). 4. it has no progress since v0.1 (December 21, 2005) *** And Now for Something Completely Different!!!*** (Oh, is it me citing Monthy *Python*? Shame on me! Chunky bacon! Chunky bacon!) The proposed code is rather proof-of-concept than library (it has some ugly fragments), but it is working. Some examples: "sample".pattern_match("sample") #=> [] "sample".pattern_match("other sample") #=>nil #array of remembered matches returned. #Empty array means "pattern matched, but nothing to remember" #Ok, it's very trivial. More complex: "sample".pattern_match(String) #=> ["sample"] #The example above checked, if "sample" match String pattern, #and remembers matched values "sample".pattern_match(Object) #=> ["sample"] "sample".pattern_match(Numeric) #=> nil #life becames more intersting: ["sample", 1].pattern_match(["sample", 1]) #=> [] ["sample", 1].pattern_match(["sample", Numeric]) #=> [1] ["sample", 1].pattern_match([String, Numeric]) #=> ["sample", 1] #patterns can be nested: ["sample", [1,2]].pattern_match([String, [Numeric, Numeric]]) #=> ["sample", 1, 2] #hashes also work {"sample" => [1,2]}.pattern_match({String => Array}) #=> ["sample", [1,2]] {"sample" => [1,2]}.pattern_match({String => [Integer, Integer]}) #=> ["sample", 1, 2] #splat operator used to say "tail", "the rest of array" ["sample", 1].pattern_match([String, *Object]) #=> ["sample", [1]] ["sample", 1,2,3,4].pattern_match([String, *Object]) #=> ["sample", [1,2,3,4]] ["sample"].pattern_match([String, *Object]) #=> ["sample", []] And, finally, the usage of above for multiple dispatch: ------------------------------------------------------- def reverse(*arg) arg.pmatch([]){ [] } || arg.pmatch([Object, *Object]){|first, tail| reverse(tail) + [first] } || arg.nomatch! end p reverse([1,2,3]) #=> [3,2,1] p reverse("") #in `Array#nomatch!': Can't find match for [""] (TypeError) Isn't it SEXY? :) More complex example: def printTag(*arg) arg.pmatch(String){|tag| printTag(tag, []) } || arg.pmatch(String, []){|tag| "<#{tag}>" } || arg.pmatch(String, Array){|tag, attr| "<#{tag}#{printAttrs(attr)}>" } || arg.nomatch! end def printAttrs(*arg) arg.pmatch([]){ "" } || arg.pmatch([ [String, String], *Object]){|key, value, tail| " #{key}='#{value}'" + printAttrs(tail) } || arg.nomatch! end puts printTag('tag') #=> "" puts printTag('tag', [['a','b'], ['c','d']]) #=> "" No more word. Here's the code (a bit ugly, sorry): ------- class Object def pattern_match(pattern) pattern.match_by(self) end end class Object def match_by(obj) obj == self && [] end end class Class def match_by(obj) obj.kind_of?(self) && [obj] end end class Array def match_by(obj) if !self.empty? && self.last == :splat head, tail = obj[0...self.size-1], obj[self.size-1..-1] res = head.pattern_match(self[0..-2]) res ? res + [tail] : nil else obj.instance_of?(Array) && obj.size == self.size && self.zip(obj).inject([]){|res, po| p,o = po; r = o.pattern_match(p); break unless r; res += r } end end end class Hash def match_by(obj) return false unless obj.instance_of?(Hash) && obj.size == self.size res = [] self.each{|k, v| pair = obj.detect{|ko, vo| rk = ko.pattern_match(k) rv = vo.pattern_match(v) if rk && rv res += rk + rv true else false end } return false unless pair } res end end class Class def to_splat; [:splat] end end class Array def pmatch(*patterns, &block) res = self.pattern_match(patterns) if res if block block[*res] else res end else nil end end def nomatch! raise TypeError, "Can't find match for #{self.inspect}" end end ------- That's all. Thanks. V.