jdantonio/functional-ruby

View on GitHub
lib/functional/either.rb

Summary

Maintainability
A
25 mins
Test Coverage
require 'functional/abstract_struct'
require 'functional/protocol'
require 'functional/synchronization'

Functional::SpecifyProtocol(:Either) do
  instance_method :left, 0
  instance_method :left?, 0
  instance_method :right, 0
  instance_method :right?, 0
end

module Functional

  # The `Either` type represents a value of one of two possible types (a
  # disjoint union). It is an immutable structure that contains one and only one
  # value. That value can be stored in one of two virtual position, `left` or
  # `right`. The position provides context for the encapsulated data.
  #
  # One of the main uses of `Either` is as a return value that can indicate
  # either success or failure. Object oriented programs generally report errors
  # through either state or exception handling, neither of which work well in
  # functional programming. In the former case, a method is called on an object
  # and when an error occurs the state of the object is updated to reflect the
  # error. This does not translate well to functional programming because they
  # eschew state and mutable objects. In the latter, an exception handling block
  # provides branching logic when an exception is thrown. This does not
  # translate well to functional programming because it eschews side effects
  # like structured exception handling (and structured exception handling tends
  # to be very expensive). `Either` provides a powerful and easy-to-use
  # alternative.
  #
  # A function that may generate an error can choose to return an immutable
  # `Either` object in which the position of the value (left or right) indicates
  # the nature of the data. By convention, a `left` value indicates an error and
  # a `right` value indicates success. This leaves the caller with no ambiguity
  # regarding success or failure, requires no persistent state, and does not
  # require expensive exception handling facilities.
  #
  # `Either` provides several aliases and convenience functions to facilitate
  # these failure/success conventions. The `left` and `right` functions,
  # including their derivatives, are mirrored by `reason` and `value`. Failure
  # is indicated by the presence of a `reason` and success is indicated by the
  # presence of a `value`. When an operation has failed the either is in a
  # `rejected` state, and when an operation has successed the either is in a
  # `fulfilled` state. A common convention is to use a Ruby `Exception` as the
  # `reason`. The factory method `error` facilitates this. The semantics and
  # conventions of `reason`, `value`, and their derivatives follow the
  # conventions of the Concurrent Ruby gem.
  #
  # The `left`/`right` and `reason`/`value` methods are not mutually exclusive.
  # They can be commingled and still result in functionally correct code. This
  # practice should be avoided, however. Consistent use of either `left`/`right`
  # or `reason`/`value` against each `Either` instance will result in more
  # expressive, intent-revealing code.
  #
  # @example
  #
  #   require 'uri'
  #
  #   def web_host(url)
  #     uri = URI(url)
  #     if uri.scheme != 'http'
  #       Functional::Either.left('Invalid HTTP URL')
  #     else
  #       Functional::Either.right(uri.host)
  #     end
  #   end
  #
  #   good = web_host('http://www.concurrent-ruby.com')
  #   good.right? #=> true
  #   good.right  #=> "www.concurrent-ruby"
  #   good.left #=> nil
  #
  #   good = web_host('bogus')
  #   good.right? #=> false
  #   good.right  #=> nil
  #   good.left #=> "Invalid HTTP URL"
  #
  # @see http://functionaljava.googlecode.com/svn/artifacts/3.0/javadoc/fj/data/Either.html Functional Java
  # @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Either.html Haskell Data.Either
  # @see http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Obligation.html Concurrent Ruby
  #
  # @!macro thread_safe_immutable_object
  class Either < Synchronization::Object
    include AbstractStruct

    self.datatype = :either
    self.fields = [:left, :right].freeze

    # @!visibility private
    NO_VALUE = Object.new.freeze

    private_class_method :new

    class << self

      # Construct a left value of either.
      #
      # @param [Object] value The value underlying the either.
      # @return [Either] A new either with the given left value.
      def left(value)
        new(value, true).freeze
      end
      alias_method :reason, :left

      # Construct a right value of either.
      #
      # @param [Object] value The value underlying the either.
      # @return [Either] A new either with the given right value.
      def right(value)
        new(value, false).freeze
      end
      alias_method :value, :right

      # Create an `Either` with the left value set to an `Exception` object
      # complete with message and backtrace. This is a convenience method for
      # supporting the reason/value convention with the reason always being
      # an `Exception` object. When no exception class is given `StandardError`
      # will be used. When no message is given the default message for the
      # given error class will be used.
      #
      # @example
      #
      #   either = Functional::Either.error("You're a bad monkey, Mojo Jojo")
      #   either.fulfilled? #=> false
      #   either.rejected?  #=> true
      #   either.value      #=> nil
      #   either.reason     #=> #<StandardError: You're a bad monkey, Mojo Jojo>
      #
      # @param [String] message The message for the new error object.
      # @param [Exception] clazz The class for the new error object.
      # @return [Either] A new either with an error object as the left value.
      def error(message = nil, clazz = StandardError)
        ex = clazz.new(message)
        ex.set_backtrace(caller)
        left(ex)
      end
    end

    # Projects this either as a left.
    #
    # @return [Object] The left value or `nil` when `right`.
    def left
      left? ? to_h[:left] : nil
    end
    alias_method :reason, :left

    # Projects this either as a right.
    #
    # @return [Object] The right value or `nil` when `left`.
    def right
      right? ? to_h[:right] : nil
    end
    alias_method :value, :right

    # Returns true if this either is a left, false otherwise.
    #
    # @return [Boolean] `true` if this either is a left, `false` otherwise.
    def left?
      @is_left
    end
    alias_method :reason?, :left?
    alias_method :rejected?, :left?

    # Returns true if this either is a right, false otherwise.
    #
    # @return [Boolean] `true` if this either is a right, `false` otherwise.
    def right?
      ! left?
    end
    alias_method :value?, :right?
    alias_method :fulfilled?, :right?

    # If this is a left, then return the left value in right, or vice versa.
    #
    # @return [Either] The value of this either swapped to the opposing side.
    def swap
      if left?
        self.class.send(:new, left, false)
      else
        self.class.send(:new, right, true)
      end
    end

    # The catamorphism for either. Folds over this either breaking into left or right.
    #
    # @param [Proc] lproc The function to call if this is left.
    # @param [Proc] rproc The function to call if this is right.
    # @return [Object] The reduced value.
    def either(lproc, rproc)
      left? ? lproc.call(left) : rproc.call(right)
    end

    # If the condition satisfies, return the given A in left, otherwise, return the given B in right.
    #
    # @param [Object] lvalue The left value to use if the condition satisfies.
    # @param [Object] rvalue The right value to use if the condition does not satisfy.
    # @param [Boolean] condition The condition to test (when no block given).
    # @yield The condition to test (when no condition given).
    #
    # @return [Either] A constructed either based on the given condition.
    #
    # @raise [ArgumentError] When both a condition and a block are given.
    def self.iff(lvalue, rvalue, condition = NO_VALUE)
      raise ArgumentError.new('requires either a condition or a block, not both') if condition != NO_VALUE && block_given?
      condition = block_given? ? yield : !! condition
      condition ? left(lvalue) : right(rvalue)
    end

    private

    # Create a new Either wil the given value and disposition.
    #
    # @param [Object] value the value of this either
    # @param [Boolean] is_left is this a left either or right?
    #
    # @!visibility private
    def initialize(value, is_left)
      super
      @is_left = is_left
      hsh = is_left ? {left: value, right: nil} : {left: nil, right: value}
      set_data_hash(hsh)
      set_values_array(hsh.values)
      ensure_ivar_visibility!
    end
  end
end