PatternMatching

A gem for adding Erlang-style function/method overloading through pattern matching to Ruby classes.

The project is hosted on the following sites:

Introduction

Ruby is my favorite programming by far. As much as I love Ruby I’ve always been a little disappointed that Ruby doesn’t support function overloading. Function overloading tends to reduce branching and keep function signatures simpler. No sweat, I learned to do without. Then I started programming in Erlang

I’ve really started to enjoy working in Erlang. Erlang is good at all the things Ruby is bad at and vice versa. Together, Ruby and Erlang make me happy. My favorite Erlang feature is, without question, pattern matching. Pattern matching is like function overloading cranked to 11. So one day I was musing on Twitter that I’d like to see Erlang-stype pattern matching in Ruby and one of my friends responded “Build it!” So I did. And here it is.

For fun I’ve also thrown in Erlang’s sparsely documented -behaviour functionality plus a few other functions and constants I find useful.

Goals

Features

For good -behavior(timeoff).

One of Ruby’s greatest strengths is duck typing. Usually this is awesome and I’m happy to not have to deal with static typing and the compiler. Usually. The problem with duck typing is that is is impossible in Ruby to enforce an interface definition. I would never advocate turning Ruby into the cesspool complex object creation that Java has unfortunately become, but occasionally it would be nice to make sure a class implements a set of required methods. Enter Erlang’s -behavior keyword. Basically, you define a behavior_info then drop a behavior call within a class. Forget to implement a required method and Ruby will let you know. See the examples below for details.

Supported Ruby versions

MRI 1.9.x and above. Anything else and your mileage may vary.

Install

gem install pattern-matching

or add the following line to Gemfile:

gem 'pattern-matching'

and run bundle install from your shell.

Once you’ve installed the gem you must require it in your project. Becuase this gem includes multiple features that not all users may want, several require options are available:

require 'behavior'
require 'behaviour' # alternate spelling
require 'pattern_matching'
require 'pattern_matching/functions'

If you want everything you can do that, too:

require 'pattern_matching/all'

PatternMatching

First, familiarize yourself with Erlang pattern matching. This gem may not make much sense if you don’t understand how Erlang dispatches functions.

In the Ruby class file where you want to use pattern matching, require the pattern_matching gem:

require 'pattern_matching'

Then include PatternMatching in your class:

require 'pattern_matching'

class Foo
  include PatternMatching

  ...

end

You can then define functions with defn instead of the normal def statement. The syntax for defn is:

defn(:symbol_name_of_function, zero, or, more, parameters) { |block, arguments|
  # code to execute
}

You can then call your new function just like any other:

require 'pattern_matching'

class Foo
  include PatternMatching

  defn(:hello) {
    puts "Hello, World!"
  }
end

foo = Foo.new
foo.hello #=> "Hello, World!"

Patterns to match against are included in the parameter list:

defn(:greet, :male) {
  puts "Hello, sir!"
}

defn(:greet, :female) {
  puts "Hello, ma'am!"
}

...

foo.hello(:male)   #=> "Hello, sir!"
foo.hello(:female) #=> "Hello, ma'am!"

If a particular method call can not be matched a NoMethodError is thrown with a reasonably helpful error message:

foo.greet(:unknown) #=> NoMethodError: no method `greet` matching [:unknown] found for class Foo
foo.greet           #=> NoMethodError: no method `greet` matching [] found for class Foo

Parameters that are expected to exist but that can take any value are considered unbound parameters. Unbound parameters are specified by the _ underscore character or UNBOUND:

defn(:greet, _) do |name|
  "Hello, #{name}!"
end

defn(:greet, UNBOUND, UNBOUND) do |first, last|
  "Hello, #{first} #{last}!"
end

...

foo.greet('Jerry') #=> "Hello, Jerry!"

All unbound parameters will be passed to the block in the order they are specified in the definition:

defn(:greet, _, _) do |first, last|
  "Hello, #{first} #{last}!"
end

...

foo.greet('Jerry', "D'Antonio") #=> "Hello, Jerry D'Antonio!"

If for some reason you don’t care about one or more unbound parameters within the block you can use the _ underscore character in the block parameters list as well:

defn(:greet, _, _, _) do |first, _, last|
  "Hello, #{first} #{last}!"
end

...

foo.greet('Jerry', "I'm not going to tell you my middle name!", "D'Antonio") #=> "Hello, Jerry D'Antonio!"

Hash parameters can match against specific keys and either bound or unbound parameters. This allows for function dispatch by hash parameters without having to dig through the hash:

defn(:hashable, {foo: :bar}) { |opts|
  :foo_bar
}
defn(:hashable, {foo: _}) { |f|
  f
}

...

foo.hashable({foo: :bar})      #=> :foo_bar
foo.hashable({foo: :baz})      #=> :baz

The Ruby idiom of the final parameter being a hash is also supported:

defn(:options, _) { |opts|
  opts
}

...

