Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pattern matching without Qo #28

Merged
merged 5 commits into from
Mar 11, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -5,6 +5,9 @@ AllCops:
Naming/MethodName:
Enabled: false

Naming/UncommunicativeMethodParamName:
Enabled: false

Style/Documentation:
Enabled: false

@@ -29,9 +32,14 @@ Style/TrailingCommaInArrayLiteral:
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: comma

Style/NumericPredicate:
Enabled: false

Metrics/BlockLength:
Exclude:
- spec/**/*
- Rakefile
- fear.gemspec

Metrics/LineLength:
Max: 120
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -4,3 +4,5 @@ source 'https://rubygems.org'
gemspec

# gem 'codeclimate-test-reporter', group: :test, require: nil

gem 'qo', github: 'baweaver/qo'
245 changes: 216 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -48,12 +48,9 @@ having to check for the existence of a value.
A less-idiomatic way to use `Option` values is via pattern matching

```ruby
name = Option(params[:name])
case name
when Some
puts name.strip.upcase
when None
puts 'No name value'
Option(params[:name]).match do |m|
m.some { |name| name.strip.upcase }
m.none { 'No name value' }
end
```

@@ -201,11 +198,19 @@ dividend = Try { Integer(params[:dividend]) }
divisor = Try { Integer(params[:divisor]) }
problem = dividend.flat_map { |x| divisor.map { |y| x / y } }

if problem.success?
puts "Result of #{dividend.get} / #{divisor.get} is: #{problem.get}"
else
puts "You must've divided by zero or entered something wrong. Try again"
puts "Info from the exception: #{problem.exception.message}"
problem.match |m|
m.success do |result|
puts "Result of #{dividend.get} / #{divisor.get} is: #{result}"
end

m.failure(ZeroDivisionError) do
puts "Division by zero is not allowed"
end

m.failure do |exception|
puts "You entered something wrong. Try again"
puts "Info from the exception: #{exception.message}"
end
end
```

@@ -408,12 +413,15 @@ rescue ArgumentError
Left(in)
end

puts(
result.reduce(
-> (x) { "You passed me the String: #{x}" },
-> (x) { "You passed me the Int: #{x}, which I will increment. #{x} + 1 = #{x+1}" }
)
)
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
@@ -688,23 +696,202 @@ For(maybe_user, ->(user) { user.birthday }) do |user, birthday|
end #=> Some('Paul was born on 1987-06-17')
```

### Pattern Matching
### Pattern Matching (See API Documentation)

Pattern matcher is a combination of partial functions wrapped into nice DSL. Every partial function
defined on domain described with guard.

`Option`, `Either`, and `Try` contains enhanced version of `#===` method. It performs matching not
only on container itself, but on enclosed value as well. I'm writing all the options in a one
case statement in sake of simplicity.
```ruby
pf = Fear.case(Integer) { |x| x / 2 }
pf.defined_at?(4) #=> true
pf.defined_at?('Foo') #=> false
pf.call('Foo') #=> raises Fear::MatchError
pf.call_or_else('Foo') { 'not a number' } #=> 'not a number'
pf.call_or_else(4) { 'not a number' } #=> 2
pf.lift.call('Foo') #=> Fear::None
pf.lift.call(4) #=> Fear::Some(2)
```

It uses `#===` method under the hood, so you can pass:

* Class to check kind of an object.
* Lambda to evaluate it against an object.
* Any literal, like `4`, `"Foobar"`, etc.
* Symbol -- it is converted to lambda using `#to_proc` method.
* Qo matcher -- `m.case(Qo[name: 'John']) { .... }`

Partial functions may be combined with each other:

```ruby
is_even = Fear.case(->(arg) { arg % 2 == 0}) { |arg| "#{arg} is even" }
is_odd = Fear.case(->(arg) { arg % 2 == 1}) { |arg| "#{arg} is odd" }

(10..20).map(&is_even.or_else(is_odd))

to_integer = Fear.case(String, &:to_i)
integer_two_times = Fear.case(Integer) { |x| x * 2 }

two_times = to_integer.and_then(integer_two_times).or_else(integer_two_times)
two_times.(4) #=> 8
two_times.('42') #=> 84
```

To create custom pattern match use `Fear.match` method and `case` builder to define
branches. For instance this matcher applies different functions to Integers and Strings

```ruby
Fear.match(value) do |m|
m.case(Integer) { |n| "#{n} is a number" }
m.case(String) { |n| "#{n} is a string" }
end
```

if you pass something other than Integer or string, it will raise `Fear::MatchError` error.
To avoid raising `MatchError`, you can use `else` method. It defines a branch matching
on any value.

```ruby
Fear.match(10..20) do |m|
m.case(Integer) { |n| "#{n} is a number" }
m.case(String) { |n| "#{n} is a string" }
m.else { |n| "#{n} is a #{n.class}" }
end #=> "10..20 is a Range"
```

You can use anything as a guardian if it responds to `#===` method:

```ruby
m.case(20..40) { |m| "#{m} is within range" }
m.case(->(x) { x > 10}) { |m| "#{m} is greater than 10" }
```

If you pass a Symbol, it will be converted to proc using `#to_proc` method

```ruby
m.case(:even?) { |x| "#{x} is even" }
m.case(:odd?) { |x| "#{x} is odd" }
```

It's also possible to pass several guardians. All should match to pass

```ruby
m.case(Integer, :even?) { |x| ... }
m.case(Integer, :odd?) { |x| ... }
```

It's also possible to create matcher and use it several times:

```ruby
matcher = Fear.matcher do |m|
m.case(Integer) { |n| "#{n} is a number" }
m.case(String) { |n| "#{n} is a string" }
m.else { |n| "#{n} is a #{n.class}" }
end

matcher.(42) #=> "42 is a number"
matcher.(10..20) #=> "10..20 is a Range"
```

Since matcher is just a syntactic sugar for partial functions, you can combine matchers with partial
functions and each other.

```ruby
handle_numbers = Fear.case(Integer, &:itself).and_then(
Fear.matcher do |m|
m.case(0) { 'zero' }
m.case(->(n) { n < 10 }) { 'smaller than ten' }
m.case(->(n) { n > 10 }) { 'bigger than ten' }
end
)

handle_strings = Fear.case(String, &:itself).and_then(
Fear.matcher do |m|
m.case('zero') { 0 }
m.case('one') { 1 }
m.else { 'unexpected' }
end
)

handle = handle_numbers.or_else(handle_strings)
handle.(0) #=> 'zero'
handle.(12) #=> 'bigger than ten'
handle.('one') #=> 1
```

#### More examples

Factorial using pattern matching

```ruby
factorial = Fear.matcher do |m|
m.case(->(n) { n <= 1} ) { 1 }
m.else { |n| n * factorial.(n - 1) }
end

factorial.(10) #=> 3628800
```

Fibonacci number

```ruby
fibonnaci = Fear.matcher do |m|
m.case(0) { 0 }
m.case(1) { 1 }
m.case(->(n) { n > 1}) { |n| fibonnaci.(n - 1) + fibonnaci.(n - 2) }
m.else { raise 'should be positive' }
end

fibonnaci.(10) #=> 55
```

#### Monads pattern matching

You can use `Option#match`, `Either#match`, and `Try#match` method. It performs matching not
only on container itself, but on enclosed value as well.

Pattern match against an `Option`

```ruby
case Some(42)
when Some(42) #=> matches
when Some(41) #=> does not match
when Some(Fixnum) #=> matches
when Some(String) #=> does not match
when Some((40..43)) #=> matches
when Some(-> (x) { x > 40 }) #=> matches
end
Some(42).match do |m|
m.some { |x| x * 2 }
m.none { 'none' }
end #=> 84
```

pattern match on enclosed value

```ruby
Some(41).match do |m|
m.some(:even?) { |x| x / 2 }
m.some(:odd?, ->(v) { v > 0 }) { |x| x * 2 }
m.none { 'none' }
end #=> 82
```

it raises `Fear::MatchError` error if nothing matched. To avoid exception, you can pass `#else` branch

```ruby
Some(42).match do |m|
m.some(:odd?) { |x| x * 2 }
m.else { 'nothing' }
end #=> nothing
```

Pattern matching works the similar way for `Either` and `Try` monads.

In sake of performance, you may want to generate pattern matching function and reuse it multiple times:

```ruby
matcher = Option.matcher do |m|
m.some(42) { 'Yep' }
m.some { 'Nope' }
m.none { 'Error' }
end

matcher.(Some(42)) #=> 'Yep'
matcher.(Some(40)) #=> 'Nope'
```

## Testing

