pattern-match icon indicating copy to clipboard operation
pattern-match copied to clipboard

A pattern matching library for Ruby

= 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 Object#match.

require 'pattern-match' using PatternMatch

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 ordinary object is a value pattern.

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

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

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

match(0) do with(_(Integer, :==)) { :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 class(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 & Integer, ! ([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 _, _?, __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 & Integer, 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 & Integer), ___]) do a #=> [:a, :b] b #=> [0, 1] end end

=== EXPERIMENTAL

  • Object.()
  • Matcher
    • KeyMatcher
      • Hash.()
    • AttributeMatcher
  • fallthrough (requires binding_of_caller gem)

To use experimental features, you must also require 'pattern-match/experimental'. 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)

require 'pattern-match/experimental'

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

match('0') do with(/\d+/.(a << :to_i)) do a #=> 0 end end

match([Set[0, 1, 2], Set[3, 4]]) do with(_[Set.(a, b), Set.(c)], guard { a + b * c == 2 } ) do [a, b, c] #=> [2, 0, 3] end end

match([]) do with([]) do opts = {} fallthrough end with([opts & Hash]) do opts #=> {} end end

  • {tokland/RubyTextProcessing}[https://github.com/tokland/tokland/wiki/RubyTextProcessing]
  • {yhara/tapl-ruby}[https://github.com/yhara/tapl-ruby]
  • {k-tsj/power_assert}[https://github.com/k-tsj/power_assert/blob/8e9e0399a032936e3e3f3c1f06e0d038565f8044/lib/power_assert.rb]

== Reference

  • {Pattern Matching in Ruby (at Sapporo RubyKaigi 2012) // Speaker Deck}[https://speakerdeck.com/k_tsj/patternmatchinginruby]

== 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