foo.options(bar: :baz, one: 1, many: 2)

As is the Ruby idiom of variable-length argument lists. The constant ALL as the last parameter will match one or more arguments and pass them to the block as an array:

defn(:baz, Integer, ALL) { |int, args|
  [int, args]
}
defn(:baz, ALL) { |args|
  args
}

Superclass polymorphism is supported as well. If an object cannot match a method signature it will defer to the parent class:

class Bar
  def greet
    return 'Hello, World!'
  end
end

class Foo < Bar
  include PatternMatching

  defn(:greet, _) do |name|
    "Hello, #{name}!"
  end
end

...

foo.greet('Jerry') #=> "Hello, Jerry!"
foo.greet          #=> "Hello, World!"

Guard clauses in Erlang are defined with when clauses between the parameter list and the function body. In Ruby, guard clauses are defined by chaining a call to when onto the the defn call and passing a block. If the guard clause evaluates to true then the function will match. If the guard evaluates to false the function will not match and pattern matching will continue:

Erlang:

old_enough(X) when X >= 16 -> true;
old_enough(_) -> false.

Ruby:

defn(:old_enough, _){ true }.when{|x| x >= 16 }
defn(:old_enough, _){ false }

Order Matters

As with Erlang, the order of pattern matches is significant. Patterns will be matched in the order declared and the first match will be used. If a particular function call can be matched by more than one pattern, the first matched pattern will be used. It is the programmer’s responsibility to ensure patterns are declared in the correct order.

Blocks and Procs and Lambdas, oh my!

When using this gem it is critical to remember that defn takes a block and that blocks in Ruby have special rules. There are plenty of good tutorials on the web explaining blocks and Procs and lambdas in Ruby. Please read them. Please don’t submit a bug report if you use a return statement within your defn and your code blows up with a LocalJumpError.

Examples

For more examples see the integration tests in spec/integration_spec.rb.

Simple Functions

This example is based on Syntax in defnctions: Pattern Matching in Learn You Some Erlang for Great Good!.

Erlang:

greet(male, Name) ->
  io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
  io:format("Hello, Mrs. ~s!", [Name]);
greet(_, Name) ->
  io:format("Hello, ~s!", [Name]).

Ruby:

require 'pattern_matching'

class Foo
  include PatternMatching

  defn(:greet, _) do |name|
    "Hello, #{name}!"
  end

  defn(:greet, :male, _) { |name|
    "Hello, Mr. #{name}!"
  }
  defn(:greet, :female, _) { |name|
    "Hello, Ms. #{name}!"
  }
  defn(:greet, _, _) { |_, name|
    "Hello, #{name}!"
  }
end

Simple Functions with Overloading

This example is based on Syntax in defnctions: Pattern Matching in Learn You Some Erlang for Great Good!.

Erlang:

greet(Name) ->
  io:format("Hello, ~s!", [Name]).

greet(male, Name) ->
  io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
  io:format("Hello, Mrs. ~s!", [Name]);
greet(_, Name) ->
  io:format("Hello, ~s!", [Name]).

Ruby:

require 'pattern_matching'

class Foo
  include PatternMatching

  defn(:greet, _) do |name|
    "Hello, #{name}!"
  end

  defn(:greet, :male, _) { |name|
    "Hello, Mr. #{name}!"
  }
  defn(:greet, :female, _) { |name|
    "Hello, Ms. #{name}!"
  }
  defn(:greet, nil, _) { |name|
    "Goodbye, #{name}!"
  }
  defn(:greet, _, _) { |_, name|
    "Hello, #{name}!"
  }
end

Constructor Overloading

require 'pattern_matching'

class Foo
  include PatternMatching

  defn(:initialize) { @name = 'baz' }
  defn(:initialize, _) {|name| @name = name.to_s }
end

Matching by Class/Datatype

require 'pattern_matching'

class Foo
  include PatternMatching

  defn(:concat, Integer, Integer) { |first, second|
    first + second
  }
  defn(:concat, Integer, String) { |first, second|
    "#{first} #{second}"
  }
  defn(:concat, String, String) { |first, second|
    first + second
  }
  defn(:concat, Integer, _) { |first, second|
    first + second.to_i
  }
end

Matching a Hash Parameter

require 'pattern_matching'

class Foo
  include PatternMatching
  
  defn(:hashable, {foo: :bar}) { |opts|
    # matches any hash with key :foo and value :bar
    :foo_bar
  }
  defn(:hashable, {foo: _, bar: _}) { |f, b|
    # matches any hash with keys :foo and :bar
    # passes the values associated with those keys to the block
    [f, b]
  }
  defn(:hashable, {foo: _}) { |f|
    # matches any hash with key :foo
    # passes the value associated with that key to the block
    # must appear AFTER the prior match or it will override that one
    f
  }
  defn(:hashable, {}) { ||
    # matches an empty hash
    :empty
  }
  defn(:hashable, _) { |opts|
    # matches any hash (or any other value)
    opts
  }
