pattern-match

About

A pattern matching library for Ruby.

Installation

$ gem install pattern-match

or

$ git clone git://github.com/k-tsj/pattern-match.git
$ cd pattern-match
$ gem build pattern-match.gemspec
$ gem install pattern-match-*.gem

or

$ gem install bundler (if you need)
$ echo "gem 'pattern-match', :git => 'git://github.com/k-tsj/pattern-match.git'" > Gemfile
$ bundle install --path vendor/bundle

Basic Usage

pattern-match library provides Kernel#match.

require 'pattern-match'

match(object) do
  with(pattern[, guard]) do
    ...
  end
  with(pattern[, guard]) do
    ...
  end
  ...
end

The patterns are run in sequence until the first one that matches.

If a pattern matches, a block passed to with is called and return its result. If no pattern matches, a PatternMatch::NoMatchingPatternError exception is raised.

You can specify pattern guard if you want.

Patterns

Value

An object (expect the instance of PatternMatch::Pattern) is a value pattern.

The pattern matches an object such that pattern === object.

match(0) do
  with(Fixnum) { :match } #=> :match
end

If you want to use an another method of matching, you have to use _ as follows.

match(0) do
  with(_(Fixnum, :==)) { :match }
end #=> NoMatchingPatternError

Deconstructor

A deconstructor pattern is (typically) of the form deconstructor.([pattern, ...]).

It is equivalent to Extractor in Scala.

Consider the following example:

match([0, 1]) do
  with(Array.(0, 1)) { :match } #=> :match
end

match('ab') do
  with(/(.)(.)/.('a', 'b')) { :match } #=> :match
end

Array, Regexp object(/(.)(.)/) are deconstructors. You can use any object has the following features as deconstructor.

  • PatternMatch::Deconstructable is included in a class of deconstructor

  • Can be responded to deconstruct method

Note that _[] is provided as syntactic sugar for Array.().

match([0, 1]) do
  with(_[0, 1]) { :match } #=> :match
end

Variable

An identifier is a variable pattern.

It matches any value, and binds the variable name to that value. A special case is the wild-card pattern _ which matches any value, and never binds.

match([0, 1]) do
  with(_[a, b]) { [a, b] } #=> [0, 1]
end

match(0) do
  with(_) { _ } #=> NameError
end

When several patterns with the same name occur in a single pattern, all objects bound to variable must be equal.

match([0, 1]) do
  with(_[a, a]) { a }
end #=> NoMatchingPatternError

And/Or/Not

PatternMatch::Pattern#&, PatternMatch::Pattern#|, PatternMatch::Pattern#!@, And, Or, Not return and/or/not pattern.

match([0, [1]]) do
  with(a & Finuxm, ! (_[2] | _[3])) { a } #=> 0
end

match(0) do
  with(0 | 1 | 2) { } # (0 | 1 | 2) is evaluated to 3, so the pattern does not match.
  with(Or(0, 1, 2)) { :match } #=> :match
end

Quantifier

_(triple underscore), _?, __n(double underscore + n where n >= 0), __n? are quantifier patterns.

They are equivalent to *, *?, {n,}, {n,}? in regular expression. You can write as *pattern instead of pattern, _.

match([:a, 0, :b, :c]) do
  with(_[a & Symbol, ___, b & Fixnum, c & Symbol, ___]) do
    a #=> [:a]
    b #=> 0
    c #=> [:b, :c]
  end
end

Sequence

Seq returns a sequence pattern.

It is equivalent to () in regular expression.

match([:a, 0, :b, 1]) do
  with(_[Seq(a & Symbol, b & Fixnum), ___]) do
    a #=> [:a, :b]
    b #=> [0, 1]
  end
end

EXPERIMENTAL

  • Object.()

  • Matcher

    • KeyMatcher

      • Hash.()

    • AttributeMatcher

See source code for more details.

Pattern guard

Pattern guard can be specified as a second argument to with.

match([1, 2, 3, 4, 5]) do
  with(_[*_, *a, *_], guard { a.inject(:*) == 12 }) do
    a #=> [3, 4]
  end
end

Examples

# (A)
Node = Struct.new(:left, :key, :right)
class R < Node; end
class B < Node; end

def balance(left, key, right)
  match([left, key, right]) do
    with(_[R.(a, x, b), y, R.(c, z, d)]) { R[B[a, x, b], y, B[c, z, d]] }
    with(_[R.(R.(a, x, b), y, c), z, d]) { R[B[a, x, b], y, B[c, z, d]] }
    with(_[R.(a, x, R.(b, y, c)), z, d]) { R[B[a, x, b], y, B[c, z, d]] }
    with(_[a, x, R.(b, y, R.(c, z, d))]) { R[B[a, x, b], y, B[c, z, d]] }
    with(_[a, x, R.(R.(b, y, c), z, d)]) { R[B[a, x, b], y, B[c, z, d]] }
    with(_) { B[left, key, right] }
  end
end

# (B)
class EMail
  def self.deconstruct(value)
    parts = value.to_s.split(/@/)
    if parts.length == 2
      parts
    else
      raise PatternMatch::PatternNotMatch
    end
  end
end

match(['[email protected]', '[email protected]']) do
  with(_[mail & EMail.(name & /(\w+)-(\w+)/.(firstname, 'bar'), domain), ___]) do
    mail      #=> ["[email protected]", "[email protected]"]
    name      #=> ["foo-bar", "baz-bar"]
    firstname #=> ["foo", "baz"]
    domain    #=> ["example.com", "example.com"]
  end
end

# (C)
def replace_repeated(obj, &block)
  ret = match(obj, &block)
  if ret == obj
    ret
  else
    replace_repeated(ret, &block)
  end
rescue PatternMatch::NoMatchingPatternError
  obj
end

replace_repeated([1, 2, 4, 4, 3, 3, 4, 0, 0]) do
  with(_[*a, x, x, *b]) { [*a, x, *b] }
end #=> [1, 2, 4, 3, 4, 0]

# (D)
match({a: 0, b: 1}) do
  with(Hash.(:a, b: Object.(:odd? => true))) do
    a #=> 0
  end
end

C = Struct.new(:a, :b) do
  include PatternMatch::AttributeMatcher
end
match(C[0, 1]) do
  with(C.(:b, a: 0)) do
    b # => 1
  end
end

Reference

Development

$ git clone git://github.com/k-tsj/pattern-match.git
$ cd pattern-match
$ gem install bundler (if you need)
$ bundle install --path vendor/bundle
$ bundle exec rake test (or "bundle exec rake")
$ bundle exec rake build

Travis Build Status <img src=“https://secure.travis-ci.org/k-tsj/pattern-match.png”/>