Releases: bolshakov/fear
v3.0.0
What's Changed
- Implement
Fear::Either#left
by @bolshakov in #169
Left projection allows performing right-biased operation of the left side of the Fear::Either
:
Fear.left(42).left.map(&:succ) #=> Fear.left(43)
Fear.right(42).left.map(&:succ) #=> Fear.left(42)
Before implementing left projection, the same operation was possible to achieve with double swapping:
Fear.left(42).swap.map(&:succ).swap #=> Fear.left(43)
Fear.right(42).swap.map(&:succ).swap #=> Fear.left(42)
- Add an alias
#apply
to the#each
method by @ngrebenshikov in #164
Fear.option(42).apply { puts "The answer is #{_1}" }
the above code is the same as the following:
Fear.option(42).each { puts "The answer is #{_1}" }
- Code cleanup by @bolshakov in #171
- Remove development dependencies from the gem-spec
- Reorganised files structure
- Test against ruby >= 3.1.0 by @bolshakov in #170
- Use zeitwerk to load fear files by @bolshakov in #172
Breaking Changes
- Extract Dry::Types integration into a separate gem by @bolshakov in #173
To use Fear::Option
as optional type for Dry::Types
use the dry-types-fear gem.
- Drop Fear::Struct by @bolshakov in #174
Consider using Data from the standard library.
New Contributors
- @ngrebenshikov made their first contribution in #164
Full Changelog: v2.0.1...v3.0.0
v2.0.1
v2.0.0
What's Changed
- Provide top-level factory methods by @bolshakov in #114
- Feature | Add non-emptiness check method by @Lokideos in #138
Breaking changes
- Drop pattern extraction support. Use ruby's pattern matching instead by @bolshakov in #122
New Contributors
Full Changelog: v1.2.0...v2.0.0
v1.2.0
Fear::Future#zip
Now you can call Future#zip
with block argument which maps result
this = Fear.future { 1 }
that = Fear.future { 2 }
this.zip(that) { |x, y| x + y } #=> Fear.success(3)
Fear::Option#zip
Fear.some("foo").zip(Fear.some("bar")) #=> Fear.some(["foo", "bar"])
Fear.some("foo").zip(Fear.some("bar")) { |x, y| x + y } #=> Fear.some("foobar")
Fear.some("foo").zip(Fear.none) #=> Fear.none
Fear.none.zip(Fear.some("bar")) #=> Fear.none
Fear::Option#filter_map
Returns a new Some
of truthy results (everything except false
or nil
) or +None+ otherwise.
Fear.some(42).filter_map { |v| v/2 if v.even? } #=> Fear.some(21)
Fear.some(42).filter_map { |v| v/2 if v.odd? } #=> Fear.none
Fear.some(42).filter_map { |v| false } #=> Fear.none
Fear.none.filter_map { |v| v/2 } #=> Fear.none
Dry-Types and Dry-Struct integration
require 'dry-types'
require 'dry/types/fear'
Dry::Types.load_extensions(:fear_option)
module Types
include Dry.Types()
end
Append .option
to a type name to return Fear::Option
of object:
Types::Option::Strict::Integer[nil]
#=> Fear.none
Types::Option::Coercible::String[nil]
#=> Fear.none
Types::Option::Strict::Integer[123]
#=> Fear.some(123)
Types::Option::Strict::String[123]
#=> Fear.some(123)
Types::Option::Coercible::Float['12.3']
#=> Fear.some(12.3)
Option
types can also accessed by calling .option
on a regular type:
Types::Strict::Integer.option # equivalent to Types::Option::Strict::Integer
You can define your own optional types:
option_string = Types::Strict::String.option
option_string[nil]
# => Fear.none
option_string[nil].map(&:upcase)
# => Fear.none
option_string['something']
# => Fear.some('something')
option_string['something'].map(&:upcase)
# => Fear.some('SOMETHING')
option_string['something'].map(&:upcase).get_or_else { 'NOTHING' }
# => "SOMETHING"
You can use it with dry-struct as well:
class User < Dry::Struct
attribute :name, Types::Coercible::String
attribute :age, Types::Coercible::Integer.option
end
user = User.new(name: 'Bob', age: nil)
user.name #=> "Bob"
user.age #=> Fear.none
user = User.new(name: 'Bob', age: 42)
user.age #=> Fear.some(42)
Implement pattern matching for ruby >= 2.7
Pattern matching works for Fear::Option
, Fear::Either
, and Fear::Try
:
case Fear.some(41)
in Fear::Some(x) if x.even?
x / 2
in Fear::Some(x) if x.odd? && x > 0
x * 2
in Fear::None
'none'
end #=> 82
Fear.xcase
is deprecated and will be removed in fear 2.0.
v1.1.0
v1.0.0
Added
-
Now you can use pattern matching against monads and your own values. See documentation.
Fear.some(42).match do |m| m.some { |x| x * 2 } m.none { 'none' } end #=> 84 x = Random.rand(10) Fear.match(x) do |m| m.case(0) { 'zero' } m.case(1) { 'one' } m.case(2) { 'two' } m.else(Integer, ->(n) { n > 2} ) { 'many' } end
Despite of standard case statement, pattern matching raises
Fear::MatchError
error if nothing was matched. Another interesting property is reusability. Matcher behaves like a function defined on a subset of all possible inputs. Look at recursive factorial definition:factorial = Fear.matcher do |m| m.case(->(n) { n <= 1} ) { 1 } m.else { |n| n * factorial.(n - 1) } end factorial.(10) #=> 3628800
You can compose several matchers together using
#and_then
and#or_else
methods: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
To avoid raising error, you use either
#lift
method or#call_or_else
. Lets look at the following fibonnaci number calculator: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) } end fibonnaci.(10) #=> 55 fibonnaci.(-1) #=> raises Fear::MatchError fibonnaci.lift.(-1) #=> Fear::None fibonnaci.lift.(10) #=> Fear::Some.new(55) fibonnaci.call_or_else(-1) { 'nothing' } #=> 'nothing' fibonnaci.call_or_else(10) { 'nothing' } #=> 55
-
Pattern extraction added. See documentation
It enables special syntax to match against pattern and extract values from that pattern at the same time. For example the following pattern matches an array starting from1
and captures its tail:matcher = Fear.matcher do |m| m.xcase('[1, *tail]') { |tail:| tail } end matcher.([1,2,3]) #=> [2,3] matcher.([2,3]) #=> raises MatchError
_
matches any value. Thus, the following pattern matches[1, 2, 3]
,[1, 'foo', 3]
, etc.matcher = Fear.matcher do |m| m.xcase('[1, _, 3]') { # ... } end
This syntax allows to match and extract deeply nested structures
matcher = Fear.matcher do |m| m.xcase('[["status", first_status], 4, *tail]') { |first_status:, tail: |.. } end matcher.([['status', 400], 4, 5, 6]) #=> yields block with `{first_status: 400, tail: [5,6]}`
It's also possible to extract custom data structures. Documentation has detailed explanation how to implement own extractor.
Fear has several built-in reference extractors:
matcher = Fear.matcher do |m| m.xcase('Date(year, 2, 29)', ->(year:) { year < 2000 }) do |year:| "#{year} is a leap year before Millennium" end m.xcase('Date(year, 2, 29)') do |year:| "#{year} is a leap year after Millennium" end m.case(Date) do |date| "#{date.year} is not a leap year" end end matcher.(Date.new(1996,02,29)) #=> "1996 is a leap year before Millennium" matcher.(Date.new(2004,02,29)) #=> "1996 is a leap year after Millennium" matcher.(Date.new(2003,01,24)) #=> "2003 is not a leap year"
-
All monads got
#match
and.matcher
method to match against contained values or build reusable matcher:Fear.some(41).match do |m| m.some(:even?.to_proc) { |x| x / 2 } m.some(:odd?.to_proc, ->(v) { v > 0 }) { |x| x * 2 } m.none { 'none' } end #=> 82 matcher = Fear::Option.matcher do |m| m.some(42) { 'Yep' } m.some { 'Nope' } m.none { 'Error' } end matcher.(Fear.some(42)) #=> 'Yep' matcher.(Fear.some(40)) #=> 'Nope'
-
Fear::Future
was deleted long time ago, but now it's back. It's implemented on top ofconcurrent-ruby
gem and provides monadic interface for asynchronous computations. Its API inspired by future implementation in Scala, but with ruby flavor. See API Documentationsuccess = "Hello" f = Fear.future { success + ' future!' } f.on_success do |result| puts result end
The simplest way to wait for several futures to complete
Fear.for(Fear.future { 5 }, Fear.future { 3 }) do |x, y| x + y end #=> eventually will be 8
Since Futures use Concurrent::Promise under the hood.
Fear.future
accepts optional configuration Hash passed directly to underlying promise. For example, run it on custom thread pool.require 'open-uri' pool = Concurrent::FixedThreadPool.new(5) future = Fear.future(executor: pool) { open('https://example.com/') } future.map(&:read).each do |body| puts "#{body}" end
-
A bunch of factory method added to build monads without mixin a module:
Fear.some(value)
Fear.option(value_or_nil)
Fear.none
Fear.left(value)
Fear.right(value)
Fear.try(&block)
Fear.success(value)
Fear.failure(error)
Fear.for(*monads, &block)
Breaking
-
Support for ruby 2.3.7 was dropped.
-
Fear::None
is singleton now and the only instance ofFear::NoneClass
. -
Fear.for
syntax changed. Now it accepts a list of monads (previously hash)Fear.for(Fear.some(2), Fear.some(3)) do |a, b| a * b end #=> Fear.some(6) Fear.for(Fear.some(2), Fear.none) do |a, b| a * b end #=> Fear::None
It's internal implementation also changed -- less metaprogramming magic, faster execution
-
#to_a
method removed. -
Fear::Done
was renamed toFear::Unit
-
Signatures of
Try#recover
andTry#recover_with
have changedFear.failure(ArgumentError.new).recover_with do |m| m.case(ZeroDivisionError) { Fear.success(0) } m.case(ArgumentError) { |error| Fear.success(error.class.name) } end #=> Fear.success('ArgumentError') Fear.failure(ArgumentError.new).recover do |m| m.case(ZeroDivisionError) { 0 } m.case(&:message) end #=> Fear.success('ArgumentError')