RuboClaus

RuboClaus is an open source project that gives the Ruby developer a DSL to implement functions with multiple clauses and varying numbers of arguments on a pattern matching paradigm, inspired by functional programming in Elixir and Erlang.

gem install rubo_claus

Note

RuboClaus is still in very early stage of development and thought process. We are still treating this as a proof-of-concept and are still exploring the topic. As such, we don't suggest you use this in any kind of production environment, without first looking at the library code and feeling comfortable with how it works. And, if you would like to continue this thought experiment and provide feedback/suggestions/changes, we would love to hear it.

Rationale

The beauty of multiple function clauses with pattern matching is fewer conditionals and fewer lines of unnecessary defensive logic. Focus on the happy path. Control types as they come in, and handle for edge cases with catch all clauses. It does not work great for simple methods, like this:

Ruby:

def add(first, second)
  return "Please use numbers" unless [first, second].all? { |obj| obj.is_a? Fixnum }
  first + second
end

Ruby With RuboClaus:

define_function :add do
  clauses(
    clause([Fixnum, Fixnum], proc { |first, second| first + second }),
    catch_all(proc { "Please use numbers" })
  )
end

It is cumbersome for problems like add--in which case we don't recommend using it. But as soon as we add complexity that depends on parameter arity or type, we can see how RuboClaus makes our code more extendible and maintainable. For example:

Ruby:

def handle_response(status, has_body, is_chunked)
  if status == 200 && has_body && is_chunked
    # ...
  else
    if status == 200 && has_body && !is_chunked
      # ...
    else
      if status == 200 && !has_body
        # ...
      else
        # ...
      end
    end
  end
end

Ruby with RuboClaus:

define_function :handle_response do
  clauses(
    clause([200, true, true], proc { |status, has_body, is_chunked| ... }),
    clause([200, true, false], proc { |status, has_body, is_chunked| ... }),
    clause([200, false], proc { |status, has_body| ... }),
    catch_all(proc { return_error })
  )
end

Credit

To learn more about this style of programming read about function overloading and pattern matching.

Usage

Below are the public API methods and their associated arguments.

Clause pattern arguments

The first argument to the clause method is an array of pattern match options. This array can vary in length, and values depending on your pattern match case.

You can match against specific values:

clause(["foo"], proc {...})
clause([42], proc {...})
clause(["Hello", :darth_vader], proc {...})

You can match against specifc argument types:

clause([String], proc {...})
clause([Fixnum], proc {...})
clause([String, Symbol], proc {...})

You can match against specific values and types:

clause(["Hello", String], proc {...})
clause([42, Fixnum], proc {...})
clause([String, :darth_vader], proc {...})

You also can match against any value or type if you don't have a specific requirement for an argument by using the :any symbol.

clause(["Hello", :any], proc {...})
clause([:any], proc {...})
clause([42, :any], proc {...})

You also can destructure an array with :tail.

clause(["Hello", [Fixnum, :tail]], proc { |string, number, tail_array| ...  })
clause([Hash, [Fixnum, Fixnum :tail]], proc { |hash, number1, number2,  tail_array| ... })

Examples

Please see the examples directory for various example use cases. Most examples include direct comparisons of the Ruby code to a similar implementation in Elixir.

Development

Don't introduce unneeded external dependencies.

Nothing else special to note for development. Just add tests associated to any code changes and make sure they pass.

TODO


Build Status contributions welcome