jdantonio/functional-ruby

View on GitHub
lib/functional/pattern_matching.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'functional/method_signature'

module Functional

  # 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. 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.
  #
  # {include:file:doc/pattern_matching.md}
  module PatternMatching

    # A parameter that is required but that can take any value.
    # @!visibility private
    UNBOUND = Object.new.freeze

    # A match for one or more parameters in the last position of the match.
    # @!visibility private
    ALL = Object.new.freeze

    private

    # A guard clause on a pattern match.
    # @!visibility private
    GuardClause = Class.new do
      def initialize(function, clazz, pattern)
        @function = function
        @clazz = clazz
        @pattern = pattern
      end
      def when(&block)
        unless block_given?
          raise ArgumentError.new("block missing for `when` guard on function `#{@function}` of class #{@clazz}")
        end
        @pattern.guard = block
        self
      end
    end
    private_constant :GuardClause

    # @!visibility private
    FunctionPattern = Struct.new(:function, :args, :body, :guard)
    private_constant :FunctionPattern

    # @!visibility private
    def __unbound_args__(match, args)
      argv = []
      match.args.each_with_index do |p, i|
        if p == ALL && i == match.args.length-1
          # when got ALL, then push all to the end to the list of args,
          # so we can get them as usual *args in matched method
          argv.concat args[(i..args.length)]
        elsif p.is_a?(Hash) && p.values.include?(UNBOUND)
          p.each do |key, value|
            argv << args[i][key] if value == UNBOUND
          end
        elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
          argv << args[i]
        end
      end
      argv
    end

    def __pass_guard__?(matcher, args)
      matcher.guard.nil? ||
        self.instance_exec(*__unbound_args__(matcher, args), &matcher.guard)
    end

    # @!visibility private
    def __pattern_match__(clazz, function, *args, &block)
      args = args.first
      matchers = clazz.__function_pattern_matches__.fetch(function, [])
      matchers.detect do |matcher|
        MethodSignature.match?(matcher.args, args) && __pass_guard__?(matcher, args)
      end
    end

    # @!visibility private
    def self.included(base)
      base.extend(ClassMethods)
      super(base)
    end

    # Class methods added to a class that includes {Functional::PatternMatching}
    # @!visibility private
    module ClassMethods

      # @!visibility private
      def _()
        UNBOUND
      end

      # @!visibility private
      def defn(function, *args, &block)
        unless block_given?
          raise ArgumentError.new("block missing for definition of function `#{function}` on class #{self}")
        end

        # Check that number of free variables in pattern match method's arity
        pat_arity = __pattern_arity__(args)
        unless pat_arity == block.arity
          raise ArgumentError.new("Pattern and block arity mismatch: "\
                                    "#{pat_arity}, #{block.arity}")
        end

        # add a new pattern for this function
        pattern = __register_pattern__(function, *args, &block)

        # define the delegator function if it doesn't exist yet
        unless self.instance_methods(false).include?(function)
          __define_method_with_matching__(function)
        end

        # return a guard clause to be added to the pattern
        GuardClause.new(function, self, pattern)
      end

      # @!visibility private
      # define an arity -1 function that dispatches to the appropriate
      # pattern match variant or raises an exception
      def __define_method_with_matching__(function)
        define_method(function) do |*args, &block|
          begin
            # get the collection of matched patterns for this function
            # use owner to ensure we climb the inheritance tree
            match = __pattern_match__(self.method(function).owner, function, args, block)
            if match
              # call the matched function
              argv = __unbound_args__(match, args)
              self.instance_exec(*argv, &match.body)
            elsif defined?(super)
              # delegate to the superclass
              super(*args, &block)
            else
              raise NoMethodError.new("no method `#{function}` matching "\
                "#{args} found for class #{self.class}")
            end
          end
        end
      end

      # @!visibility private
      def __function_pattern_matches__
        @__function_pattern_matches__ ||= Hash.new
      end

      # @!visibility private
      def __register_pattern__(function, *args, &block)
        block = Proc.new{} unless block_given?
        pattern = FunctionPattern.new(function, args, block)
        patterns = self.__function_pattern_matches__.fetch(function, [])
        patterns << pattern
        self.__function_pattern_matches__[function] = patterns
        pattern
      end

      # @!visibility private
      def __pattern_arity__(pat)
        r = pat.reduce(0) do |acc, v|
          if v.is_a?(Hash) 
            ub = v.values.count { |e| e == UNBOUND }
            # if hash have UNBOUND then treat each unbound as separate arg
            # alse all hash is one arg
            ub > 0 ? acc + ub : acc + 1
          elsif v == ALL || v == UNBOUND || v.is_a?(Class)
            acc + 1
          else
            acc
          end
        end
        pat.last == ALL ? -r : r
      end

    end
  end
end