lib/qo/pattern_matchers/pattern_match.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Qo
  module PatternMatchers
    # Classic Pattern Match. This matcher uses `when` and `else` branches and
    # is meant to be a more powerful variant of the case statement
    #
    # @author baweaver
    # @since 0.2.0
    class PatternMatch
      include Branching

      # The regular pattern matcher from classic Qo uses `when` and `else`
      # branches, like a `case` statement
      register_branch Qo::Branches::WhenBranch.new
      register_branch Qo::Branches::ElseBranch.new

      # Creates a new instance of a pattern matcher
      #
      # @param destructure: false [Boolean]
      #   Whether or not to destructure values before yielding to a block
      #
      # @param &fn [Proc]
      #   Function to be used to construct the pattern matcher's branches
      #
      # @return [Qo::PatternMatchers::PatternMatch]
      def initialize(destructure: false, &fn)
        @matchers    = []
        @default     = nil
        @destructure = destructure

        yield(self) if block_given?
      end

      # Allows for the creation of an anonymous PatternMatcher based on this
      # parent class. To be used by people wishing to make their own pattern
      # matchers with variant branches and other features not included in the
      # defaultly provided ones
      #
      # @param branches: [] [Array[Branch]]
      #   Branches to be used with this new pattern matcher
      #
      # @return [Class]
      #   Anonymous pattern matcher class to be bound to a constant or used
      #   anonymously.
      def self.create(branches: [])
        Class.new(Qo::PatternMatchers::PatternMatch) do
          branches.each { |branch| register_branch(branch.new) }
        end
      end

      # Allows for the injection of a pattern matching function into a type class
      # for direct access, rather than yielding an instance of that class to a
      # pattern matcher.
      #
      # This is typically done for monadic types that need to `match`. When
      # combined with extractor type branches it can be very handy for dealing
      # with container types.
      #
      # @example
      #
      #   ```ruby
      #   # Technically Some and None don't exist yet, so we have to "cheat" instead
      #   # of just saying `Some` for the precondition
      #   SomeBranch = Qo.create_branch(
      #     name:        'some',
      #     precondition: -> v { v.is_a?(Some) },
      #     extractor:    :value
      #   )
      #
      #   NoneBranch = Qo.create_branch(
      #     name:        'none',
      #     precondition: -> v { v.is_a?(None) },
      #     extractor:    :value
      #   )
      #
      #   SomePatternMatch = Qo.create_pattern_match(branches: [SomeBranch, NoneBranch])
      #
      #   class Some
      #     include SomePatternMatch.mixin
      #
      #     attr_reader :value
      #
      #     def initialize(value) @value = value end
      #
      #     def fmap(&fn)
      #       new_value = fn.call(value)
      #       new_value ? Some.new(new_value) : None(value)
      #     end
      #   end
      #
      #   class None
      #     include SomePatternMatch.mixin
      #
      #     attr_reader :value
      #
      #     def initialize(value) @value = value end
      #     def fmap(&fn) None.new(value) end
      #   end
      #
      #   Some.new(1)
      #     .fmap { |v| v * 2 }
      #     .match { |m|
      #       m.some { |v| v + 100 }
      #       m.none { "OHNO!" }
      #     }
      #   => 102
      #
      #   Some.new(1)
      #     .fmap { |v| nil }
      #     .match { |m|
      #       m.some { |v| v + 100 }
      #       m.none { "OHNO!" }
      #     }
      #   => "OHNO!"
      #   ```
      #
      # @param destructure: false [Boolean]
      #   Whether or not to destructure values before yielding to a block
      #
      # @param as: :match [Symbol]
      #   Name to use as a method name bound to the including class
      #
      # @return [Module]
      #   Module to be mixed into a class
      def self.mixin(destructure: false, as: :match)
        create_self = -> &function { new(destructure: destructure, &function) }

        Module.new do
          define_method(as) do |&function|
            create_self.call(&function).call(self)
          end
        end
      end

      # Calls the pattern matcher, yielding the target value to the first
      # matching branch it encounters.
      #
      # @param value [Any]
      #   Value to match against
      #
      # @return [Any]
      #   Result of the called branch
      #
      # @return [nil]
      #   Returns nil if no branch is matched
      def call(value)
        @matchers.each do |matcher|
          status, return_value = matcher.call(value)
          return return_value if status
        end

        if @default
          _, return_value = @default.call(value)
          return_value
        else
          nil
        end
      end

      alias === call
      alias [] call

      # Procified version of `call`
      #
      # @return [Proc[Any] => Any]
      def to_proc
        -> target { self.call(target) }
      end
    end
  end
end