Skip to content

Instantly share code, notes, and snippets.

@k-tsj
Created February 4, 2012 02:52
Show Gist options
  • Save k-tsj/1734716 to your computer and use it in GitHub Desktop.
Save k-tsj/1734716 to your computer and use it in GitHub Desktop.
Pattern match in ruby
# Copyright (C) 2012 Kazuki Tsujimoto, All rights reserved.
#
# You can redistribute it and/or modify it under the terms of the 2-clause BSDL.
class Pattern
attr_accessor :parent
attr_reader :env
def initialize(*children)
@parent = nil
@children = children.map {|i| i.is_a?(Pattern) ? i : PatternValue.new(i) }
@children.each do |i|
i.parent = self
end
@env = {}
end
def method_missing(name, *)
@as = PatternVariable.new(name)
@as.parent = self
self
end
def bind(name, val)
if @parent
@parent.bind(name, val)
else
@env[name] = val
end
end
end
class PatternExtractor < Pattern
def initialize(extractor, *patterns)
super(*patterns)
@extractor = extractor
end
def match(val)
unapplied_val = @extractor.unapply(val)
return false unless unapplied_val.length == @children.length
@children.zip(unapplied_val).all? do |pat, v|
pat.match(v)
end.tap {|ret| @as.match(val) if ret and @as }
end
end
class PatternVariable < Pattern
def initialize(name)
super()
@name = name
end
def match(val)
bind(@name, val)
true
end
end
class PatternValue < Pattern
def initialize(val)
super()
@val = val
end
def match(v)
@val === v
end
end
class PatternMatchingEnv < Object
def initialize(ctx, val)
@ctx = ctx
@val = val
@matched = false
end
def pattern(pat, &block)
if (! @matched) and pat.match(@val)
@matched = true
with_tmpenv(@ctx, pat.env, &block)
end
rescue PatternNotMatchError
end
def method_missing(name, *)
PatternVariable.new(name)
end
private
def with_tmpenv(obj, env, &block)
tmpenv_module(obj).instance_eval do
begin
env.each do|name, val|
define_method(name) { val }
end
obj.instance_eval(&block)
ensure
env.each do |name, _|
remove_method(name)
end
end
end
end
def tmpenv_module(obj)
m = class << obj; self.ancestors.find{|i| i.respond_to? :tmpenv? }; end
unless m
m = Module.new do |m|
def tmpenv?; true; end
def m.method_added(name, *); end
end
obj.extend(m)
end
m
end
end
module Kernel
def match(expr, &block)
PatternMatchingEnv.new(self, expr).instance_eval(&block)
end
end
class PatternNotMatchError < Exception
end
module Extractor
def call(*patterns)
PatternExtractor.new(self, *patterns)
end
end
class Class
include Extractor
end
def Array.unapply(value)
case value
when Array
value
else
raise PatternNotMatchError.new
end
end
class Regexp
include Extractor
def unapply(value)
m = self.match(value.to_s)
raise PatternNotMatchError.new unless m
m.captures.empty? ? [m[0]] : m.captures
end
end
if $0 == __FILE__
class EMail
def self.unapply(value)
value.to_s.split(/@/).tap {|parts| raise PatternNotMatchError.new unless parts.length == 2}
end
end
match([1, "foo-bar@example.com"]) {
pattern(Array.(i, EMail.(/(\w+)-(\w+)/.(firstname, 'bar') :: name, domain) :: mail)) {
p [i, firstname, name, domain, mail] # => [1, "foo", "foo-bar", "example.com", "foo-bar@example.com"]
}
}
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment