Skip to content

Releases: bolshakov/fear

v3.0.0

02 Apr 21:06
Compare
Choose a tag to compare

What's Changed

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)
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.

Consider using Data from the standard library.

New Contributors

Full Changelog: v2.0.1...v3.0.0

v2.0.1

21 Nov 09:41
2471667
Compare
Choose a tag to compare

What's Changed

Full Changelog: v2.0.0...v2.0.1

v2.0.0

02 Feb 11:41
33efe37
Compare
Choose a tag to compare

What's Changed

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

15 Nov 21:52
512d95c
Compare
Choose a tag to compare

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

14 Jul 18:06
Compare
Choose a tag to compare
  • Add Fear::Await.ready and Fear::Await.result.
  • Add callback versions with pattern matching Fear::Future#on_success_match, #on_failure_match and #on_complete_match.
  • Implement immutable Fear::Struct

v1.0.0

29 Mar 09:09
7cd2f18
Compare
Choose a tag to compare

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 from 1 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 of concurrent-ruby gem and provides monadic interface for asynchronous computations. Its API inspired by future implementation in Scala, but with ruby flavor. See API Documentation

    success = "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 of Fear::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 to Fear::Unit

  • Signatures of Try#recover and Try#recover_with have changed

    Fear.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')