To simplify testing, you may use [fear-rspec](https://github.com/bolshakov/fear-rspec) gem. It
393 changes: 393 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1 +1,394 @@
require 'bundler/gem_tasks'
require 'benchmark/ips'
require_relative 'lib/fear'

namespace :perf do
namespace :guard do
task :and1 do
condition = Integer

Benchmark.ips do |x|
x.report('Guard.new') do |n|
Fear::PartialFunction::Guard.new(condition) === n
end

x.report('Guard.single') do |n|
Fear::PartialFunction::Guard.and1(condition) === n
end

x.compare!
end
end

task :and1 do
first = Integer

and1 = Fear::PartialFunction::Guard.and1(first)
guard = Fear::PartialFunction::Guard.new(first)

Benchmark.ips do |x|
x.report('guard') do |n|
and1 === n
end

x.report('single') do |n|
guard === n
end

x.compare!
end
end

task :and2 do
first = Integer
second = ->(x) { x > 2 }

and2 = Fear::PartialFunction::Guard.and2(first, second)
and_and = Fear::PartialFunction::Guard.new(first).and(Fear::PartialFunction::Guard.new(second))

Benchmark.ips do |x|
x.report('and2') do |n|
and2 === n
end

x.report('Guard#and') do |n|
and_and === n
end

x.compare!
end
end

task :and3 do
first = Integer
second = ->(x) { x > 2 }
third = ->(x) { x < 10 }

and3 = Fear::PartialFunction::Guard.and3(first, second, third)

and_and_and = Fear::PartialFunction::Guard.new(first)
.and(Fear::PartialFunction::Guard.new(second))
.and(Fear::PartialFunction::Guard.new(third))

Benchmark.ips do |x|
x.report('Guard.and3') do |n|
and3 === n
end

x.report('Guard#and') do |n|
and_and_and === n
end

x.compare!
end
end
end

require 'qo'
require 'dry/matcher'

namespace :pattern_matching do
task :try do
module ExhaustivePatternMatch
def initialize(*)
super
@default ||= self.else { raise Fear::MatchError }
end
end

SuccessBranch = Qo.create_branch(name: 'success', precondition: Fear::Success, extractor: :get)
FailureBranch = Qo.create_branch(name: 'failure', precondition: Fear::Failure, extractor: :exception)

PatternMatch = Qo.create_pattern_match(
branches: [
SuccessBranch,
FailureBranch,
],
).prepend(ExhaustivePatternMatch)

Fear::Success.include(PatternMatch.mixin(as: :qo_match))

success_case = Dry::Matcher::Case.new(
match: lambda { |try, *pattern|
try.is_a?(Fear::Success) && pattern.all? { |p| p === try.get }
},
resolve: ->(try) { try.get },
)

failure_case = Dry::Matcher::Case.new(
match: lambda { |try, *pattern|
try.is_a?(Fear::Failure) && pattern.all? { |p| p === try.exception }
},
resolve: ->(value) { value.exception },
)

# Build the matcher
matcher = Dry::Matcher.new(success: success_case, failure: failure_case)

success = Fear::Success.new(4)

Benchmark.ips do |x|
x.report('Qo') do
success.qo_match do |m|
m.failure { |y| y }
m.success(->(y) { y % 4 == 0 }) { |y| y }
m.success { 'else' }
end
end

x.report('Fear') do
success.match do |m|
m.failure { |y| y }
m.success(->(y) { y % 4 == 0 }) { |y| y }
m.success { 'else' }
end
end

x.report('Dr::Matcher') do
matcher.call(success) do |m|
m.failure { |_y| 'failure' }
m.success(->(y) { y % 4 == 0 }) { |y| "2: #{y}" }
m.success { 'else' }
end
end

x.compare!
end
end

task :either do
module ExhaustivePatternMatch
def initialize(*)
super
@default ||= self.else { raise Fear::MatchError }
end
end

RightBranch = Qo.create_branch(name: 'right', precondition: Fear::Right, extractor: :right_value)
LeftBranch = Qo.create_branch(name: 'left', precondition: Fear::Left, extractor: :left_value)

PatternMatch = Qo.create_pattern_match(
branches: [
RightBranch,
LeftBranch,
],
).prepend(ExhaustivePatternMatch)

Fear::Right.include(PatternMatch.mixin(as: :qo_match))

right = Fear::Right.new(4)

Benchmark.ips do |x|
x.report('Qo') do
right.qo_match do |m|
m.left(->(y) { y % 3 == 0 }) { |y| y }
m.right(->(y) { y % 4 == 0 }) { |y| y }
m.else { 'else' }
end
end

x.report('Fear') do
right.match do |m|
m.left(->(y) { y % 3 == 0 }) { |y| y }
m.right(->(y) { y % 4 == 0 }) { |y| y }
m.else { 'else' }
end
end

x.compare!
end
end

task :option do
module ExhaustivePatternMatch
def initialize(*)
super
@default ||= self.else { raise Fear::MatchError }
end
end

SomeBranch = Qo.create_branch(name: 'some', precondition: Fear::Some, extractor: :get)
NoneBranch = Qo.create_branch(name: 'none', precondition: Fear::None)

PatternMatch = Qo.create_pattern_match(
branches: [
SomeBranch,
NoneBranch,
],
).prepend(ExhaustivePatternMatch)

Fear::Some.include(PatternMatch.mixin(as: :qo_match))

some = Fear::Some.new(4)

some_case = Dry::Matcher::Case.new(
match: lambda { |option, *pattern|
option.is_a?(Fear::Some) && pattern.all? { |p| p === option.get }
},
resolve: ->(try) { try.get },
)

none_case = Dry::Matcher::Case.new(
match: lambda { |option, *pattern|
Fear::None == option && pattern.all? { |p| p === option }
},
resolve: ->(value) { value },
)

else_case = Dry::Matcher::Case.new(
match: ->(*) { true },
resolve: ->(value) { value },
)

# Build the matcher
matcher = Dry::Matcher.new(some: some_case, none: none_case, else: else_case)

option_matcher = Fear::Option.matcher do |m|
m.some(->(y) { y % 3 == 0 }) { |y| y }
m.some(->(y) { y % 4 == 0 }) { |y| y }
m.none { 'none' }
m.else { 'else' }
end

Benchmark.ips do |x|
x.report('Qo') do
some.qo_match do |m|
m.some(->(y) { y % 3 == 0 }) { |y| y }
m.some(->(y) { y % 4 == 0 }) { |y| y }
m.none { 'none' }
m.else { 'else' }
end
end

x.report('Fear::Some#math') do
some.match do |m|
m.some(->(y) { y % 3 == 0 }) { |y| y }
m.some(->(y) { y % 4 == 0 }) { |y| y }
m.none { 'none' }
m.else { 'else' }
end
end

x.report('Fear::Option.mather') do
option_matcher.call(some)
end

x.report('Dry::Matcher') do
matcher.call(some) do |m|
m.some(->(y) { y % 3 == 0 }) { |y| y }
m.some(->(y) { y % 4 == 0 }) { |y| y }
m.none { 'none' }
m.else { 'else' }
end
end

x.compare!
end
end

task :option_execution do
module ExhaustivePatternMatch
def initialize(*)
super
@default ||= self.else { raise Fear::MatchError }
end
end

SomeBranch = Qo.create_branch(name: 'some', precondition: Fear::Some, extractor: :get)
NoneBranch = Qo.create_branch(name: 'none', precondition: Fear::None)

PatternMatch = Qo.create_pattern_match(
branches: [
SomeBranch,
NoneBranch,
],
).prepend(ExhaustivePatternMatch)

some = Fear::Some.new(4)

qo_matcher = PatternMatch.new do |m|
m.some(->(y) { y % 3 == 0 }) { |y| y }
m.some(->(y) { y % 4 == 0 }) { |y| y }
m.none { 'none' }
m.else { 'else' }
end

fear_matcher = Fear::OptionPatternMatch.new do |m|
m.some(->(y) { y % 3 == 0 }) { |y| y }
m.some(->(y) { y % 4 == 0 }) { |y| y }
m.none { 'none' }
m.else { 'else' }
end

Benchmark.ips do |x|
x.report('Qo') do
qo_matcher.call(some)
end

x.report('Fear') do
fear_matcher.call(some)
end

x.compare!
end
end

task :factorial do
factorial_proc = proc do |n|
if n <= 1
1
else
n * factorial_proc.call(n - 1)
end
end

factorial_pm = Fear.matcher do |m|
m.case(->(n) { n <= 1 }) { 1 }
m.else { |n| n * factorial_pm.call(n - 1) }
end

factorial_qo = Qo.match do |m|
m.when(->(n) { n <= 1 }) { 1 }
m.else { |n| n * factorial_qo.call(n - 1) }
end

Benchmark.ips do |x|
x.report('Proc') do
factorial_proc.call(100)
end

x.report('Fear') do
factorial_pm.call(100)
end

x.report('Qo') do
factorial_qo.call(100)
end

x.compare!
end
end

task :construction_vs_execution do
matcher = Fear::PatternMatch.new do |m|
m.case(Integer) { |x| x * 2 }
m.case(String) { |x| x.to_i(10) * 2 }
end

Benchmark.ips do |x|
x.report('construction') do
Fear::PatternMatch.new do |m|
m.case(Integer) { |y| y * 2 }
m.case(String) { |y| y.to_i(10) * 2 }
end
end

x.report('execution') do
matcher.call(42)
end

x.compare!
end
end
end
end
4 changes: 4 additions & 0 deletions fear.gemspec
Original file line number Diff line number Diff line change
@@ -27,9 +27,13 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency 'dry-equalizer', '<= 0.2.1'

spec.add_development_dependency 'appraisal'
spec.add_development_dependency 'benchmark-ips'
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'dry-matcher'
spec.add_development_dependency 'qo'
spec.add_development_dependency 'rake', '~> 10.0'
spec.add_development_dependency 'rspec', '~> 3.1'
spec.add_development_dependency 'rubocop', '0.65.0'
spec.add_development_dependency 'rubocop-rspec', '1.32.0'
spec.add_development_dependency 'yard'
end
1 change: 1 addition & 0 deletions gemfiles/dry_equalizer_0.1.0.gemfile
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

source "https://rubygems.org"

gem "qo", github: "baweaver/qo"
gem "dry-equalizer", "0.1.0"

gemspec path: "../"
17 changes: 16 additions & 1 deletion gemfiles/dry_equalizer_0.1.0.gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
GIT
remote: git://github.com/baweaver/qo.git
revision: 8951ce899559118eb60321014b43cf4211730bd0
specs:
qo (0.99.0)
any (= 0.1.0)

PATH
remote: ..
specs:
@@ -7,15 +14,18 @@ PATH
GEM
remote: https://rubygems.org/
specs:
any (0.1.0)
appraisal (2.2.0)
bundler
rake
thor (>= 0.14.0)
ast (2.4.0)
benchmark-ips (2.7.2)
diff-lcs (1.3)
dry-equalizer (0.1.0)
dry-matcher (0.7.0)
jaro_winkler (1.5.2)
parallel (1.13.0)
parallel (1.14.0)
parser (2.6.0.0)
ast (~> 2.4.0)
powerpack (0.1.2)
@@ -49,19 +59,24 @@ GEM
ruby-progressbar (1.10.0)
thor (0.20.3)
unicode-display_width (1.4.1)
yard (0.9.18)

PLATFORMS
ruby

DEPENDENCIES
appraisal
benchmark-ips
bundler
dry-equalizer (= 0.1.0)
dry-matcher
fear!
qo!
rake (~> 10.0)
rspec (~> 3.1)
rubocop (= 0.65.0)
rubocop-rspec (= 1.32.0)
yard

BUNDLED WITH
1.16.2
1 change: 1 addition & 0 deletions gemfiles/dry_equalizer_0.2.1.gemfile
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

source "https://rubygems.org"

gem "qo", github: "baweaver/qo"
gem "dry-equalizer", "0.2.1"

gemspec path: "../"
17 changes: 16 additions & 1 deletion gemfiles/dry_equalizer_0.2.1.gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
GIT
remote: git://github.com/baweaver/qo.git
revision: 8951ce899559118eb60321014b43cf4211730bd0
specs:
qo (0.99.0)
any (= 0.1.0)

PATH
remote: ..
specs:
@@ -7,15 +14,18 @@ PATH
GEM
remote: https://rubygems.org/
specs:
any (0.1.0)
appraisal (2.2.0)
bundler
rake
thor (>= 0.14.0)
ast (2.4.0)
benchmark-ips (2.7.2)
diff-lcs (1.3)
dry-equalizer (0.2.1)
dry-matcher (0.7.0)
jaro_winkler (1.5.2)
parallel (1.13.0)
parallel (1.14.0)
parser (2.6.0.0)
ast (~> 2.4.0)
powerpack (0.1.2)
@@ -49,19 +59,24 @@ GEM
ruby-progressbar (1.10.0)
thor (0.20.3)
unicode-display_width (1.4.1)
yard (0.9.18)

PLATFORMS
ruby

DEPENDENCIES
appraisal
benchmark-ips
bundler
dry-equalizer (= 0.2.1)
dry-matcher
fear!
qo!
rake (~> 10.0)
rspec (~> 3.1)
rubocop (= 0.65.0)
rubocop-rspec (= 1.32.0)
yard

BUNDLED WITH
1.16.2
26 changes: 21 additions & 5 deletions lib/fear.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
require 'dry-equalizer'
require 'fear/version'
require 'fear/pattern_matching_api'

module Fear
Error = Class.new(StandardError)
IllegalStateException = Class.new(Error)
NoSuchElementError = Class.new(Error)
MatchError = Class.new(Error)
extend PatternMatchingApi

autoload :EmptyPartialFunction, 'fear/empty_partial_function'
autoload :PartialFunction, 'fear/partial_function'
autoload :PartialFunctionClass, 'fear/partial_function_class'
autoload :PatternMatch, 'fear/pattern_match'

autoload :Done, 'fear/done'
autoload :For, 'fear/for'
autoload :RightBiased, 'fear/right_biased'
autoload :Utils, 'fear/utils'

autoload :None, 'fear/none'
autoload :NoneClass, 'fear/none'
autoload :NonePatternMatch, 'fear/none_pattern_match'
autoload :Option, 'fear/option'
autoload :OptionPatternMatch, 'fear/option_pattern_match'
autoload :Some, 'fear/some'
autoload :NoneClass, 'fear/none'
autoload :None, 'fear/none'
autoload :SomePatternMatch, 'fear/some_pattern_match'

autoload :Try, 'fear/try'
autoload :Success, 'fear/success'
autoload :Failure, 'fear/failure'
autoload :FailurePatternMatch, 'fear/failure_pattern_match'
autoload :Success, 'fear/success'
autoload :SuccessPatternMatch, 'fear/success_pattern_match'
autoload :Try, 'fear/try'
autoload :TryPatternMatch, 'fear/try_pattern_match'

autoload :Either, 'fear/either'
autoload :EitherPatternMatch, 'fear/either_pattern_match'
autoload :Left, 'fear/left'
autoload :LeftPatternMatch, 'fear/left_pattern_match'
autoload :Right, 'fear/right'
autoload :RightPatternMatch, 'fear/right_pattern_match'

module Mixin
include Either::Mixin
55 changes: 49 additions & 6 deletions lib/fear/either.rb
Original file line number Diff line number Diff line change
@@ -19,12 +19,15 @@ module Fear
# Left(in)
# end
#
# puts(
# result.reduce(
# -> (x) { "You passed me the String: #{x}" },
# -> (x) { "You passed me the Int: #{x}, which I will increment. #{x} + 1 = #{x+1}" }
# )
# )
# 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
@@ -227,6 +230,23 @@ module Fear
# Right("daisy").join_left #=> Right("daisy")
# Right(Left("daisy")).join_left #=> Right(Left("daisy"))
#
# @!method match(&matcher)
# Pattern match against this +Either+
# @yield matcher [Fear::EitherPatternMatch]
# @example
# Either(val).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
@@ -249,6 +269,29 @@ def initialize(value)
attr_reader :value
protected :value

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(Some(42))
#
# @yieldparam [Fear::EitherPatternMatch]
# @return [Fear::PartialFunction]
def matcher(&matcher)
EitherPatternMatch.new(&matcher)
end
end

# Include this mixin to access convenient factory methods.
# @example
# include Fear::Either::Mixin
48 changes: 48 additions & 0 deletions lib/fear/either_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module Fear
# Either pattern matcher
#
# @example
# pattern_match =
# EitherPatternMatch.new
# .right(Integer, ->(x) { x > 2 }) { |x| x * 2 }
# .right(String) { |x| x.to_i * 2 }
# .left(String) { :err }
# .else { 'error '}
#
# pattern_match.call(42) => 'NaN'
#
# @example the same matcher may be defined using block syntax
# EitherPatternMatch.new 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
#
# @note it has two optimized subclasses +Fear::LeftPatternMatch+ and +Fear::RightPatternMatch+
# @api private
class EitherPatternMatch < Fear::PatternMatch
LEFT_EXTRACTOR = :left_value.to_proc
RIGHT_EXTRACTOR = :right_value.to_proc

# Match against +Fear::Right+
#
# @param conditions [<#==>]
# @return [Fear::EitherPatternMatch]
def right(*conditions, &effect)
branch = Fear.case(Fear::Right, &RIGHT_EXTRACTOR).and_then(Fear.case(*conditions, &effect))
or_else(branch)
end
alias success right

# Match against +Fear::Left+
#
# @param conditions [<#==>]
# @return [Fear::EitherPatternMatch]
def left(*conditions, &effect)
branch = Fear.case(Fear::Left, &LEFT_EXTRACTOR).and_then(Fear.case(*conditions, &effect))
or_else(branch)
end
alias failure left
end
end
36 changes: 36 additions & 0 deletions lib/fear/empty_partial_function.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Fear
# Use singleton version of EmptyPartialFunction -- PartialFunction::EMPTY
# @api private
module EmptyPartialFunction
include PartialFunction

def defined_at?(_)
false
end

def call(arg)
raise MatchError, "partial function not defined at: #{arg}"
end

alias === call
alias [] call

def call_or_else(arg)
yield arg
end

def or_else(other)
other
end

def and_then(*)
self
end

def to_s
'Empty partial function'
end
end

private_constant :EmptyPartialFunction
end
1 change: 1 addition & 0 deletions lib/fear/failure.rb
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ class Failure
include Try
include Dry::Equalizer(:exception)
include RightBiased::Left
include FailurePatternMatch.mixin

# @param [StandardError]
def initialize(exception)
8 changes: 8 additions & 0 deletions lib/fear/failure_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Fear
# @api private
class FailurePatternMatch < Fear::TryPatternMatch
def success(*_conditions)
self
end
end
end
6 changes: 6 additions & 0 deletions lib/fear/left.rb
Original file line number Diff line number Diff line change
@@ -2,6 +2,12 @@ module Fear
class Left
include Either
include RightBiased::Left
include LeftPatternMatch.mixin

# @api private
def left_value
value
end

# @return [false]
def right?
9 changes: 9 additions & 0 deletions lib/fear/left_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Fear
# @api private
class LeftPatternMatch < Fear::EitherPatternMatch
def right(*)
self
end
alias success right
end
end
7 changes: 7 additions & 0 deletions lib/fear/none.rb
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ class NoneClass
include Option
include Dry::Equalizer()
include RightBiased::Left
include NonePatternMatch.mixin

# @raise [NoSuchElementError]
def get
@@ -41,6 +42,12 @@ def to_s
def inspect
AS_STRING
end

# @param other
# @return [Boolean]
def ===(other)
self == other
end
end

private_constant(:NoneClass)
12 changes: 12 additions & 0 deletions lib/fear/none_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Fear
# @api private
class NonePatternMatch < OptionPatternMatch
# @param conditions [<#==>]
# @return [Fear::OptionPatternMatch]
def some(*_conditions)
self
end
end

private_constant :NonePatternMatch
end
54 changes: 47 additions & 7 deletions lib/fear/option.rb
Original file line number Diff line number Diff line change
@@ -11,12 +11,9 @@ module Fear
# having to check for the existence of a value.
#
# @example A less-idiomatic way to use +Option+ values is via pattern matching
# name = Option(params[:name])
# case name
# when Some
# puts name.strip.upcase
# when NoneClass
# puts 'No name value'
# Option(params[:name]).match do |m|
# m.some { |name| name.strip.upcase }
# m.none { 'No name value' }
# end
#
# @example or manually checking for non emptiness
@@ -145,6 +142,23 @@ module Fear
# Some(42).empty? #=> false
# None.empty? #=> true
#
# @!method match(&matcher)
# Pattern match against this +Option+
# @yield matcher [Fear::OptionPatternMatch]
# @example
# Option(val).match do |m|
# m.some(Integer) do |x|
# x * 2
# end
#
# m.some(String) do |x|
# x.to_i * 2
# end
#
# m.none { 'NaN' }
# m.else { 'error '}
# end
#
# @see https://github.com/scala/scala/blob/2.11.x/src/library/scala/Option.scala
#
module Option
@@ -158,14 +172,37 @@ def right_class
Some
end

class << self
# Build pattern matcher to be used later, despite off
# +Option#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 =
# Option.matcher do |m|
# m.some(Integer) { |x| x * 2 }
# m.some(String) { |x| x.to_i * 2 }
# m.none { 'NaN' }
# m.else { 'error '}
# end
# matcher.call(Some(42))
#
# @yieldparam [OptionPatternMatch]
# @return [Fear::PartialFunction]
def matcher(&matcher)
OptionPatternMatch.new(&matcher)
end
end

# Include this mixin to access convenient factory methods.
# @example
# include Fear::Option::Mixin
#
# Option(17) #=> #<Fear::Some value=17>
# Option(nil) #=> #<Fear::None>
# Some(17) #=> #<Fear::Some value=17>
# None #=> #<Fear::None>
# None #=> #<Fear::None>
#
module Mixin
None = Fear::None
@@ -175,6 +212,9 @@ module Mixin
# @param value [any]
# @return [Some, None]
#
# @example
# Option(v)
#
def Option(value)
if value.nil?
None
45 changes: 45 additions & 0 deletions lib/fear/option_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Fear
# Option pattern matcher
#
# @example
# pattern_match =
# OptionPatternMatch.new
# .some(Integer) { |x| x * 2 }
# .some(String) { |x| x.to_i * 2 }
# .none { 'NaN' }
# .else { 'error '}
#
# pattern_match.call(42) => 'NaN'
#
# @example the same matcher may be defined using block syntax
# OptionPatternMatch.new do |m|
# m.some(Integer) { |x| x * 2 }
# m.some(String) { |x| x.to_i * 2 }
# m.none { 'NaN' }
# m.else { 'error '}
# end
#
# @note it has two optimized subclasses +Fear::SomePatternMatch+ and +Fear::NonePatternMatch+
# @api private
class OptionPatternMatch < Fear::PatternMatch
GET_METHOD = :get.to_proc

# Match against Some
#
# @param conditions [<#==>]
# @return [Fear::OptionPatternMatch]
def some(*conditions, &effect)
branch = Fear.case(Fear::Some, &GET_METHOD).and_then(Fear.case(*conditions, &effect))
or_else(branch)
end

# Match against None
#
# @param effect [Proc]
# @return [Fear::OptionPatternMatch]
def none(&effect)
branch = Fear.case(Fear::None, &effect)
or_else(branch)
end
end
end
171 changes: 171 additions & 0 deletions lib/fear/partial_function.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
module Fear
# A partial function is a unary function defined on subset of all possible inputs.
# The method +defined_at?+ allows to test dynamically if an arg is in
# the domain of the function.
#
# Even if +defined_at?+ returns true for given arg, calling +call+ may
# still throw an exception, so the following code is legal:
#
# @example
# Fear.case(->(_) { true }) { 1/0 }
#
# It is the responsibility of the caller to call +defined_at?+ before
# calling +call+, because if +defined_at?+ is false, it is not guaranteed
# +call+ will throw an exception to indicate an error guard. If an
# exception is not thrown, evaluation may result in an arbitrary arg.
#
# The main distinction between +PartialFunction+ and +Proc+ is
# that the user of a +PartialFunction+ may choose to do something different
# with input that is declared to be outside its domain. For example:
#
# @example
# sample = 1...10
#
# is_even = Fear.case(->(arg) { arg % 2 == 0}) do |arg|
# "#{arg} is even"
# end
#
# is_odd = Fear.case(->(arg) { arg % 2 == 1}) do |arg|
# "#{arg} is odd"
# end
#
# The method or_else allows chaining another partial function to handle
# input outside the declared domain
#
# numbers = sample.map(is_even.or_else(is_odd).to_proc)
#
# @see https://github.com/scala/scala/commit/5050915eb620af3aa43d6ddaae6bbb83ad74900d
module PartialFunction
autoload :AndThen, 'fear/partial_function/and_then'
autoload :Any, 'fear/partial_function/any'
autoload :Combined, 'fear/partial_function/combined'
autoload :EMPTY, 'fear/partial_function/empty'
autoload :Guard, 'fear/partial_function/guard'
autoload :Lifted, 'fear/partial_function/lifted'
autoload :OrElse, 'fear/partial_function/or_else'

# @param condition [#call] describes the domain of partial function
# @param function [Proc] function definition
def initialize(condition, &function)
@condition = condition
@function = function
end
attr_reader :condition, :function
private :condition
private :function

# Checks if a value is contained in the function's domain.
#
# @param arg [any]
# @return [Boolean]
def defined_at?(arg)
condition === arg
end

# @!method call(arg)
# @param arg [any]
# @return [any] Calls this partial function with the given argument when it
# is contained in the function domain.
# @raise [MatchError] when this partial function is not defined.
# @abstract

# Converts this partial function to other
#
# @return [Proc]
def to_proc
proc { |arg| call(arg) }
end

# Calls this partial function with the given argument when it is contained in the function domain.
# Calls fallback function where this partial function is not defined.
#
# @param arg [any]
# @yield [arg] if partial function not defined for this +arg+
#
# @note that expression +pf.call_or_else(arg, &fallback)+ is equivalent to
# +pf.defined_at?(arg) ? pf.(arg) : fallback.(arg)+
# except that +call_or_else+ method can be implemented more efficiently to avoid calling +defined_at?+ twice.
#
def call_or_else(arg)
if defined_at?(arg)
call(arg)
else
yield arg
end
end

# Composes this partial function with a fallback partial function which
# gets applied where this partial function is not defined.
#
# @param other [PartialFunction]
# @return [PartialFunction] a partial function which has as domain the union of the domains
# of this partial function and +other+.
def or_else(other)
OrElse.new(self, other)
end

# @see or_else
def |(other)
or_else(other)
end

# Composes this partial function with a fallback partial function (or Proc) which
# gets applied where this partial function is not defined.
#
# @overload and_then(other)
# @param other [Fear::PartialFunction]
# @return [Fear::PartialFunction] a partial function with the same domain as this partial function, which maps
# argument +x+ to +other.(self.call(x))+.
# @note calling +#defined_at?+ on the resulting partial function may call the first
# partial function and execute its side effect. It is highly recommended to call +#call_or_else+
# instead of +#defined_at?+/+#call+ for efficiency.
# @overload and_then(other)
# @param other [Proc]
# @return [Fear::PartialFunction] a partial function with the same domain as this partial function, which maps
# argument +x+ to +other.(self.call(x))+.
# @overload and_then(&other)
# @param other [Proc]
# @return [Fear::PartialFunction]
#
def and_then(other = Utils::UNDEFINED, &block)
Utils.with_block_or_argument('Fear::PartialFunction#and_then', other, block) do |fun|
if fun.is_a?(Fear::PartialFunction)
Combined.new(self, fun)
else
AndThen.new(self, &fun)
end
end
end

# @see and_then
def &(other)
and_then(other)
end

# Turns this partial function in Proc-like object, returning +Option+
# @return [#call]
def lift
Lifted.new(self)
end

class << self
# Creates partial function guarded by several condition.
# All guards should match.
# @param guards [<#===, symbol>]
# @param function [Proc]
# @return [Fear::PartialFunction]
def and(*guards, &function)
PartialFunctionClass.new(Guard.and(guards), &function)
end

# Creates partial function guarded by several condition.
# Any condition should match.
# @param guards [<#===, symbol>]
# @param function [Proc]
# @return [Fear::PartialFunction]
def or(*guards, &function)
PartialFunctionClass.new(Guard.or(guards), &function)
end
end
end
end
48 changes: 48 additions & 0 deletions lib/fear/partial_function/and_then.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module Fear
module PartialFunction
# Composite function produced by +PartialFunction#and_then+ method
# @api private
class AndThen
include PartialFunction

# @param partial_function [Fear::PartialFunction]
# @param function [Proc]
def initialize(partial_function, &function)
@partial_function = partial_function
@function = function
end
# @!attribute partial_function
# @return [Fear::PartialFunction]
# @!attribute function
# @return [Proc]
attr_reader :partial_function
attr_reader :function
private :partial_function
private :function

# @param arg [any]
# @return [any ]
def call(arg)
function.call(partial_function.call(arg))
end

# @param arg [any]
# @return [Boolean]
def defined_at?(arg)
partial_function.defined_at?(arg)
end

# @param arg [any]
# @yield [arg]
# @return [any]
def call_or_else(arg)
result = partial_function.call_or_else(arg) do
return yield(arg)
end
function.call(result)
end
end

private_constant :AndThen
end
end
26 changes: 26 additions & 0 deletions lib/fear/partial_function/any.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Fear
module PartialFunction
# Any is an object which is always truthy
# @api private
class Any
class << self
# @param _other [any]
# @return [true]
def ===(_other)
true
end

# @param _other [any]
# @return [true]
def ==(_other)
true
end

# @return [Proc]
def to_proc
proc { true }
end
end
end
end
end
51 changes: 51 additions & 0 deletions lib/fear/partial_function/combined.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module Fear
module PartialFunction
# Composite function produced by +PartialFunction#and_then+ method
# @api private
class Combined
include PartialFunction

# @param f1 [Fear::PartialFunction]
# @param f2 [Fear::PartialFunction]
def initialize(f1, f2)
@f1 = f1
@f2 = f2
end
# @!attribute f1
# @return [Fear::PartialFunction]
# @!attribute f2
# @return [Fear::PartialFunction]
attr_reader :f1, :f2
private :f1
private :f2

# @param arg [any]
# @return [any ]
def call(arg)
f2.call(f1.call(arg))
end

alias === call
alias [] call

# @param arg [any]
# @yieldparam arg [any]
# @return [any]
def call_or_else(arg)
result = f1.call_or_else(arg) { return yield(arg) }
f2.call_or_else(result) { |_| return yield(arg) }
end

# @param arg [any]
# @return [Boolean]
def defined_at?(arg)
result = f1.call_or_else(arg) do
return false
end
f2.defined_at?(result)
end
end

private_constant :AndThen
end
end
6 changes: 6 additions & 0 deletions lib/fear/partial_function/empty.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Fear
module PartialFunction
EMPTY = Object.new.extend(EmptyPartialFunction)
EMPTY.freeze
end
end
90 changes: 90 additions & 0 deletions lib/fear/partial_function/guard.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module Fear
module PartialFunction
# Guard represents PartialFunction guardian
#
# @api private
class Guard
autoload :And, 'fear/partial_function/guard/and'
autoload :And3, 'fear/partial_function/guard/and3'
autoload :Or, 'fear/partial_function/guard/or'

class << self
# Optimized version for combination of two guardians
# Two guarding is a very common situation. For example checking for Some, and checking
# a value withing contianer.
#
def and2(c1, c2)
Guard::And.new(
(c1.is_a?(Symbol) ? c1.to_proc : c1),
(c2.is_a?(Symbol) ? c2.to_proc : c2),
)
end

def and3(c1, c2, c3)
Guard::And3.new(
(c1.is_a?(Symbol) ? c1.to_proc : c1),
(c2.is_a?(Symbol) ? c2.to_proc : c2),
(c3.is_a?(Symbol) ? c3.to_proc : c3),
)
end

def and1(c)
c.is_a?(Symbol) ? c.to_proc : c
end

# @param conditions [<#===, Symbol>]
# @return [Fear::PartialFunction::Guard]
def and(conditions)
case conditions.size
when 1 then and1(*conditions)
when 2 then and2(*conditions)
when 3 then and3(*conditions)
when 0 then Any
else
head, *tail = conditions
tail.inject(new(head)) { |acc, condition| acc.and(new(condition)) }
end
end

# @param conditions [<#===, Symbol>]
# @return [Fear::PartialFunction::Guard]
def or(conditions)
return Any if conditions.empty?

head, *tail = conditions
tail.inject(new(head)) { |acc, condition| acc.or(new(condition)) }
end
end

# @param condition [<#===, Symbol>]
def initialize(condition)
@condition =
if condition.is_a?(Symbol)
condition.to_proc
else
condition
end
end
attr_reader :condition
private :condition

# @param other [Fear::PartialFunction::Guard]
# @return [Fear::PartialFunction::Guard]
def and(other)
Guard::And.new(condition, other)
end

# @param other [Fear::PartialFunction::Guard]
# @return [Fear::PartialFunction::Guard]
def or(other)
Guard::Or.new(condition, other)
end

# @param arg [any]
# @return [Boolean]
def ===(arg)
condition === arg
end
end
end
end
36 changes: 36 additions & 0 deletions lib/fear/partial_function/guard/and.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Fear
module PartialFunction
class Guard
# @api private
class And < Guard
# @param c1 [Fear::PartialFunction::Guard]
# @param c2 [Fear::PartialFunction::Guard]
def initialize(c1, c2)
@c1 = c1
@c2 = c2
end
attr_reader :c1, :c2
private :c1
private :c2

# @param other [Fear::PartialFunction::Guard]
# @return [Fear::PartialFunction::Guard]
def and(other)
Guard::And.new(self, other)
end

# @param other [Fear::PartialFunction::Guard]
# @return [Fear::PartialFunction::Guard]
def or(other)
Guard::Or.new(self, other)
end

# @param arg [any]
# @return [Boolean]
def ===(arg)
(c1 === arg) && (c2 === arg)
end
end
end
end
end
39 changes: 39 additions & 0 deletions lib/fear/partial_function/guard/and3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module Fear
module PartialFunction
class Guard
# @api private
class And3 < Guard
# @param c1 [#===]
# @param c2 [#===]
# @param c3 [#===]
def initialize(c1, c2, c3)
@c1 = c1
@c2 = c2
@c3 = c3
end
attr_reader :c1, :c2, :c3
private :c1
private :c2
private :c3

# @param other [Fear::PartialFunction::Guard]
# @return [Fear::PartialFunction::Guard]
def and(other)
Guard::And.new(self, other)
end

# @param other [Fear::PartialFunction::Guard]
# @return [Fear::PartialFunction::Guard]
def or(other)
Guard::Or.new(self, other)
end

# @param arg [any]
# @return [Boolean]
def ===(arg)
(c1 === arg) && (c2 === arg) && (c3 === arg)
end
end
end
end
end
36 changes: 36 additions & 0 deletions lib/fear/partial_function/guard/or.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Fear
module PartialFunction
class Guard
# @api private
class Or < Guard
# @param c1 [Fear::PartialFunction::Guard]
# @param c2 [Fear::PartialFunction::Guard]
def initialize(c1, c2)
@c1 = c1
@c2 = c2
end
attr_reader :c1, :c2
private :c1
private :c2

# @param other [Fear::PartialFunction::Guard]
# @return [Fear::PartialFunction::Guard]
def and(other)
Guard::And.new(self, other)
end

# @param other [Fear::PartialFunction::Guard]
# @return [Fear::PartialFunction::Guard]
def or(other)
Guard::Or.new(self, other)
end

# @param arg [any]
# @return [Boolean]
def ===(arg)
(c1 === arg) || (c2 === arg)
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/fear/partial_function/lifted.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Fear
module PartialFunction
# @api private
class Lifted
# @param pf [Fear::PartialFunction]
def initialize(pf)
@pf = pf
end
attr_reader :pf

# @param arg [any]
# @return [Fear::Option]
def call(arg)
Some.new(pf.call_or_else(arg) { return Fear::None })
end
end

private_constant :Lifted
end
end
62 changes: 62 additions & 0 deletions lib/fear/partial_function/or_else.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Fear
module PartialFunction
# Composite function produced by +PartialFunction#or_else+ method
# @api private
class OrElse
include PartialFunction

# @param f1 [Fear::PartialFunction]
# @param f2 [Fear::PartialFunction]
def initialize(f1, f2)
@f1 = f1
@f2 = f2
end
# @!attribute f1
# @return [Fear::PartialFunction]
# @!attribute f2
# @return [Fear::PartialFunction]
attr_reader :f1, :f2
private :f1
private :f2

# @param arg [any]
# @return [any]
def call(arg)
f1.call_or_else(arg, &f2)
end

alias === call
alias [] call

# @param other [Fear::PartialFunction]
# @return [Fear::PartialFunction]
def or_else(other)
OrElse.new(f1, f2.or_else(other))
end

# @see Fear::PartialFunction#and_then
def and_then(other = Utils::UNDEFINED, &block)
Utils.with_block_or_argument('Fear::PartialFunction::OrElse#and_then', other, block) do |fun|
OrElse.new(f1.and_then(&fun), f2.and_then(&fun))
end
end

# @param arg [any]
# @return [Boolean]
def defined_at?(arg)
f1.defined_at?(arg) || f2.defined_at?(arg)
end

# @param arg [any]
# @param fallback [Proc]
# @return [any]
def call_or_else(arg, &fallback)
f1.call_or_else(arg) do
return f2.call_or_else(arg, &fallback)
end
end
end

private_constant :OrElse
end
end
26 changes: 26 additions & 0 deletions lib/fear/partial_function_class.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Fear
# @api private
class PartialFunctionClass
include PartialFunction

# @param arg [any]
# @return [any] Calls this partial function with the given argument when it
# is contained in the function domain.
# @raise [MatchError] when this partial function is not defined.
def call(arg)
call_or_else(arg, &PartialFunction::EMPTY)
end

# @param arg [any]
# @yield [arg] if function not defined
def call_or_else(arg)
if defined_at?(arg)
function.call(arg)
else
yield arg
end
end
end

private_constant :PartialFunctionClass
end
102 changes: 102 additions & 0 deletions lib/fear/pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
module Fear
# Pattern match builder. Provides DSL for building pattern matcher
# Pattern match is just a combination of partial functions
#
# matcher = Fear.matcher do |m|
# m.case(Integer) { |x| x * 2 }
# m.case(String) { |x| x.to_i(10) * 2 }
# end
# matcher.is_a?(Fear::PartialFunction) #=> true
# matcher.defined_at?(4) #=> true
# matcher.defined_at?('4') #=> true
# matcher.defined_at?(nil) #=> false
#
# The previous example is the same as:
#
# Fear.case(Integer) { |x| x * ) }
# .or_else(
# Fear.case(String) { |x| x.to_i(10) * 2 }
# )
#
# You can provide +else+ branch, so partial function will be defined
# on any input:
#
# matcher = Fear.matcher do |m|
# m.else { 'Match' }
# end
# matcher.call(42) #=> 'Match'
#
# @see Fear.matcher public interface for building matchers
# @api Fear
# @note Use this class only to build custom pattern match classes. See +Fear::OptionPatternMatch+ as an example.
class PatternMatch
class << self
alias __new__ new

# @return [Fear::PartialFunction]
def new
builder = __new__(PartialFunction::EMPTY)
yield builder
builder.result
end

# Creates anonymous module to add `#mathing` behaviour to a class
#
# @example
# class User
# include Fear::PatternMatch.mixin
# end
#
# user.match do |m\
# m.case(:admin?) { |u| ... }
# m.else { |u| ... }
# end
#
# @param as [Symbol, String] (:match) method name
# @return [Module]
def mixin(as: :match)
matcher_class = self

Module.new do
define_method(as) do |&matchers|
matcher_class.new(&matchers).call(self)
end
end
end
end

# @param result [Fear::EmptyPartialFunction]
def initialize(result)
@result = result
end
attr_accessor :result
private :result=

# @see Fear::PartialFunction#else
def else(&effect)
or_else(Fear.case(&effect))
end

# This method is syntactic sugar for `PartialFunction#or_else`, but rather than passing
# another partial function as an argument, you pass arguments to build such partial function.
# @example This two examples produces the same result
# other = Fear.case(Integer) { |x| x * 2 }
# this.or_else(other)
#
# this.case(Integer) { |x| x * 2 }
#
# @param guards [<#===>]
# @param effect [Proc]
# @return [Fear::PartialFunction]
# @see #or_else for details
def case(*guards, &effect)
or_else(Fear.case(*guards, &effect))
end

# @see Fear::PartialFunction#or_else
def or_else(other)
self.result = result.or_else(other)
self
end
end
end
110 changes: 110 additions & 0 deletions lib/fear/pattern_matching_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
module Fear
# @api private
module PatternMatchingApi
# Creates pattern match. Use `case` method to
# define matching branches. Branch consist of a
# guardian, which describes domain of the
# branch and function to apply to matching value.
#
# @example This mather apply different functions to Integers and to Strings
# matcher = Fear.matcher do |m|
# m.case(Integer) { |n| "#{n} is a number" }
# m.case(String) { |n| "#{n} is a string" }
# end
#
# matcher.(42) #=> "42 is a number"
# matcher.("Foo") #=> "Foo is a string"
#
# if you pass something other than Integer or string, it will raise `Fear::MatchError`:
#
# matcher.(10..20) #=> raises Fear::MatchError
#
# to avoid raising `MatchError`, you can use `else` method. It defines a branch matching
# on any value.
#
# matcher = Fear.matcher do |m|
# m.case(Integer) { |n| "#{n} is a number" }
# m.case(String) { |n| "#{n} is a string" }
# m.else { |n| "#{n} is a #{n.class}" }
# end
#
# matcher.(10..20) #=> "10..20 is a Range"
#
# You can use anything as a guardian if it responds to `#===` method:
#
# m.case(20..40) { |m| "#{m} is within range" }
# m.case(->(x) { x > 10}) { |m| "#{m} is greater than 10" }
#
# If you pass a Symbol, it will be converted to proc using +#to_proc+ method
#
# m.case(:even?) { |x| "#{x} is even" }
# m.case(:odd?) { |x| "#{x} is odd" }
#
# It's also possible to pass several guardians. All should match to pass
#
# m.case(Integer, :even?) { |x| ... }
# m.case(Integer, :odd?) { |x| ... }
#
# Since matcher returns +Fear::PartialFunction+, you can combine matchers using
# partial function API:
#
# failures = Fear.matcher do |m|
# m.case('not_found') { ... }
# m.case('network_error') { ... }
# end
#
# success = Fear.matcher do |m|
# m.case('ok') { ... }
# end
#
# response = failures.or_else(success)
#
# @yieldparam matcher [Fear::PartialFunction]
# @return [Fear::PartialFunction]
# @see Fear::OptionPatternMatch for example of custom matcher
def matcher(&block)
PatternMatch.new(&block)
end

# Pattern match against given value
#
# @example
# Fear.match(42) do |m|
# m.case(Integer, :even?) { |n| "#{n} is even number" }
# m.case(Integer, :odd?) { |n| "#{n} is odd number" }
# m.case(Strings) { |n| "#{n} is a string" }
# m.else { 'unknown' }
# end #=> "42 is even number"
#
# @param value [any]
# @yieldparam matcher [Fear::PartialFunction]
# @return [any]
def match(value, &block)
matcher(&block).call(value)
end

# Creates partial function defined on domain described with guards
#
# @example
# pf = Fear.case(Integer) { |x| x / 2 }
# pf.defined_at?(4) #=> true
# pf.defined_at?('Foo') #=> false
#
# @example multiple guards combined using logical "and"
# pf = Fear.case(Integer, :even?) { |x| x / 2 }
# pf.defined_at?(4) #=> true
# pf.defined_at?(3) #=> false
#
# @note to make more complex matches, you are encouraged to use Qo gem.
# @see Qo https://github.com/baweaver/qo
# @example
# Fear.case(Qo[age: 20..30]) { |_| 'old enough' }
#
# @param guards [<#===, symbol>]
# @param function [Proc]
# @return [Fear::PartialFunction]
def case(*guards, &function)
PartialFunction.and(*guards, &function)
end
end
end
6 changes: 6 additions & 0 deletions lib/fear/right.rb
Original file line number Diff line number Diff line change
@@ -2,6 +2,12 @@ module Fear
class Right
include Either
include RightBiased::Right
include RightPatternMatch.mixin

# @api private
def right_value
value
end

# @return [true]
def right?
9 changes: 9 additions & 0 deletions lib/fear/right_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Fear
# @api private
class RightPatternMatch < EitherPatternMatch
def left(*)
self
end
alias failure left
end
end
3 changes: 3 additions & 0 deletions lib/fear/some.rb
Original file line number Diff line number Diff line change
@@ -3,10 +3,13 @@ class Some
include Option
include Dry::Equalizer(:get)
include RightBiased::Right
include SomePatternMatch.mixin

attr_reader :value
protected :value

# FIXME: nice inspect

def initialize(value)
@value = value
end
11 changes: 11 additions & 0 deletions lib/fear/some_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module Fear
# @api private
class SomePatternMatch < OptionPatternMatch
# @return [Fear::OptionPatternMatch]
def none
self
end
end

private_constant :SomePatternMatch
end
1 change: 1 addition & 0 deletions lib/fear/success.rb
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ class Success
include Try
include Dry::Equalizer(:value)
include RightBiased::Right
include SuccessPatternMatch.mixin

attr_reader :value
protected :value
10 changes: 10 additions & 0 deletions lib/fear/success_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Fear
# @api private
class SuccessPatternMatch < Fear::TryPatternMatch
# @param conditions [<#==>]
# @return [Fear::TryPatternMatch]
def failure(*_conditions)
self
end
end
end
58 changes: 53 additions & 5 deletions lib/fear/try.rb
Original file line number Diff line number Diff line change
@@ -15,11 +15,19 @@ module Fear
# divisor = Try { Integer(params[:divisor]) }
# problem = dividend.flat_map { |x| divisor.map { |y| x / y } }
#
# if problem.success?
# puts "Result of #{dividend.get} / #{divisor.get} is: #{problem.get}"
# else
# puts "You must've divided by zero or entered something wrong. Try again"
# puts "Info from the exception: #{problem.exception.message}"
# problem.match |m|
# m.success do |result|
# puts "Result of #{dividend.get} / #{divisor.get} is: #{result}"
# end
#
# m.failure(ZeroDivisionError) do
# puts "Division by zero is not allowed"
# end
#
# m.failure do |exception|
# puts "You entered something wrong. Try again"
# puts "Info from the exception: #{exception.message}"
# end
# end
#
# An important property of +Try+ shown in the above example is its
@@ -210,6 +218,23 @@ module Fear
# Success(42).to_either #=> Right(42)
# Failure(ArgumentError.new).to_either #=> Left(ArgumentError.new)
#
# @!method match(&matcher)
# Pattern match against this +Try+
# @yield matcher [Fear::TryPatternMatch]
# @example
# Try { ... }.match do |m|
# m.success(Integer) do |x|
# x * 2
# end
#
# m.success(String) do |x|
# x.to_i * 2
# end
#
# m.failure(ZeroDivisionError) { 'not allowed to divide by 0' }
# m.else { 'something unexpected' }
# end
#
# @author based on Twitter's original implementation.
# @see https://github.com/scala/scala/blob/2.11.x/src/library/scala/util/Try.scala
#
@@ -224,6 +249,29 @@ def right_class
Success
end

class << self
# Build pattern matcher to be used later, despite off
# +Try#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 =
# Try.matcher do |m|
# m.success(Integer, ->(x) { x > 2 }) { |x| x * 2 }
# m.success(String) { |x| x.to_i * 2 }
# m.failure(ActiveRecord::RecordNotFound) { :err }
# m.else { 'error '}
# end
# matcher.call(try)
#
# @yieldparam [Fear::TryPatternMatch]
# @return [Fear::PartialFunction]
def matcher(&matcher)
TryPatternMatch.new(&matcher)
end
end

# Include this mixin to access convenient factory methods.
# @example
# include Fear::Try::Mixin
28 changes: 28 additions & 0 deletions lib/fear/try_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Fear
# Try pattern matcher
#
# @note it has two optimized subclasses +Fear::SuccessPatternMatch+ and +Fear::FailurePatternMatch+
# @api private
class TryPatternMatch < Fear::PatternMatch
SUCCESS_EXTRACTOR = :get.to_proc
FAILURE_EXTRACTOR = :exception.to_proc

# Match against +Fear::Success+
#
# @param conditions [<#==>]
# @return [Fear::TryPatternMatch]
def success(*conditions, &effect)
branch = Fear.case(Fear::Success, &SUCCESS_EXTRACTOR).and_then(Fear.case(*conditions, &effect))
or_else(branch)
end

# Match against +Fear::Failure+
#
# @param conditions [<#==>]
# @return [Fear::TryPatternMatch]
def failure(*conditions, &effect)
branch = Fear.case(Fear::Failure, &FAILURE_EXTRACTOR).and_then(Fear.case(*conditions, &effect))
or_else(branch)
end
end
end
10 changes: 10 additions & 0 deletions lib/fear/utils.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
module Fear
# @private
module Utils
UNDEFINED = Object.new.freeze

class << self
def assert_arg_or_block!(method_name, *args)
unless block_given? ^ !args.empty?
raise ArgumentError, "##{method_name} accepts either one argument or block"
end
end

def with_block_or_argument(method_name, arg = UNDEFINED, block = nil)
if block.nil? ^ arg.equal?(UNDEFINED)
yield(block || arg)
else
raise ArgumentError, "#{method_name} accepts either block or partial function"
end
end

def assert_type!(value, *types)
if types.none? { |type| value.is_a?(type) }
raise TypeError, "expected `#{value.inspect}` to be of #{types.join(', ')} class"
37 changes: 37 additions & 0 deletions spec/fear/either_pattern_match_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
RSpec.describe Fear::EitherPatternMatch do
include Fear::Either::Mixin

context 'Right' do
let(:matcher) do
described_class.new do |m|
m.right(:even?) { |x| "#{x} is even" }
m.right(:odd?) { |x| "#{x} is odd" }
end
end

it do
expect(matcher.call(Right(4))).to eq('4 is even')
expect(matcher.call(Right(3))).to eq('3 is odd')
expect do
matcher.call(Left(44))
end.to raise_error(Fear::MatchError)
end
end

context 'Left' do
let(:matcher) do
described_class.new do |m|
m.left(:even?) { |x| "#{x} is even" }
m.left(:odd?) { |x| "#{x} is odd" }
end
end

it do
expect(matcher.call(Left(4))).to eq('4 is even')
expect(matcher.call(Left(3))).to eq('3 is odd')
expect do
matcher.call(Right(44))
end.to raise_error(Fear::MatchError)
end
end
end
38 changes: 38 additions & 0 deletions spec/fear/failure_spec.rb
Original file line number Diff line number Diff line change
@@ -115,4 +115,42 @@
it { is_expected.to eq(false) }
end
end

describe '#match' do
context 'matched' do
subject do
failure.match do |m|
m.failure(->(x) { x.message.length < 2 }) { |x| "Error: #{x}" }
m.failure(->(x) { x.message.length > 2 }) { |x| "Error: #{x}" }
m.success(->(x) { x.length > 2 }) { |x| "Success: #{x}" }
end
end

it { is_expected.to eq('Error: error') }
end

context 'nothing matched and no else given' do
subject do
proc do
failure.match do |m|
m.failure(->(x) { x.message.length < 2 }) { |x| "Error: #{x}" }
m.success { 'noop' }
end
end
end

it { is_expected.to raise_error(Fear::MatchError) }
end

context 'nothing matched and else given' do
subject do
failure.match do |m|
m.failure(->(x) { x.message.length < 2 }) { |x| "Error: #{x}" }
m.else { :default }
end
end

it { is_expected.to eq(:default) }
end
end
end
101 changes: 101 additions & 0 deletions spec/fear/guard_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
RSpec.describe Fear::PartialFunction::Guard do
context 'Class' do
context 'match' do
subject { Fear::PartialFunction::Guard.new(Integer) === 4 }

it { is_expected.to eq(true) }
end

context 'not match' do
subject { Fear::PartialFunction::Guard.new(Integer) === '4' }

it { is_expected.to eq(false) }
end
end

context 'Symbol' do
context 'match' do
subject { Fear::PartialFunction::Guard.new(:even?) === 4 }

it { is_expected.to eq(true) }
end

context 'not match' do
subject { Fear::PartialFunction::Guard.new(:even?) === 3 }

it { is_expected.to eq(false) }
end
end

context 'Proc' do
context 'match' do
subject { Fear::PartialFunction::Guard.new(->(x) { x.even? }) === 4 }

it { is_expected.to eq(true) }
end

context 'not match' do
subject { Fear::PartialFunction::Guard.new(->(x) { x.even? }) === 3 }

it { is_expected.to eq(false) }
end
end

describe '.and' do
context 'match' do
subject { guard === 4 }
let(:guard) { Fear::PartialFunction::Guard.and([Integer, :even?, ->(x) { x.even? }]) }

it { is_expected.to eq(true) }
end

context 'not match' do
subject { guard === 3 }
let(:guard) { Fear::PartialFunction::Guard.and([Integer, :even?, ->(x) { x.even? }]) }

it { is_expected.to eq(false) }
end

context 'empty array' do
subject { guard === 4 }
let(:guard) { Fear::PartialFunction::Guard.and([]) }

it 'matches any values' do
is_expected.to eq(true)
end
end

context 'short circuit' do
let(:guard) { Fear::PartialFunction::Guard.and([first, second, third]) }
let(:first) { ->(_) { false } }
let(:second) { ->(_) { raise } }
let(:third) { ->(_) { raise } }

it 'does not call the second and the third' do
expect { guard === 4 }.not_to raise_error
end
end
end

describe '.or' do
let(:guard) { Fear::PartialFunction::Guard.or(['F', Integer]) }

context 'match second' do
subject { guard === 4 }

it { is_expected.to eq(true) }
end

context 'match first' do
subject { guard === 'F' }

it { is_expected.to eq(true) }
end

context 'not match' do
subject { guard === 'A&' }

it { is_expected.to eq(false) }
end
end
end
38 changes: 38 additions & 0 deletions spec/fear/left_spec.rb
Original file line number Diff line number Diff line change
@@ -141,4 +141,42 @@
it { is_expected.to eq(false) }
end
end

describe '#match' do
context 'matched' do
subject do
left.match do |m|
m.left(->(x) { x.length < 2 }) { |x| "Left: #{x}" }
m.left(->(x) { x.length > 2 }) { |x| "Left: #{x}" }
m.right(->(x) { x.length > 2 }) { |x| "Right: #{x}" }
end
end

it { is_expected.to eq('Left: value') }
end

context 'nothing matched and no else given' do
subject do
proc do
left.match do |m|
m.left(->(x) { x.length < 2 }) { |x| "Left: #{x}" }
m.right { |_| 'noop' }
end
end
end

it { is_expected.to raise_error(Fear::MatchError) }
end

context 'nothing matched and else given' do
subject do
left.match do |m|
m.left(->(x) { x.length < 2 }) { |x| "Left: #{x}" }
m.else { :default }
end
end

it { is_expected.to eq(:default) }
end
end
end
56 changes: 56 additions & 0 deletions spec/fear/none_spec.rb
Original file line number Diff line number Diff line change
@@ -70,4 +70,60 @@

it { is_expected.to eq('Fear::None') }
end

describe '#===' do
context 'None' do
subject { Fear::None === none }

it { is_expected.to eq(true) }
end

context 'Fear::Some' do
subject { Fear::None === Some(4) }

it { is_expected.to eq(false) }
end

context 'Integer' do
subject { Fear::None === 4 }

it { is_expected.to eq(false) }
end
end

describe '#match' do
context 'matched' do
subject do
none.match do |m|
m.some { |x| x * 2 }
m.none { 'noop' }
end
end

it { is_expected.to eq('noop') }
end

context 'nothing matched and no else given' do
subject do
proc do
none.match do |m|
m.some { |x| x * 2 }
end
end
end

it { is_expected.to raise_error(Fear::MatchError) }
end

context 'nothing matched and else given' do
subject do
none.match do |m|
m.some { |x| x * 2 }
m.else { :default }
end
end

it { is_expected.to eq(:default) }
end
end
end
35 changes: 35 additions & 0 deletions spec/fear/option_pattern_match_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
RSpec.describe Fear::OptionPatternMatch do
include Fear::Option::Mixin

context 'Some' do
let(:matcher) do
described_class.new do |m|
m.some(:even?) { |x| "#{x} is even" }
m.some(:odd?) { |x| "#{x} is odd" }
end
end

it do
expect(matcher.call(Some(4))).to eq('4 is even')
expect(matcher.call(Some(3))).to eq('3 is odd')
expect do
matcher.call(None())
end.to raise_error(Fear::MatchError)
end
end

context 'None' do
let(:matcher) do
described_class.new do |m|
m.none { 'nil' }
end
end

it do
expect(matcher.call(None())).to eq('nil')
expect do
matcher.call(Some(3))
end.to raise_error(Fear::MatchError)
end
end
end
36 changes: 36 additions & 0 deletions spec/fear/partial_function/empty_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
RSpec.describe Fear::PartialFunction::EMPTY do
describe '#defined?' do
subject { described_class.defined_at?(42) }

it { is_expected.to be(false) }
end

describe '#call' do
subject { -> { described_class.call(42) } }

it { is_expected.to raise_error(Fear::MatchError, 'partial function not defined at: 42') }
end

describe '#call_or_else' do
subject { described_class.call_or_else(42, &default) }
let(:default) { ->(x) { "default: #{x}" } }

it { is_expected.to eq('default: 42') }
end

describe '#and_then' do
subject { described_class.and_then { |_x| 'then' } }

it { is_expected.to eq(described_class) }
end

describe '#or_else' do
subject { described_class.or_else(other) }

let(:other) { Fear.case(proc { true }) { 'other' } }

it { is_expected.to eq(other) }
end

it { is_expected.to be_kind_of(Fear::PartialFunction) }
end
145 changes: 145 additions & 0 deletions spec/fear/partial_function_and_then_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
RSpec.describe Fear::PartialFunction, '#and_then' do
context 'proc' do
subject(:pf_and_f) { partial_function.and_then(&function) }

let(:partial_function) { Fear.case(->(x) { x.even? }) { |x| "pf: #{x}" } }
let(:function) { ->(x) { "f: #{x}" } }

describe '#defined_at?' do
context 'defined' do
subject { pf_and_f.defined_at?(4) }

it { is_expected.to eq(true) }
end

context 'not defined' do
subject { pf_and_f.defined_at?(3) }

it { is_expected.to eq(false) }
end
end

describe '#call' do
context 'defined' do
subject { pf_and_f.call(4) }

it { is_expected.to eq('f: pf: 4') }
end

context 'not defined' do
subject { -> { pf_and_f.call(3) } }

it { is_expected.to raise_error(Fear::MatchError, 'partial function not defined at: 3') }
end
end

describe '#call_or_else' do
let(:fallback) { ->(x) { "fallback: #{x}" } }

context 'defined' do
subject { pf_and_f.call_or_else(4, &fallback) }

it { is_expected.to eq('f: pf: 4') }
end

context 'not defined' do
subject { pf_and_f.call_or_else(3, &fallback) }

it { is_expected.to eq('fallback: 3') }
end
end
end

context 'partial function' do
subject(:first_and_then_second) { first.and_then(second) }

describe '#defined_at?' do
context 'first defined, second defined on result of first' do
subject { first_and_then_second.defined_at?(6) }

let(:first) { Fear.case(->(x) { x.even? }) { |x| x / 2 } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| x / 3 } }

