begin require 'concurrent' rescue LoadError puts "You must add 'concurrent-ruby' to your Gemfile in order to use Fear::Future" end require_relative 'promise' module Fear # Asynchronous computations that yield futures are created # with the +Fear.future+ call: # # @example # success = "Hello" # f = Fear.future { success + ' future!' } # f.on_success do |result| # puts result # end # # Multiple callbacks may be registered; there is no guarantee # that they will be executed in a particular order. # # The future may contain an exception and this means # that the future failed. Futures obtained through combinators # have the same error as the future they were obtained from. # # @example # f = Fear.future { 5 } # g = Fear.future { 3 } # f.flat_map do |x| # g.map { |y| x + y } # end # # @example the same program may be written using +Fear.for+ # Fear.for(Fear.future { 5 }, Fear.future { 3 }) do |x, y| # x + y # end # # @see https://github.com/scala/scala/blob/2.11.x/src/library/scala/concurrent/Future.scala # class Future # @param promise [nil, Concurrent::Future] converts # +Concurrent::Future+ into +Fear::Future+. # @param options [see Concurrent::Future] options will be passed # directly to +Concurrent::Future+ # @yield given block and evaluate it in the future. # @api private def initialize(promise = nil, **options, &block) if block_given? && promise raise ArgumentError, 'pass block or future' end @options = options @promise = promise || Concurrent::Promise.execute(@options) do Fear.try(&block) end end attr_reader :promise private :promise # Calls the provided callback When this future is completed successfully. # # If the future has already been completed with a value, # this will either be applied immediately or be scheduled asynchronously. # @yieldparam [value] # @return [self] # @see #transform # # @example # Fear.future { }.on_success do |value| # # ... # end # def on_success(&block) on_complete do |result| result.each(&block) end end # When this future is completed with a failure apply the provided callback to the error. # # If the future has already been completed with a failure, # this will either be applied immediately or be scheduled asynchronously. # # Will not be called in case that the future is completed with a value. # @yieldparam [StandardError] # @return [self] # # @example # Fear.future { }.on_failure do |error| # if error.is_a?(HTTPError) # # ... # end # end # def on_failure on_complete do |result| if result.failure? yield result.exception end end end # When this future is completed call the provided block. # # If the future has already been completed, # this will either be applied immediately or be scheduled asynchronously. # @yieldparam [Fear::Try] # @return [self] # # @example # Fear.future { }.on_complete do |try| # try.map(&:do_the_job) # end # def on_complete promise.add_observer do |_time, try, _error| yield try end self end # Returns whether the future has already been completed with # a value or an error. # # @return [true, false] +true+ if the future is already # completed, +false+ otherwise. # # @example # future = Fear.future { } # future.completed? #=> false # sleep(1) # future.completed? #=> true # def completed? promise.fulfilled? end # The value of this +Future+. # # @return [Fear::Option] if the future is not completed # the returned value will be +Fear::None+. If the future is # completed the value will be +Fear::Some+ if it # contains a valid result, or +Fear::Some+ if it # contains an error. # def value Fear.option(promise.value(0)) end # Asynchronously processes the value in the future once the value # becomes available. # # Will not be called if the future fails. # @yieldparam [any] yields with successful feature value # @see {#on_complete} # alias each on_success # Creates a new future by applying the +success+ function to the successful # result of this future, or the +failure+ function to the failed result. # If there is any non-fatal error raised when +success+ or +failure+ is # applied, that error will be propagated to the resulting future. # # @yieldparam success [#get] function that transforms a successful result of the # receiver into a successful result of the returned future # @yieldparam failure [#exception] function that transforms a failure of the # receiver into a failure of the returned future # @return [Fear::Future] a future that will be completed with the # transformed value # # @example # Fear.future { open('http://example.com').read } # .transform( # ->(value) { ... }, # ->(error) { ... }, # ) # def transform(success, failure) promise = Promise.new(@options) on_complete do |try| try.match do |m| m.success { |value| promise.success(success.call(value)) } m.failure { |error| promise.failure(failure.call(error)) } end end promise.to_future end # Creates a new future by applying a block to the successful result of # this future. If this future is completed with an error then the new # future will also contain this error. # # @return [Fear::Future] # # @example # future = Fear.future { 2 } # future.map { |v| v * 2 } #=> the same as Fear.future { 2 * 2 } # def map(&block) promise = Promise.new(@options) on_complete do |try| promise.complete!(try.map(&block)) end promise.to_future end # Creates a new future by applying a block to the successful result of # this future, and returns the result of the function as the new future. # If this future is completed with an exception then the new future will # also contain this exception. # # @yieldparam [any] # @return [Fear::Future] # # @example # f1 = Fear.future { 5 } # f2 = Fear.future { 3 } # f1.flat_map do |v1| # f1.map do |v2| # v2 * v1 # end # end # def flat_map # rubocop: disable Metrics/MethodLength promise = Promise.new(@options) on_complete do |result| result.match do |m| m.failure { promise.complete!(result) } m.success do |value| begin yield(value).on_complete { |callback_result| promise.complete!(callback_result) } rescue StandardError => error promise.failure!(error) end end end end promise.to_future end # Creates a new future by filtering the value of the current future # with a predicate. # # If the current future contains a value which satisfies the predicate, # the new future will also hold that value. Otherwise, the resulting # future will fail with a +NoSuchElementError+. # # If the current future fails, then the resulting future also fails. # # @yieldparam [#get] # @return [Fear::Future] # # @example # f = Fear.future { 5 } # f.select { |value| value % 2 == 1 } # evaluates to 5 # f.select { |value| value % 2 == 0 } # fail with NoSuchElementError # def select map do |result| if yield(result) result else raise NoSuchElementError, '#select predicate is not satisfied' end end end # Creates a new future that will handle any matching error that this # future might contain. If there is no match, or if this future contains # a valid result then the new future will contain the same. # # @return [Fear::Future] # # @example # Fear.future { 6 / 0 }.recover { |error| 0 } # result: 0 # Fear.future { 6 / 0 }.recover do |m| # m.case(ZeroDivisionError) { 0 } # m.case(OtherTypeOfError) { |error| ... } # end # result: 0 # # def recover(&block) promise = Promise.new(@options) on_complete do |try| promise.complete!(try.recover(&block)) end promise.to_future end # Zips the values of +self+ and +other+ future, and creates # a new future holding the array of their results. # # If +self+ future fails, the resulting future is failed # with the error stored in +self+. # Otherwise, if +other+ future fails, the resulting future is failed # with the error stored in +other+. # # @param other [Fear::Future] # @return [Fear::Future] # # @example # future1 = Fear.future { call_service1 } # future1 = Fear.future { call_service2 } # future1.zip(future2) #=> returns the same result as Fear.future { [call_service1, call_service2] }, # # but it performs two calls asynchronously # def zip(other) # rubocop: disable Metrics/MethodLength promise = Promise.new(@options) on_complete do |try_of_self| try_of_self.match do |m| m.success do |value| other.on_complete do |try_of_other| promise.complete!(try_of_other.map { |other_value| [value, other_value] }) end end m.failure do |error| promise.failure!(error) end end end promise.to_future end # Creates a new future which holds the result of +self+ future if it # was completed successfully, or, if not, the result of the +fallback+ # future if +fallback+ is completed successfully. # If both futures are failed, the resulting future holds the error # object of the first future. # # @param fallback [Fear::Future] # @return [Fear::Future] # # @example # f = Fear.future { fail 'error' } # g = Fear.future { 5 } # f.fallback_to(g) # evaluates to 5 # # rubocop: disable Metrics/MethodLength def fallback_to(fallback) promise = Promise.new(@options) on_complete do |try| try.match do |m| m.success { |value| promise.complete!(value) } m.failure do |error| fallback.on_complete do |fallback_try| fallback_try.match do |m2| m2.success { |value| promise.complete!(value) } m2.failure { promise.failure!(error) } end end end end end promise.to_future end # rubocop: enable Metrics/MethodLength # Applies the side-effecting block to the result of +self+ future, # and returns a new future with the result of this future. # # This method allows one to enforce that the callbacks are executed in a # specified order. # # @note that if one of the chained +and_then+ callbacks throws # an error, that error is not propagated to the subsequent # +and_then+ callbacks. Instead, the subsequent +and_then+ callbacks # are given the original value of this future. # # @example The following example prints out +5+: # f = Fear.future { 5 } # f.and_then do # fail 'runtime error' # end.and_then do |m| # m.success { |value| puts value } # it evaluates this branch # m.failure { |error| puts error.massage } # end # def and_then(&block) promise = Promise.new(@options) on_complete do |try| Fear.try { try.match(&block) } promise.complete!(try) end promise.to_future end class << self # Creates an already completed +Future+ with the specified error. # @param exception [StandardError] # @return [Fear::Future] # def failed(exception) new(executor: Concurrent::ImmediateExecutor.new) do raise exception end end # Creates an already completed +Future+ with the specified result. # @param result [Object] # @return [Fear::Future] # def successful(result) new(executor: Concurrent::ImmediateExecutor.new) do result end end end end end