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:
- it is not Ruby:
#normal method:
def reverse(array)
end
#multiple dispatched method:
multi(:reverse, []){}
multi(:reverse, Array){}
-
it is checking for argument types, not their “shape” (read further
to
understand it) -
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). -
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§;
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.