it { is_expected.to eq(true) }
end

context 'first defined, second not defined on result of first' do
subject { first_and_then_second.defined_at?(4) }

let(:first) { Fear.case(->(x) { x.even? }) { |x| x / 2 } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| x / 3 } }

it { is_expected.to eq(false) }
end

context 'first not defined' do
subject { first_and_then_second.defined_at?(3) }

let(:first) { Fear.case(->(x) { x.even? }) { |x| "first: #{x}" } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| "second: #{x}" } }

it { is_expected.to eq(false) }
end
end

describe '#call' do
context 'first defined, second defined on result of first' do
subject { first_and_then_second.call(6) }

let(:first) { Fear.case(->(x) { x.even? }) { |x| x / 2 } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| x / 3 } }

it { is_expected.to eq(1) }
end

context 'first defined, second not defined on result of first' do
subject { -> { first_and_then_second.call(4) } }

let(:first) { Fear.case(->(x) { x.even? }) { |x| x / 2 } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| x / 3 } }

it { is_expected.to raise_error(Fear::MatchError, 'partial function not defined at: 2') }
end

context 'first not defined' do
subject { -> { first_and_then_second.call(3) } }

let(:first) { Fear.case(->(x) { x.even? }) { |x| "first: #{x}" } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| "second: #{x}" } }

