Pattern-matching for method arguments in Ruby

  • Thread starter Victor \Zverok\ Shepelev
  • Start date

V

Victor \Zverok\ Shepelev

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') #=> "<tag>"
puts printTag('tag', [['a','b'], ['c','d']]) #=> "<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.
 
Ad

Advertisements


Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top