lib/fear/either.rb
# frozen_string_literal: true
module Fear
# Represents a value of one of two possible types (a disjoint union.)
# An instance of +Either+ is either an instance of +Left+ or +Right+.
#
# A common use of +Either+ is as an alternative to +Option+ for dealing
# with possible missing values. In this usage, +None+ is replaced
# with a +Left+ which can contain useful information.
# +Right+ takes the place of +Some+. Convention dictates
# that +Left+ is used for failure and +Right+ is used for Right.
#
# For example, you could use +Either<String, Fixnum>+ to select_or_else whether a
# received input is a +String+ or an +Fixnum+.
#
# @example
# in = Readline.readline('Type Either a string or an Int: ', true)
# result = begin
# Fear.right(Integer(in))
# rescue ArgumentError
# Fear.left(in)
# end
#
# result.match do |m|
# m.right do |x|
# "You passed me the Int: #{x}, which I will increment. #{x} + 1 = #{x+1}"
# end
#
# m.left do |x|
# "You passed me the String: #{x}"
# end
# end
#
# Either is right-biased, which means that +Right+ is assumed to be the default case to
# operate on. If it is +Left+, operations like +#map+, +#flat_map+, ... return the +Left+ value
# unchanged:
#
# @!method get_or_else(*args)
# Returns the value from this +Right+ or evaluates the given
# default argument if this is a +Left+.
# @overload get_or_else(&default)
# @yieldreturn [any]
# @return [any]
# @example
# Fear.right(42).get_or_else { 24/2 } #=> 42
# Fear.left('undefined').get_or_else { 24/2 } #=> 12
# @overload get_or_else(default)
# @return [any]
# @example
# Fear.right(42).get_or_else(12) #=> 42
# Fear.left('undefined').get_or_else(12) #=> 12
#
# @!method or_else(&alternative)
# Returns this +Right+ or the given alternative if this is a +Left+.
# @return [Either]
# @example
# Fear.right(42).or_else { Fear.right(21) } #=> Fear.right(42)
# Fear.left('unknown').or_else { Fear.right(21) } #=> Fear.right(21)
# Fear.left('unknown').or_else { Fear.left('empty') } #=> Fear.left('empty')
#
# @!method include?(other_value)
# Returns +true+ if +Right+ has an element that is equal
# (as determined by +==+) to +other_value+, +false+ otherwise.
# @param [any]
# @return [Boolean]
# @example
# Fear.right(17).include?(17) #=> true
# Fear.right(17).include?(7) #=> false
# Fear.left('undefined').include?(17) #=> false
#
# @!method each(&block)
# Performs the given block if this is a +Right+.
# @yieldparam [any] value
# @yieldreturn [void]
# @return [Fear::Either] itself
# @example
# Fear.right(17).each do |value|
# puts value
# end #=> prints 17
#
# Fear.left('undefined').each do |value|
# puts value
# end #=> does nothing
#
# @!method map(&block)
# Maps the given block to the value from this +Right+ or
# returns this if this is a +Left+.
# @yieldparam [any] value
# @yieldreturn [any]
# @example
# Fear.right(42).map { |v| v/2 } #=> Fear.right(21)
# Fear.left('undefined').map { |v| v/2 } #=> Fear.left('undefined')
#
# @!method flat_map(&block)
# Returns the given block applied to the value from this +Right+
# or returns this if this is a +Left+.
# @yieldparam [any] value
# @yieldreturn [Fear::Either]
# @return [Fear::Either]
# @example
# Fear.right(42).flat_map { |v| Fear.right(v/2) } #=> Fear.right(21)
# Fear.left('undefined').flat_map { |v| Fear.right(v/2) } #=> Fear.left('undefined')
#
# @!method to_option
# Returns an +Some+ containing the +Right+ value or a +None+ if
# this is a +Left+.
# @return [Option]
# @example
# Fear.right(42).to_option #=> Fear.some(42)
# Fear.left('undefined').to_option #=> Fear.none()
#
# @!method any?(&predicate)
# Returns +false+ if +Left+ or returns the result of the
# application of the given predicate to the +Right+ value.
# @yieldparam [any] value
# @yieldreturn [Boolean]
# @return [Boolean]
# @example
# Fear.right(12).any? { |v| v > 10 } #=> true
# Fear.right(7).any? { |v| v > 10 } #=> false
# Fear.left('undefined').any? { |v| v > 10 } #=> false
#
# -----
#
# @!method right?
# Returns +true+ if this is a +Right+, +false+ otherwise.
# @note this method is also aliased as +#success?+
# @return [Boolean]
# @example
# Fear.right(42).right? #=> true
# Fear.left('err').right? #=> false
#
# @!method left?
# Returns +true+ if this is a +Left+, +false+ otherwise.
# @note this method is also aliased as +#failure?+
# @return [Boolean]
# @example
# Fear.right(42).left? #=> false
# Fear.left('err').left? #=> true
#
# @!method select_or_else(default, &predicate)
# Returns +Left+ of the default if the given predicate
# does not hold for the right value, otherwise, returns +Right+.
# @param default [Object, Proc]
# @yieldparam value [Object]
# @yieldreturn [Boolean]
# @return [Either]
# @example
# Fear.right(12).select_or_else(-1, &:even?) #=> Fear.right(12)
# Fear.right(7).select_or_else(-1, &:even?) #=> Fear.left(-1)
# Fear.left(12).select_or_else(-1, &:even?) #=> Fear.left(12)
# Fear.left(12).select_or_else(-> { -1 }, &:even?) #=> Fear.left(12)
#
# @!method select(&predicate)
# Returns +Left+ of value if the given predicate
# does not hold for the right value, otherwise, returns +Right+.
# @yieldparam value [Object]
# @yieldreturn [Boolean]
# @return [Either]
# @example
# Fear.right(12).select(&:even?) #=> Fear.right(12)
# Fear.right(7).select(&:even?) #=> Fear.left(7)
# Fear.left(12).select(&:even?) #=> Fear.left(12)
# Fear.left(7).select(&:even?) #=> Fear.left(7)
#
# @!method reject(&predicate)
# Returns +Left+ of value if the given predicate holds for the
# right value, otherwise, returns +Right+.
# @yieldparam value [Object]
# @yieldreturn [Boolean]
# @return [Either]
# @example
# Fear.right(12).reject(&:even?) #=> Fear.left(12)
# Fear.right(7).reject(&:even?) #=> Fear.right(7)
# Fear.left(12).reject(&:even?) #=> Fear.left(12)
# Fear.left(7).reject(&:even?) #=> Fear.left(7)
#
# @!method swap
# If this is a +Left+, then return the left value in +Right+ or vice versa.
# @return [Fear::Either]
# @example
# Fear.left('left').swap #=> Fear.right('left')
# Fear.right('right').swap #=> Fear.left('left')
#
# @!method reduce(reduce_left, reduce_right)
# Applies +reduce_left+ if this is a +Left+ or +reduce_right+ if
# this is a +Right+.
# @param reduce_left [Proc] the Proc to apply if this is a +Left+
# @param reduce_right [Proc] the Proc to apply if this is a +Right+
# @return [any] the results of applying the Proc
# @example
# result = possibly_failing_operation()
# log(
# result.reduce(
# ->(ex) { "Operation failed with #{ex}" },
# ->(v) { "Operation produced value: #{v}" },
# )
# )
#
#
# @!method join_right
# Joins an +Either+ through +Right+. This method requires
# that the right side of this +Either+ is itself an
# +Either+ type.
# @note This method, and +join_left+, are analogous to +Option#flatten+
# @return [Either]
# @raise [TypeError] if it does not contain +Either+.
# @example
# Fear.right(Fear.right(12)).join_right #=> Fear.right(12)
# Fear.right(Fear.left("flower")).join_right #=> Fear.left("flower")
# Fear.left("flower").join_right #=> Fear.left("flower")
# Fear.left(Fear.right("flower")).join_right #=> Fear.left(Fear.right("flower"))
#
# @!method join_right
# Joins an +Either+ through +Left+. This method requires
# that the left side of this +Either+ is itself an
# +Either+ type.
# @note This method, and +join_right+, are analogous to +Option#flatten+
# @return [Either]
# @raise [TypeError] if it does not contain +Either+.
# @example
# Fear.left(Fear.right("flower")).join_left #=> Fear.right("flower")
# Fear.left(Fear.left(12)).join_left #=> Fear.left(12)
# Fear.right("daisy").join_left #=> Fear.right("daisy")
# Fear.right(Fear.left("daisy")).join_left #=> Fear.right(Fear.left("daisy"))
#
# @!method match(&matcher)
# Pattern match against this +Either+
# @yield matcher [Fear::Either::PatternMatch]
# @example
# either.match do |m|
# m.right(Integer) do |x|
# x * 2
# end
#
# m.right(String) do |x|
# x.to_i * 2
# end
#
# m.left { |x| x }
# m.else { 'something unexpected' }
# end
#
# @see https://github.com/scala/scala/blob/2.12.x/src/library/scala/util/Either.scala
#
module Either
# @private
def left_class
Left
end
# @private
def right_class
Right
end
def initialize(value)
@value = value
end
attr_reader :value
protected :value
# @param other [Any]
# @return [Boolean]
def ==(other)
other.is_a?(self.class) && value == other.value
end
# @return [String]
def inspect
"#<#{self.class} value=#{value.inspect}>"
end
# @return [String]
alias to_s inspect
# @return [<any>]
def deconstruct
[value]
end
# Projects this +Fear::Either+ as a +Fear::Left+.
# This allows performing right-biased operation of the left
# side of the +Fear::Either+.
#
# @example
# Fear.left(42).left.map(&:succ) #=> Fear.left(43)
# Fear.right(42).left.map(&:succ) #=> Fear.left(42)
#
# @return [Fear::LeftProjection]
def left
LeftProjection.new(self)
end
class << self
# Build pattern matcher to be used later, despite off
# +Either#match+ method, id doesn't apply matcher immanently,
# but build it instead. Unusually in sake of efficiency it's better
# to statically build matcher and reuse it later.
#
# @example
# matcher =
# Either.matcher do |m|
# m.right(Integer, ->(x) { x > 2 }) { |x| x * 2 }
# m.right(String) { |x| x.to_i * 2 }
# m.left(String) { :err }
# m.else { 'error '}
# end
# matcher.call(Fear.right(42))
#
# @yieldparam [Fear::Either::PatternMatch]
# @return [Fear::PartialFunction]
def matcher(&matcher)
Either::PatternMatch.new(&matcher)
end
end
# Include this mixin to access convenient factory methods.
# @example
# include Fear::Either::Mixin
#
# Right('flower') #=> #<Fear::Right value='flower'>
# Left('beaf') #=> #<Fear::Legt value='beaf'>
#
module Mixin
# @param value [any]
# @return [Fear::Left]
# @example
# Left(42) #=> #<Fear::Left value=42>
#
def Left(value)
Fear.left(value)
end
# @param value [any]
# @return [Fear::Right]
# @example
# Right(42) #=> #<Fear::Right value=42>
#
def Right(value)
Fear.right(value)
end
end
end
end