it { is_expected.to raise_error(Fear::MatchError, 'partial function not defined at: 3') }
end
end

describe '#call_or_else' do
let(:fallback) { ->(x) { "fallback: #{x}" } }

context 'first defined, second defined on result of first' do
subject { first_and_then_second.call_or_else(6, &fallback) }

let(:first) { Fear.case(->(x) { x.even? }) { |x| x / 2 } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| x / 3 } }

it { is_expected.to eq(1) }
end

context 'first defined, second not defined on result of first' do
subject { first_and_then_second.call_or_else(4, &fallback) }

let(:first) { Fear.case(->(x) { x.even? }) { |x| x / 2 } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| x / 3 } }

it { is_expected.to eq('fallback: 4') }
end

context 'first not defined' do
subject { first_and_then_second.call_or_else(3, &fallback) }

let(:first) { Fear.case(->(x) { x.even? }) { |x| "first: #{x}" } }
let(:second) { Fear.case { |x| "second: #{x}" } }

it { is_expected.to eq('fallback: 3') }
end
end
end
end
80 changes: 80 additions & 0 deletions spec/fear/partial_function_composition_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# This file contains tests from
# https://github.com/scala/scala/blob/2.13.x/test/junit/scala/PartialFunctionCompositionTest.scala
RSpec.describe Fear::PartialFunction do
let(:fallback_fun) { ->(_) { 'fallback' } }
let(:pass_all) { Fear.case(proc { true }) { |x| x } }
let(:pass_short) { Fear.case(->(x) { x.length < 5 }) { |x| x } }
let(:pass_pass) { Fear.case(->(x) { x.include?('pass') }) { |x| x } }

