Skip to content

Erlang like pattern matching function definitions for ruby

License

Notifications You must be signed in to change notification settings

mwarnock/defmatch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Defmatch

CircleCI

Switching between erlang and ruby a fair amount has me missing erlang's function definition features. Particularly dispatching based on pattern matching. In erlang it's common to write a function that handles both a single item or a list of those items like this:

times_two(List) when is_list(List) ->
  [times_two(I) || I <- List]; %% This is a basic list comprehension (think collect if you're a ruby person)
times_two(I) when is_number(I) ->
  I * 2.

times_two(4). % Returns 8
times_two([1,2,3,4]). % Returns [2,4,6,8]
times_two(<<"asdf">>). % Throws a bad match (function clause) error

To do the same type of operation in ruby I'd have to write something like this:

def times_two(number_or_list)
  if number_or_list.class == Fixnum
    number_or_list * 2
  elsif number_or_list.class == Array
    number_or_list.collect(&:times_two)
  else
    throw "Not a valid argument type"
  end
end

Functionally these two are identical, but from a readability stand point I'd take erlang's version every time; even more so when this type of dispatching gets really complicated.

So how would you write the same thing using Defmatch?

class TestMe
  extend(Defmatch)

  defmatch(:times,Fixnum) {|num| num * 2 }
  defmatch(:times,Array) {|list| list.collect {|i| times(i) } }
end

x = TestMe.new
x.times(4) # -> 8
x.times([1,2,3,4]) # -> [2,4,6,8]

How does it work and how do I use it?

Defmatch is written as a module and when it's used to extend a class it creates a defmatch class method. The defmatch method takes one required argument as the name of the method you're defining. The remaining arguments are the pattern to match on when calling that method. Those arguments can be classes, literals, or procedures (lambdas). It also requires a block which is the actual function body that will run when the pattern matches. Those with a java background will find this similar to method overloading, but more powerful. Those with an erlang background will feel right at home. Here are some concrete examples.

class TestMe
  extend(Defmatch)

  # Run this function if the argument passed to magic is of type Fixnum
  defmatch(:magic,Fixnum) {|number| "I got a number #{number}" }
  # Run this function if the argument passed to magic is of type Array
  defmatch(:magic,Array) {|a| "I got an Array #{a.inspect}" }
  # Run this function if the argument passed to magic is the symbol :literally
  defmatch(:magic,:literally) {|duh| "This literally matched #{duh}" }
  # Run this function when there are two fixnums passed to magic
  defmatch(:magic,Fixnum,Fixnum) {|a,b| "Found two numbers #{a}:#{b}" }
  # Run this function when there is a single argument that is equal to "banana" (not a great example as this could be done with a literal)
  defmatch(:magic,lambda {|arg| arg == "banana" }) {|arg| "I matched using a procedure that made sure \"banana\" == #{arg}" }
  # Run this function with no arguments
  defmatch(:magic) { "nifty" }
end

#Now you have an instance method called magic that dispatches what runs based on the patterns you defined and their associated block
x = TestMe.new
x.magic(10) # -> Matches the first
x.magic([1,2,3]) # -> Matches the second
x.magic(:literally) # -> You get the idea
x.magic(2,3)
x.magic("banana")
x.magic()
x.magic("I","never","defined","this","to","match") # -> ArgumentError: No function clause matching arguments

This can come in very handy, but remember that the order in which you define things matters. Lets say I define my magic function like this:

  #...
  defmatch(:magic,Fixnum) {|num| num * 2 }
  defmatch(:magic,1) {|num| "I got me a 1" }
  #...

Even if I run x.magic(1) I will get 2 as the result. The second defmatch will never be matched because there is a more general match case above it. Order matters. Define your most specific matches first.

If you want to create class methods (yes there are no true class methods in ruby, but it's a convient definition) you can use the defclassmatch method. It works just like defmatch but makes a class method instead.

A note about inheritance

If you've defined a class you intend to inherit from and it uses Defmatch then be warned. Presently Defmatch defines an inherited method for your class when you extend it. So if you use this method yourself then you'll be overwriting Defmatch's and you'll get odd errors when trying to use the defmatch/defclassmatch defined methods in your subclass. The work around for this is to reference the Defmatch version of the inherited function in your own inherited function.

class MySuperClass

  def self.inherited(subklass)
    Defmatch.inherited(self,subklass)
  end

end

It's not a great solution, but it will work until I come up with a better one.

Roadmap

  • Add parameter deconstruction
  • Cleaner way to handle inheritance
  • Add it to the Kernel so it's available without having to include things. This will require ruby 2.0 and I'm not prepared to kill backwards compatability yet.

About

Erlang like pattern matching function definitions for ruby

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages