lib/rumonade/either.rb
require 'rumonade/monad'
module Rumonade
# Represents a value of one of two possible types (a disjoint union).
# The data constructors {Rumonade::Left} and {Rumonade::Right} represent the two possible values.
# The +Either+ type is often used as an alternative to {Rumonade::Option} where {Rumonade::Left} represents
# failure (by convention) and {Rumonade::Right} is akin to {Rumonade::Some}.
#
# This implementation of +Either+ also contains ideas from the +Validation+ class in the
# +scalaz+ library.
#
# @abstract
class Either
def initialize
raise(TypeError, "class Either is abstract; cannot be instantiated") if self.class == Either
end
private :initialize
# @return [Boolean] Returns +true+ if this is a {Rumonade::Left}, +false+ otherwise.
def left?
is_a?(Left)
end
# @return [Boolean] Returns +true+ if this is a {Rumonade::Right}, +false+ otherwise.
def right?
is_a?(Right)
end
# @return [Boolean] If this is a Left, then return the left value in Right or vice versa.
def swap
if left? then Right(left_value) else Left(right_value) end
end
# @param [Proc] function_of_left_value the function to apply if this is a Left
# @param [Proc] function_of_right_value the function to apply if this is a Right
# @return Returns the results of applying the function
def fold(function_of_left_value, function_of_right_value)
if left? then function_of_left_value.call(left_value) else function_of_right_value.call(right_value) end
end
# @return [LeftProjection] Projects this Either as a Left.
def left
LeftProjection.new(self)
end
# @return [RightProjection] Projects this Either as a Right.
def right
RightProjection.new(self)
end
# Default concatenation function used by {#+}
DEFAULT_CONCAT = lambda { |a,b| a + b }
# @param [Either] other the other +Either+ to concatenate
# @param [Hash] opts the options to concatenate with
# @option opts [Proc] :concat_left (DEFAULT_CONCAT) The function to concatenate +Left+ values
# @option opts [Proc] :concat_right (DEFAULT_CONCAT) the function to concatenate +Right+ values
# @yield [right_value] optional block to transform concatenated +Right+ values
# @yieldparam [Object] right_values the concatenated +Right+ values yielded to optional block
# @return [Either] if both are +Right+, returns +Right+ with +right_value+'s concatenated,
# otherwise a +Left+ with +left_value+'s concatenated
def +(other, opts = {})
opts = { :concat_left => DEFAULT_CONCAT, :concat_right => DEFAULT_CONCAT }.merge(opts)
result =
case self
when Left
case other
when Left then Left(opts[:concat_left].call(self.left_value, other.left_value))
when Right then Left(self.left_value)
end
when Right
case other
when Left then Left(other.left_value)
when Right then Right(opts[:concat_right].call(self.right_value, other.right_value))
end
end
if block_given? then result.right.map { |right_values| yield right_values } else result end
end
alias_method :concat, :+
# @return [Either] returns an +Either+ of the same type, with the +left_value+ or +right_value+
# lifted into an +Array+
def lift_to_a
lift(Array)
end
# @param [#unit] monad_class the {Monad} to lift the +Left+ or +Right+ value into
# @return [Either] returns an +Either+of the same type, with the +left_value+ or +right_value+
# lifted into +monad_class+
def lift(monad_class)
fold(lambda {|l| Left(monad_class.unit(l)) }, lambda {|r| Right(monad_class.unit(r))})
end
end
# The left side of the disjoint union, as opposed to the Right side.
class Left < Either
# @param left_value the value to store in a +Left+, usually representing a failure result
def initialize(left_value)
@left_value = left_value
end
# @return Returns the left value
attr_reader :left_value
# @return [Boolean] Returns +true+ if other is a +Left+ with an equal left value
def ==(other)
other.is_a?(Left) && other.left_value == self.left_value
end
# @return [String] Returns a +String+ representation of this object.
def to_s
"Left(#{left_value})"
end
# @return [String] Returns a +String+ containing a human-readable representation of this object.
def inspect
"Left(#{left_value.inspect})"
end
end
# The right side of the disjoint union, as opposed to the Left side.
class Right < Either
# @param right_value the value to store in a +Right+, usually representing a success result
def initialize(right_value)
@right_value = right_value
end
# @return Returns the right value
attr_reader :right_value
# @return [Boolean] Returns +true+ if other is a +Right+ with an equal right value
def ==(other)
other.is_a?(Right) && other.right_value == self.right_value
end
# @return [String] Returns a +String+ representation of this object.
def to_s
"Right(#{right_value})"
end
# @return [String] Returns a +String+ containing a human-readable representation of this object.
def inspect
"Right(#{right_value.inspect})"
end
end
# @param (see Left#initialize)
# @return [Left]
def Left(left_value)
Left.new(left_value)
end
# @param (see Right#initialize)
# @return [Right]
def Right(right_value)
Right.new(right_value)
end
class Either
# Projects an Either into a Left.
class LeftProjection
class << self
# @return [LeftProjection] Returns a +LeftProjection+ of the +Left+ of the given value
def unit(value)
self.new(Left(value))
end
# @return [LeftProjection] Returns the empty +LeftProjection+
def empty
self.new(Right(nil))
end
end
# @param either_value [Object] the Either value to project
def initialize(either_value)
@either_value = either_value
end
# @return Returns the Either value
attr_reader :either_value
# @return [Boolean] Returns +true+ if other is a +LeftProjection+ with an equal +Either+ value
def ==(other)
other.is_a?(LeftProjection) && other.either_value == self.either_value
end
# Binds the given function across +Left+.
def bind(lam = nil, &blk)
if !either_value.left? then either_value else (lam || blk).call(either_value.left_value) end
end
include Monad
# @return [Boolean] Returns +false+ if +Right+ or returns the result of the application of the given function to the +Left+ value.
def any?(lam = nil, &blk)
either_value.left? && bind(lam || blk)
end
# @return [Option] Returns +None+ if this is a +Right+ or if the given predicate does not hold for the +left+ value, otherwise, returns a +Some+ of +Left+.
def select(lam = nil, &blk)
Some(self).select { |lp| lp.any?(lam || blk) }.map { |lp| lp.either_value }
end
# @return [Boolean] Returns +true+ if +Right+ or returns the result of the application of the given function to the +Left+ value.
def all?(lam = nil, &blk)
!either_value.left? || bind(lam || blk)
end
# Returns the value from this +Left+ or raises +NoSuchElementException+ if this is a +Right+.
def get
if either_value.left? then either_value.left_value else raise NoSuchElementError end
end
# Returns the value from this +Left+ or the given argument if this is a +Right+.
def get_or_else(val_or_lam = nil, &blk)
v_or_f = val_or_lam || blk
if either_value.left? then either_value.left_value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
end
# @return [Option] Returns a +Some+ containing the +Left+ value if it exists or a +None+ if this is a +Right+.
def to_opt
Option(get_or_else(nil))
end
# @return [Either] Maps the function argument through +Left+.
def map(lam = nil, &blk)
bind { |v| Left((lam || blk).call(v)) }
end
# @return [String] Returns a +String+ representation of this object.
def to_s
"LeftProjection(#{either_value})"
end
# @return [String] Returns a +String+ containing a human-readable representation of this object.
def inspect
"LeftProjection(#{either_value.inspect})"
end
end
# Projects an Either into a Right.
class RightProjection
class << self
# @return [RightProjection] Returns a +RightProjection+ of the +Right+ of the given value
def unit(value)
self.new(Right(value))
end
# @return [RightProjection] Returns the empty +RightProjection+
def empty
self.new(Left(nil))
end
end
# @param either_value [Object] the Either value to project
def initialize(either_value)
@either_value = either_value
end
# @return Returns the Either value
attr_reader :either_value
# @return [Boolean] Returns +true+ if other is a +RightProjection+ with an equal +Either+ value
def ==(other)
other.is_a?(RightProjection) && other.either_value == self.either_value
end
# Binds the given function across +Right+.
def bind(lam = nil, &blk)
if !either_value.right? then either_value else (lam || blk).call(either_value.right_value) end
end
include Monad
# @return [Boolean] Returns +false+ if +Left+ or returns the result of the application of the given function to the +Right+ value.
def any?(lam = nil, &blk)
either_value.right? && bind(lam || blk)
end
# @return [Option] Returns +None+ if this is a +Left+ or if the given predicate does not hold for the +Right+ value, otherwise, returns a +Some+ of +Right+.
def select(lam = nil, &blk)
Some(self).select { |lp| lp.any?(lam || blk) }.map { |lp| lp.either_value }
end
# @return [Boolean] Returns +true+ if +Left+ or returns the result of the application of the given function to the +Right+ value.
def all?(lam = nil, &blk)
!either_value.right? || bind(lam || blk)
end
# Returns the value from this +Right+ or raises +NoSuchElementException+ if this is a +Left+.
def get
if either_value.right? then either_value.right_value else raise NoSuchElementError end
end
# Returns the value from this +Right+ or the given argument if this is a +Left+.
def get_or_else(val_or_lam = nil, &blk)
v_or_f = val_or_lam || blk
if either_value.right? then either_value.right_value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
end
# @return [Option] Returns a +Some+ containing the +Right+ value if it exists or a +None+ if this is a +Left+.
def to_opt
Option(get_or_else(nil))
end
# @return [Either] Maps the function argument through +Right+.
def map(lam = nil, &blk)
bind { |v| Right((lam || blk).call(v)) }
end
# @return [String] Returns a +String+ representation of this object.
def to_s
"RightProjection(#{either_value})"
end
# @return [String] Returns a +String+ containing a human-readable representation of this object.
def inspect
"RightProjection(#{either_value.inspect})"
end
end
end
module_function :Left, :Right
public :Left, :Right
end