let(:all_and_then_short) { pass_all & pass_short }
let(:short_and_then_all) { pass_short & pass_all }
let(:all_and_then_pass) { pass_all & pass_pass }
let(:pass_and_then_all) { pass_pass & pass_all }
let(:pass_and_then_short) { pass_pass & pass_short }
let(:short_and_then_pass) { pass_short & pass_pass }

it '#and_then' do
expect(all_and_then_short.call_or_else('pass', &fallback_fun)).to eq('pass')
expect(short_and_then_all.call_or_else('pass', &fallback_fun)).to eq('pass')
expect(all_and_then_short.defined_at?('pass')).to eq(true)
expect(short_and_then_all.defined_at?('pass')).to eq(true)

expect(all_and_then_pass.call_or_else('pass', &fallback_fun)).to eq('pass')
expect(pass_and_then_all.call_or_else('pass', &fallback_fun)).to eq('pass')
expect(all_and_then_pass.defined_at?('pass')).to eq(true)
expect(pass_and_then_all.defined_at?('pass')).to eq(true)

expect(all_and_then_pass.call_or_else('longpass', &fallback_fun)).to eq('longpass')
expect(pass_and_then_all.call_or_else('longpass', &fallback_fun)).to eq('longpass')
expect(all_and_then_pass.defined_at?('longpass')).to eq(true)
expect(pass_and_then_all.defined_at?('longpass')).to eq(true)

