Patme
This gem is my experiment on elixir-style pattern matching in ruby. In it's current state it is no more than a proof of concept and I suggest not to use it in production.
Info
Patme module stores your instance methods internally and removes them from your class. Then, when method is called, it tries to pattern-match given arguments with existing method implementations. If implementation was found, it executes it, otherwise you'll get a NoMethodError.
Currently this gem supports 3 types of arguments: specific, arbitrary and optional. In method definition def foo(agr1, arg2=1, _arg3=false)
arg1 is an arbitrary argument, arg2 is specific and arg3 is optional.
Patme supports class-based pattern matching. If method's specific argument is a Class
, then it will be matched based on given argument's class using is_a?
Usage
Recursion example
require 'patme'
class Factorial
include Patme::PatternMatching
def calculate(n=0)
1
end
def calculate(n)
n * calculate(n-1)
end
end
factorial_calculator = Factorial.new
factorial_calculator.calculate(0) # => 1
factorial_calculator.calculate(5) # => 120
factorial_calculator.calculate(-1) # => endless recursion, don't do so ^_^
Class-based pattern matching
require 'patme'
SeriousError = Class.new(StandardError)
RegularError = Class.new(StandardError)
UnexpectedError = Class.new(StandardError)
class ErrorHandler
include Patme::PatternMatching
def initialize(error)
@error = error
end
def handle
do_handle(@error)
end
# Will match if error.is_a?(SeriousError)
def do_handle(error=SeriousError)
puts "Alarm! SeriousError! Details: #{error.message}"
end
# Will match if error.is_a?(RegularError)
def do_handle(error=RegularError)
puts "Don't worry, just a RegularError: #{error.message}"
end
# Will match for all other StandardErrors
def do_handle(error=StandardError)
puts "Unexpected error: #{error.class}. Details: #{error.message}"
end
end
begin
raise SeriousError, 'boom!'
rescue => error
ErrorHandler.new(error).handle
end
# => Alarm! SeriousError! Details: boom!
begin
raise RegularError, 'we need to fix this'
rescue => error
ErrorHandler.new(error).handle
end
# => Don't worry, just a RegularError: we need to fix this
begin
raise UnexpectedError, 'something went completely wrong'
rescue => error
ErrorHandler.new(error).handle
end
# => Unexpected error: UnexpectedError. Details: something went completely wrong
Full-featured example
require 'patme'
class MyClass
include Patme::PatternMatching
# This method will match only when arg1 ='test'
def foo(arg1='test')
"ran foo('test') with #{arg1}"
end
# Will match only when arg1 ='other'
def foo(arg1='other')
"ran foo('other') with #{arg1}"
end
# You can also use other basic types
def foo(arg1={a: 1, b: 2})
"ran foo({a: 1, b: 2}) with #{arg1}"
end
# Will match when arg2 = 'test' no matter what arg1 is
def foo(arg1, arg2='test')
"ran foo(any, 'test') with [#{arg1}, #{arg2}]"
end
# Will match with any arg1 and both with and without arg2
# if arg2 is not supplied, method will receive arg2 = 'default'
def foo(arg1, _arg2="default")
"ran foo(any, optional) with [#{arg1}, #{_arg2}]"
end
# Will match with any one argument.
def bar(arg1)
"ran bar(any) with #{arg1}"
end
# Won't ever match because previous definition of bar will be pattern-matched
# before this one
def bar(arg1='never')
"ran bar('never') with #{arg1}"
end
def baz(arg1='test')
"ran baz('test') with #{arg1}"
end
end
my_obj = MyClass.new
my_obj.foo('test') # => "ran foo('test') with test"
my_obj.foo('other') # => "ran foo('other') with other"
my_obj.foo({a: 1, b: 2}) # => "ran foo({a: 1, b: 2}) with {:a => 1, :b => 2}"
my_obj.foo(1, 'test') # => "ran foo(any, 'test') with [1, test]"
my_obj.foo(1) # => "ran foo(any, optional) with [1, default]"
my_obj.foo(1, 'some') # => "ran foo(any, optional) with [1, some]"
my_obj.bar(1) # => "ran bar(any) with 1"
my_obj.bar('never') # => "ran bar(any) with never"
my_obj.baz('test') # => "ran baz('test') with test"
my_obj.baz(1) # => NoMethodError
Limitations
Pattern-matching in it's current implementation is slow and the more implementations of a method you have the slower it would be:
require 'spec_helper'
require 'benchmark'
class BenchmarkDummy
def no_pm(arg1, arg2)
['no_pm', arg1, arg2]
end
include Patme::PatternMatching
def pm(arg1='test', arg2='lol')
['specific', arg1, arg2]
end
def pm(arg1, arg2)
['arbitrary', arg1, arg2]
end
end
n = 1_000_000
foo = BenchmarkDummy.new
Benchmark.bm do |x|
x.report('no_pm '){ n.times{foo.no_pm('test', 'lol')} }
x.report('pm_specific '){ n.times{foo.pm('test', 'lol')} }
x.report('pm_arbitrary'){ n.times{foo.pm('some', 'arg')} }
end
# user system total real
# no_pm 0.200000 0.000000 0.200000 ( 0.205747)
# pm_specific 3.570000 0.000000 3.570000 ( 3.574598)
# pm_arbitrary 5.210000 0.000000 5.210000 ( 5.210410)
Only pattern matching on instance methods is supported.
Including Patme::PatternMatching into your class makes all of the methods below the include
statement be pattern-matched (I think I'll fix this issue later).
Todos
-
Add support for class methods
-
Add something to tell Patme not to pattern-match on every method
# Possible example class Hello include Patme::PatternMatching # Comment after method header will tell Patme not to touch this method def tell(name) # ::pm_off puts "Hello, #{name}" end # Or maybe it'll be better to use an annotation pm_start def yell(name="Boss", message) raise CommonSenceError, "don't yell on your boss" end def yell(name, message) puts message.upcase end pm_stop end
-
Add guards
# Possible example class Factorial include Patme::PatternMatch def calculate(n=0) 1 end def calculate(n) # ::when( n < 0 ) raise ArgumentError, "Can't calculate factorial of a negative number: #{n}" end def calculate(n) # ::when( n > 0 ) n * self.of(n-1) end end
-
Add support for keyword arguments (
key:
- arbitrary,key: value
- specific,_key: value
- optional) -
Your suggestions?
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/vizvamitra/patme.
License
The gem is available as open source under the terms of the MIT License.