end

...

foo.hashable({foo: :bar})      #=> :foo_bar
foo.hashable({foo: :baz})      #=> :baz
foo.hashable({foo: 1, bar: 2}) #=> [1, 2]
foo.hashable({foo: 1, baz: 2}) #=> 1
foo.hashable({bar: :baz})      #=> {bar: :baz}
foo.hashable({})               #=> :empty

Variable Length Argument Lists with ALL

defn(:all, :one, ALL) { |args|
  args
}
defn(:all, :one, Integer, ALL) { |int, args|
  [int, args]
}
defn(:all, 1, _, ALL) { |var, args|
  [var, args]
}
defn(:all, ALL) { | args|
  args
}

...

foo.all(:one, 'a', 'bee', :see) #=> ['a', 'bee', :see]
foo.all(:one, 1, 'bee', :see)   #=> [1, 'bee', :see]
foo.all(1, 'a', 'bee', :see)    #=> ['a', ['bee', :see]]
foo.all('a', 'bee', :see)       #=> ['a', 'bee', :see]
foo.all()                       #=> NoMethodError: no method `all` matching [] found for class Foo

Guard Clauses

These examples are based on Syntax in defnctions: Pattern Matching in Learn You Some Erlang for Great Good!.

Erlang:

old_enough(X) when X >= 16 -> true;
old_enough(_) -> false.

right_age(X) when X >= 16, X =< 104 ->
  true;
right_age(_) ->
  false.

wrong_age(X) when X < 16; X > 104 ->
  true;
wrong_age(_) ->
  false.
defn(:old_enough, _){ true }.when{|x| x >= 16 }
defn(:old_enough, _){ false }

defn(:right_age, _) {
  true
}.when{|x| x >= 16 && x <= 104 }

defn(:right_age, _) {
  false
}

defn(:wrong_age, _) {
  false
}.when{|x| x < 16 || x > 104 }

defn(:wrong_age, _) {
  true
}

Behavior

The behavior functionality is not imported by default. It requires a separate require statement:

require 'behavior'

# -or-

require 'behaviour'

Next, declare a behavior using the behavior_info function (this function should sit outside of any module/class definition, but will probably work regardless). The first parameter to behavior_info (or behaviour_info) is a symbol name for the behavior. The remaining parameter is a hash of function names and their arity:

behaviour_info(:gen_foo, foo: 0, bar: 1, baz: 2)

# -or (for the Java/C# crowd)

interface(:gen_foo, foo: 0, bar: 1, baz: 2)

Each function name can be listed only once and the arity must follow the rules of the Method#arity function. Though not explicitly documented, block arguments do not count toward a method’s arity. methods defined using this gem’s defn function will always have an arity of -1, regardless of how many overloads are defined.

To enforce a behavior on a class simply call the behavior function within the class, passing the name of the desired behavior:

class Foo
  behavior(:gen_foo)
  ...
end

# or use the idiomatic Erlang spelling
class Bar
  behaviour(:gen_foo)
  ...
end

# or use the idiomatic Rails syntax
class Baz
  behaves_as :gen_foo
  ...
end

Make sure you the implement the required methods in your class. If you don’t, Ruby will raise an exception when you try to create an object from the class:

Baz.new #=> ArgumentError: undefined callback functions in Baz (behavior 'gen_foo')

As an added bonus, Ruby Object will be monkey-patched with a behaves_as? predicate method.

A complete example:

behaviour_info(:gen_foo, foo: 0, bar: 1, baz: 2, boom: -1, bam: :any)

class Foo
  behavior(:gen_foo)

  def foo
    return 'foo/0'
  end

  def bar(one, &block)
    return 'bar/1'
  end

  def baz(one, two)
    return 'baz/2'
  end

  def boom(*args)
    return 'boom/-1'
  end

  def bam
    return 'bam!'
  end
end

foo = Foo.new

foo.behaves_as? :gen_foo    #=> true
foo.behaves_as?(:bogus)     #=> false
'foo'.behaves_as? :gen_foo  #=> false

Functions

Convenience functions are not imported by default. It require a separate require statement:

require 'pattern_matching/functions'
Infinity #=> Infinity
NaN #=> NaN

repl? #=> true when called under irb, pry, bundle console, or rails console

safe(1, 2){|a, b| a + b} #=> 3
safe{ eval 'puts "Hello World!"' } #=> SecurityError: Insecure operation

pp_s [1,2,3,4] #=> "[1, 2, 3, 4]\n" props to Rha7

delta(-1, 1) #=> 2
delta({count: -1}, {count: 1}){|item| item[:count]} #=> 2

This gives you access to a few constants and functions:

PatternMatching is Copyright © 2013 Jerry D’Antonio. It is free software and may be redistributed under the terms specified in the LICENSE file.

License

Released under the MIT license.

www.opensource.org/licenses/mit-license.php

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.