expect(all_and_then_short.call_or_else('longpass', &fallback_fun)).to eq('fallback')
expect(short_and_then_all.call_or_else('longpass', &fallback_fun)).to eq('fallback')
expect(all_and_then_short.defined_at?('longpass')).to eq(false)
expect(short_and_then_all.defined_at?('longpass')).to eq(false)

expect(all_and_then_pass.call_or_else('longstr', &fallback_fun)).to eq('fallback')
expect(pass_and_then_all.call_or_else('longstr', &fallback_fun)).to eq('fallback')
expect(all_and_then_pass.defined_at?('longstr')).to eq(false)
expect(pass_and_then_all.defined_at?('longstr')).to eq(false)

expect(pass_and_then_short.call_or_else('pass', &fallback_fun)).to eq('pass')
expect(short_and_then_pass.call_or_else('pass', &fallback_fun)).to eq('pass')
expect(pass_and_then_short.defined_at?('pass')).to eq(true)
expect(short_and_then_pass.defined_at?('pass')).to eq(true)

expect(pass_and_then_short.call_or_else('longpass', &fallback_fun)).to eq('fallback')
expect(short_and_then_pass.call_or_else('longpass', &fallback_fun)).to eq('fallback')
expect(pass_and_then_short.defined_at?('longpass')).to eq(false)
expect(short_and_then_pass.defined_at?('longpass')).to eq(false)

