Skip to content

v1.0.0

Compare
Choose a tag to compare
@bolshakov bolshakov released this 29 Mar 09:09
· 181 commits to master since this release
7cd2f18

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