Skip to content

Commit

Permalink
Implement Fear::Either#left
Browse files Browse the repository at this point in the history
Projects this `Fear::Either` as a `Fear::Left`. This allows performing right-biased operation of the left
side of the `Fear::Either`:

```ruby
Fear.left(42).left.map(&:succ)  #=> Fear.left(43)
Fear.right(42).left.map(&:succ) #=> Fear.left(42)
```
  • Loading branch information
bolshakov committed Mar 24, 2024
1 parent 2871aac commit 05845a0
Show file tree
Hide file tree
Showing 9 changed files with 591 additions and 10 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,20 @@ Fear.left('left').swap #=> Fear.right('left')
Fear.right('right').swap #=> Fear.left('left')
```

#### Either#left

Projects this `Fear::Either` as a `Fear::Left`.
This allows performing right-biased operation of the left
side of the `Fear::Either`.

```ruby
Fear.left(42).left.map(&:succ) #=> Fear.left(43)
Fear.right(42).left.map(&:succ) #=> Fear.left(42)

Fear.left(42).left.select(&:even?) #=> Fear.left(42)
Fear.right(42).left.select(&:odd?) #=> Fear.right(42)
```

#### Either#reduce

Applies `reduce_left` if this is a `Left` or `reduce_right` if this is a `Right`.
Expand Down
3 changes: 0 additions & 3 deletions lib/fear.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ module Fear
NoSuchElementError = Class.new(Error)
public_constant :NoSuchElementError

PatternSyntaxError = Class.new(Error)
public_constant :PatternSyntaxError

extend EitherApi
extend ForApi
extend FutureApi
Expand Down
22 changes: 18 additions & 4 deletions lib/fear/either.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ module Fear
# @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
# Fear.right(12).any? { |v| v > 10 } #=> true
# Fear.right(7).any? { |v| v > 10 } #=> false
# Fear.left('undefined').any? { |v| v > 10 } #=> false
#
# -----
#
Expand Down Expand Up @@ -177,7 +177,7 @@ module Fear
#
# @!method swap
# If this is a +Left+, then return the left value in +Right+ or vice versa.
# @return [Either]
# @return [Fear::Either]
# @example
# Fear.left('left').swap #=> Fear.right('left')
# Fear.right('right').swap #=> Fear.left('left')
Expand Down Expand Up @@ -280,6 +280,19 @@ 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,
Expand Down Expand Up @@ -335,3 +348,4 @@ def Right(value)
require "fear/either_pattern_match"
require "fear/left"
require "fear/right"
require "fear/either/left_projection"
230 changes: 230 additions & 0 deletions lib/fear/either/left_projection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# frozen_string_literal: true

module Fear
module Either
# Projects an `Either` into a `Left`.
# @see Fear::Either#left
#
class LeftProjection
extend Utils::Assertions

# @!attribute either
# @return [Fear::Either]
attr_reader :either
protected :either

# @param either [Fear::Either]
def initialize(either)
@either = either
end

# Returns +true+ if +Fear::Left+ has an element that is equal
# (as determined by +==+) to +other_value+, +false+ otherwise.
# @param [Object]
# @return [Boolean]
# @example
# Fear.left(17).left.include?(17) #=> true
# Fear.left(17).left.include?(7) #=> false
# Fear.right('undefined').left.include?(17) #=> false
#
def include?(other_value)
case either
in Fear::Left(x)
x == other_value
in Fear::Right
false
end
end

# Returns the value from this +Fear::Left+ or evaluates the given
# default argument if this is a +Fear::Right+.
#
# @overload get_or_else(&default)
# @yieldreturn [Object]
# @return [Object]
# @example
# Fear.right(42).left.get_or_else { 24/2 } #=> 12
# Fear.left('undefined').left.get_or_else { 24/2 } #=> 'undefined'
#
# @overload get_or_else(default)
# @return [Object]
# @example
# Fear.right(42).left.get_or_else(12) #=> 12
# Fear.left('undefined').left.get_or_else(12) #=> 'undefined'
def get_or_else(*args)
case either
in Fear::Left(value)
value
in Fear::Right
args.fetch(0) { yield }
end
end
assert_arg_or_block :get_or_else

# Performs the given block if this is a +Fear::Left+.
#
# @yieldparam [Object] value
# @yieldreturn [void]
# @return [Fear::Either] itself
# @example
# Fear.right(17).left.each do |value|
# puts value
# end #=> does nothing
#
# Fear.left('undefined').left.each do |value|
# puts value
# end #=> prints "nothing"
def each
case either
in Fear::Left(value)
yield(value)
either
in Fear::Right
either
end
end

# Maps the block argument through +Fear::Left+.
#
# @yieldparam [Object] value
# @yieldreturn [Fear::Either]
# @example
# Fear.left(42).left.map { _1/2 } #=> Fear.left(24)
# Fear.right(42).left.map { _1/2 } #=> Fear.right(42)
#
def map
case either
in Fear::Left(value)
Fear.left(yield(value))
in Fear::Right
either
end
end

# Returns the given block applied to the value from this +Fear::Left+
# or returns this if this is a +Fear::Right+.
#
# @yieldparam [Object] value
# @yieldreturn [Fear::Either]
# @return [Fear::Either]
#
# @example
# Fear.left(12).left.flat_map { Fear.left(_1 * 2) } #=> Fear.left(24)
# Fear.left(12).left.flat_map { Fear.right(_1 * 2) } #=> Fear.right(24)
# Fear.right(12).left.flat_map { Fear.left(_1 * 2) } #=> Fear.right(12)
#
def flat_map
case either
in Fear::Left(value)
yield(value)
in Fear::Right
either
end
end
assert_return Fear::Either, :flat_map

# Returns an +Fear::Some+ containing the +Fear::Left+ value or a +Fear::None+ if
# this is a +Fear::Right+.
# @return [Fear::Option]
# @example
# Fear.left(42).left.to_option #=> Fear.some(42)
# Fear.right(42).left.to_option #=> Fear.none
#
def to_option
case either
in Fear::Left(value)
Fear.some(value)
in Fear::Right
Fear.none
end
end

# Returns an array containing the +Fear::Left+ value or an empty array if
# this is a +Fear::Right+.
#
# @return [Array]
# @example
# Fear.left(42).left.to_a #=> [42]
# Fear.right(42).left.to_a #=> []
#
def to_a
case either
in Fear::Left(value)
[value]
in Fear::Right
[]
end
end

# Returns +false+ if +Fear::Right+ or returns the result of the
# application of the given predicate to the +Fear::Light+ value.
#
# @yieldparam [Object] value
# @yieldreturn [Boolean]
# @return [Boolean]
# @example
# Fear.left(12).left.any? { |v| v > 10 } #=> true
# Fear.left(7).left.any? { |v| v > 10 } #=> false
# Fear.right(12).left.any? { |v| v > 10 } #=> false
#
def any?(&predicate)
case either
in Fear::Left(value)
predicate.(value)
in Fear::Right
false
end
end

# Returns +Fear::Right+ of value if the given predicate
# does not hold for the left value, otherwise, returns +Fear::Left+.
#
# @yieldparam value [Object]
# @yieldreturn [Boolean]
# @return [Fear::Either]
# @example
# Fear.left(12).left.select(&:even?) #=> Fear.left(12)
# Fear.left(7).left.select(&:even?) #=> Fear.right(7)
# Fear.right(12).left.select(&:even?) #=> Fear.right(12)
# Fear.right(7).left.select(&:even?) #=> Fear.right(7)
#
def select(&predicate)
case either
in Fear::Right
either
in Fear::Left(value) if predicate.(value)
either
in Fear::Left
either.swap
end
end

# Returns +Fear::None+ if this is a +Fear::Right+ or if the given predicate
# does not hold for the left value, otherwise, returns a +Fear::Left+.
#
# @yieldparam value [Object]
# @yieldreturn [Boolean]
# @return [Fear::Option<Fear::Either>]
# @example
# Fear.left(12).left.find(&:even?) #=> #<Fear::Some value=#<Fear::Left value=12>>
# Fear.left(7).left.find(&:even?) #=> #<Fear::None>
# Fear.right(12).left.find(&:even) #=> #<Fear::None>
#
def find(&predicate)
case either
in Fear::Left(value) if predicate.(value)
Fear.some(either)
in Fear::Either
Fear.none
end
end
alias detect find

# @param other [Object]
# @return [Boolean]
def ==(other)
other.is_a?(self.class) && other.either == either
end
end
end
end
4 changes: 2 additions & 2 deletions lib/fear/either_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

module Fear
module EitherApi
# @param value [any]
# @param value [Object]
# @return [Fear::Left]
# @example
# Fear.left(42) #=> #<Fear::Left value=42>
Expand All @@ -13,7 +13,7 @@ def left(value)
Fear::Left.new(value)
end

# @param value [any]
# @param value [Object]
# @return [Fear::Right]
# @example
# Fear.right(42) #=> #<Fear::Right value=42>
Expand Down
2 changes: 1 addition & 1 deletion lib/fear/option_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def none
Fear::None
end

# @param value [any]
# @param value [Object]
# @return [Fear::Some]
# @example
# Fear.some(17) #=> #<Fear::Some get=17>
Expand Down
2 changes: 2 additions & 0 deletions lib/fear/utils.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "fear/utils/assertions"

module Fear
# @private
module Utils
Expand Down
35 changes: 35 additions & 0 deletions lib/fear/utils/assertions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Fear
module Utils
# @api private
module Assertions
def assert_return(expected_type, method_name)
alias_method "#{method_name}_without_return_type_check", method_name

define_method(method_name) do |*args, &block|
result = __send__("#{method_name}_without_return_type_check", *args, &block)

unless expected_type === result
raise TypeError, "#{self.class}##{method_name} expected to return #{expected_type.inspect}, " \
"but returned #{result.inspect}"
end

result
end
end

def assert_arg_or_block(method_name)
alias_method "#{method_name}_without_arg_or_block_check", method_name

define_method(method_name) do |*args, &block|
unless !block.nil? ^ !args.empty?
raise ArgumentError, "##{method_name} accepts either one argument or block"
end

__send__("#{method_name}_without_arg_or_block_check", *args, &block)
end
end
end
end
end
Loading

0 comments on commit 05845a0

Please sign in to comment.