expect(short_and_then_pass.call_or_else('longstr', &fallback_fun)).to eq('fallback')
expect(pass_and_then_short.call_or_else('longstr', &fallback_fun)).to eq('fallback')
expect(short_and_then_pass.defined_at?('longstr')).to eq(false)
expect(pass_and_then_short.defined_at?('longstr')).to eq(false)
end

it 'two branches' do
first_branch = Fear.case(Integer, &:itself).and_then(Fear.case(1) { 'one' })
second_branch = Fear.case(String, &:itself).and_then(
(Fear.case('zero') { 0 }).or_else(Fear.case('one') { 1 }),
)

full = first_branch.or_else(second_branch)
expect(full.call(1)).to eq('one')
expect(full.call('zero')).to eq(0)
expect(full.call('one')).to eq(1)
end

it 'or else anh then' do
f1 = Fear.case(->(x) { x < 5 }) { 1 }
f2 = Fear.case(->(x) { x > 10 }) { 10 }
f3 = Fear.case { |x| x * 2 }

f5 = f1.and_then(f3).or_else(f2)

expect(f5.call(11)).to eq(10)
expect(f5.call(3)).to eq(2)
end
end
274 changes: 274 additions & 0 deletions spec/fear/partial_function_or_else_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
RSpec.describe Fear::PartialFunction, '#or_else' do
subject(:first_or_else_second) { first.or_else(second) }

let(:first) { Fear.case(->(x) { x.even? }) { |x| "first: #{x}" } }
let(:second) { Fear.case(->(x) { x % 3 == 0 }) { |x| "second: #{x}" } }

describe '#defined_at?' do
context 'first defined, second not' do
subject { first_or_else_second.defined_at?(4) }

it { is_expected.to eq(true) }
end

context 'first not defined, second defined' do
subject { first_or_else_second.defined_at?(9) }

it { is_expected.to eq(true) }
end

context 'first not defined, second not defined' do
subject { first_or_else_second.defined_at?(5) }

it { is_expected.to eq(false) }
end

context 'first and second defined' do
subject { first_or_else_second.defined_at?(6) }

it { is_expected.to eq(true) }
end
end

describe '#call' do
context 'first defined, second not' do
subject { first_or_else_second.call(4) }

it { is_expected.to eq('first: 4') }
end

context 'first not defined, second defined' do
subject { first_or_else_second.call(9) }

it { is_expected.to eq('second: 9') }
end

context 'first not defined, second not defined' do
subject { -> { first_or_else_second.call(5) } }

it { is_expected.to raise_error(Fear::MatchError, 'partial function not defined at: 5') }
end

context 'first and second defined' do
subject { first_or_else_second.call(6) }

it { is_expected.to eq('first: 6') }
end
end

describe '#call_or_else' do
let(:fallback) { ->(x) { "fallback: #{x}" } }

context 'first defined, second not' do
subject { first_or_else_second.call_or_else(4, &fallback) }

it { is_expected.to eq('first: 4') }
end

context 'first not defined, second defined' do
subject { first_or_else_second.call_or_else(9, &fallback) }

it { is_expected.to eq('second: 9') }
end

context 'first not defined, second not defined' do
subject { first_or_else_second.call_or_else(5, &fallback) }

it { is_expected.to eq('fallback: 5') }
end

context 'first and second defined' do
subject { first_or_else_second.call_or_else(6, &fallback) }

it { is_expected.to eq('first: 6') }
end
end

describe '#or_else' do
let(:first_or_else_second_or_else_third) { first_or_else_second.or_else(third) }
let(:third) { Fear.case(->(x) { x % 7 == 0 }) { |x| "third: #{x}" } }

describe '#defined_at?' do
context 'first defined, second not' do
subject { first_or_else_second_or_else_third.defined_at?(4) }

it { is_expected.to eq(true) }
end

context 'first not defined, second defined' do
subject { first_or_else_second_or_else_third.defined_at?(9) }

it { is_expected.to eq(true) }
end

context 'first not defined, second not defined, third defined' do
subject { first_or_else_second_or_else_third.defined_at?(7) }

it { is_expected.to eq(true) }
end

context 'first not defined, second not defined, third not defined' do
subject { first_or_else_second_or_else_third.defined_at?(1) }

it { is_expected.to eq(false) }
end

context 'first, second and third defined' do
subject { first_or_else_second.defined_at?(42) }

it { is_expected.to eq(true) }
end
end

describe '#call' do
context 'first defined, second not' do
subject { first_or_else_second_or_else_third.call(4) }

it { is_expected.to eq('first: 4') }
end

context 'first not defined, second defined' do
subject { first_or_else_second_or_else_third.call(9) }

it { is_expected.to eq('second: 9') }
end

context 'first not defined, second not defined, third defined' do
subject { first_or_else_second_or_else_third.call(7) }

it { is_expected.to eq('third: 7') }
end

context 'first not defined, second not defined, third not defined' do
subject { -> { first_or_else_second_or_else_third.call(1) } }

it { is_expected.to raise_error(Fear::MatchError, 'partial function not defined at: 1') }
end

context 'first, second and third defined' do
subject { first_or_else_second.call(42) }

it { is_expected.to eq('first: 42') }
end
end

describe '#call_or_else' do
let(:fallback) { ->(x) { "fallback: #{x}" } }

context 'first defined, second not' do
subject { first_or_else_second_or_else_third.call_or_else(4, &fallback) }

it { is_expected.to eq('first: 4') }
end

context 'first not defined, second defined' do
subject { first_or_else_second_or_else_third.call_or_else(9, &fallback) }

it { is_expected.to eq('second: 9') }
end

context 'first not defined, second not defined, third defined' do
subject { first_or_else_second_or_else_third.call_or_else(7, &fallback) }

it { is_expected.to eq('third: 7') }
end

context 'first not defined, second not defined, third not defined' do
subject { first_or_else_second_or_else_third.call_or_else(1, &fallback) }

it { is_expected.to eq('fallback: 1') }
end

context 'first, second and third defined' do
subject { first_or_else_second_or_else_third.call_or_else(42, &fallback) }

it { is_expected.to eq('first: 42') }
end
end
end

describe '#and_then' do
let(:first_or_else_second_and_then_function) { first_or_else_second.and_then(&function) }
let(:function) { ->(x) { "f: #{x}" } }

describe '#defined_at?' do
context 'first defined, second not' do
subject { first_or_else_second_and_then_function.defined_at?(2) }

it { is_expected.to eq(true) }
end

context 'first not defined, second defined' do
subject { first_or_else_second_and_then_function.defined_at?(3) }

it { is_expected.to eq(true) }
end

context 'first not defined, second not defined' do
subject { first_or_else_second_and_then_function.defined_at?(5) }

it { is_expected.to eq(false) }
end

context 'first defined, second defined' do
subject { first_or_else_second_and_then_function.defined_at?(6) }

it { is_expected.to eq(true) }
end
end

describe '#call' do
context 'first defined, second not' do
subject { first_or_else_second_and_then_function.call(2) }

it { is_expected.to eq('f: first: 2') }
end

context 'first not defined, second defined' do
subject { first_or_else_second_and_then_function.call(3) }

it { is_expected.to eq('f: second: 3') }
end

context 'first not defined, second not defined' do
subject { -> { first_or_else_second_and_then_function.call(5) } }

it { is_expected.to raise_error(Fear::MatchError, 'partial function not defined at: 5') }
end

context 'first defined, second defined' do
subject { first_or_else_second_and_then_function.call(6) }

it { is_expected.to eq('f: first: 6') }
end
end

describe '#call_or_else' do
let(:fallback) { ->(x) { "fallback: #{x}" } }

context 'first defined, second not' do
subject { first_or_else_second_and_then_function.call_or_else(2, &fallback) }

it { is_expected.to eq('f: first: 2') }
end

context 'first not defined, second defined' do
subject { first_or_else_second_and_then_function.call_or_else(3, &fallback) }

it { is_expected.to eq('f: second: 3') }
end

context 'first not defined, second not defined' do
subject { first_or_else_second_and_then_function.call_or_else(5, &fallback) }

it { is_expected.to eq('fallback: 5') }
end

context 'first defined, second defined' do
subject { first_or_else_second_and_then_function.call_or_else(6, &fallback) }

it { is_expected.to eq('f: first: 6') }
end
end
end
end
Loading