Class: Fear::Future

Inherits:
Object
  • Object
show all
Includes:
Awaitable
Defined in:
lib/fear/future.rb

Overview

Asynchronous computations that yield futures are created with the Fear.future call:

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.

f = Fear.future { 5 }
g = Fear.future { 3 }
f.flat_map do |x|
  g.map { |y| x + y }
end

The same program may be written using Fear.for

Fear.for(Fear.future { 5 }, Fear.future { 3 }) do |x, y|
  x + y
end

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'

future = Fear.future(executor: :io) { open('https://example.com/') }

future.map(executor: :fast, &:read).each do |body|
  puts "#{body}"
end

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(promise = nil, **options) { ... } ⇒ Future

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Future.

Parameters:

  • promise (nil, Concurrent::Future) (defaults to: nil)

    converts Concurrent::Promise into Fear::Future.

  • options (see Concurrent::Future)

    options will be passed directly to Concurrent::Promise

Yields:

  • given block and evaluate it in the future.

See Also:

  • Fear.future


56
57
58
59
60
61
62
63
64
65
# File 'lib/fear/future.rb', line 56

def initialize(promise = nil, **options, &block)
  if block_given? && promise
    raise ArgumentError, "pass block or promise"
  end

  @options = options
  @promise = promise || Concurrent::Promise.execute(@options) do
    Fear.try(&block)
  end
end

Class Method Details

.failed(exception) ⇒ Fear::Future

Creates an already completed Future with the specified error.

Parameters:

  • exception (StandardError)

Returns:



474
475
476
477
# File 'lib/fear/future.rb', line 474

def failed(exception)
  new { raise exception }
    .yield_self { |future| Fear::Await.ready(future, 10) }
end

.successful(result) ⇒ Fear::Future

Creates an already completed Future with the specified result.

Parameters:

  • result (Object)

Returns:



483
484
485
486
# File 'lib/fear/future.rb', line 483

def successful(result)
  new { result }
    .yield_self { |future| Fear::Await.ready(future, 10) }
end

Instance Method Details

#__ready__(at_most) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



461
462
463
464
465
466
467
# File 'lib/fear/future.rb', line 461

def __ready__(at_most)
  if promise.wait(at_most).complete?
    self
  else
    raise Timeout::Error
  end
end

#__result__(at_most) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



456
457
458
# File 'lib/fear/future.rb', line 456

def __result__(at_most)
  __ready__(at_most).value.get_or_else { raise "promise not completed" }
end

#and_thenObject

Note:

that if one of the chained and_then callbacks throws

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.

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.

Examples:

The following example prints out 5:

f = Fear.future { 5 }
f.and_then do
  m.success { }fail| 'runtime error' }
end.and_then do |m|
  m.success { |value| puts value } # it evaluates this branch
  m.failure { |error| puts error.massage }
end


443
444
445
446
447
448
449
450
451
452
453
# File 'lib/fear/future.rb', line 443

def and_then
  promise = Promise.new(**@options)
  on_complete do |try|
    Fear.try do
      Fear::Try.matcher { |m| yield(m) }.call_or_else(try, &:itself)
    end
    promise.complete!(try)
  end

  promise.to_future
end

#completed?true, false

Returns whether the future has already been completed with a value or an error.

Examples:

future = Fear.future { }
future.completed? #=> false
sleep(1)
future.completed? #=> true

Returns:

  • (true, false)

    true if the future is already completed, false otherwise.



201
202
203
# File 'lib/fear/future.rb', line 201

def completed?
  promise.fulfilled?
end

#fallback_to(fallback) ⇒ Fear::Future

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.

Examples:

f = Fear.future { fail 'error' }
g = Fear.future { 5 }
f.fallback_to(g) # evaluates to 5

Parameters:

Returns:



408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/fear/future.rb', line 408

def fallback_to(fallback)
  promise = Promise.new(**@options)
  on_complete_match do |m|
    m.success { |value| promise.complete!(value) }
    m.failure do |error|
      fallback.on_complete_match do |m2|
        m2.success { |value| promise.complete!(value) }
        m2.failure { promise.failure!(error) }
      end
    end
  end

  promise.to_future
end

#flat_map {|| ... } ⇒ Fear::Future

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.

Examples:

f1 = Fear.future { 5 }
f2 = Fear.future { 3 }
f1.flat_map do |v1|
  f1.map do |v2|
    v2 * v1
  end
end

Yield Parameters:

  • (any)

Returns:



290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/fear/future.rb', line 290

def flat_map
  promise = Promise.new(**@options)
  on_complete_match do |m|
    m.case(Fear::Failure) { |failure| promise.complete!(failure) }
    m.success do |value|
      yield(value).on_complete { |callback_result| promise.complete!(callback_result) }
    rescue StandardError => error
      promise.failure!(error)
    end
  end
  promise.to_future
end

#map(&block) ⇒ Fear::Future

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.

Examples:

future = Fear.future { 2 }
future.map { |v| v * 2 } #=> the same as Fear.future { 2 * 2 }

Returns:



264
265
266
267
268
269
270
271
# File 'lib/fear/future.rb', line 264

def map(&block)
  promise = Promise.new(**@options)
  on_complete do |try|
    promise.complete!(try.map(&block))
  end

  promise.to_future
end

#on_complete {|| ... } ⇒ self

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.

Examples:

Fear.future { }.on_complete do |try|
  try.map(&:do_the_job)
end

Yield Parameters:

Returns:

  • (self)


162
163
164
165
166
167
# File 'lib/fear/future.rb', line 162

def on_complete
  promise.add_observer do |_time, try, _error|
    yield try
  end
  self
end

#on_complete_match {|| ... } ⇒ self

When this future is completed match against result.

If the future has already been completed, this will either be applied immediately or be scheduled asynchronously.

Examples:

Fear.future { }.on_complete_match do |m|
  m.success { |result| }
  m.failure { |error| }
end

Yield Parameters:

Returns:

  • (self)


182
183
184
185
186
187
# File 'lib/fear/future.rb', line 182

def on_complete_match
  promise.add_observer do |_time, try, _error|
    Fear::Try.matcher { |m| yield(m) }.call_or_else(try, &:itself)
  end
  self
end

#on_failure {|| ... } ⇒ self

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.

Examples:

Fear.future { }.on_failure do |error|
  if error.is_a?(HTTPError)
    # ...
  end
end

Yield Parameters:

  • (StandardError)

Returns:

  • (self)


122
123
124
125
126
127
128
# File 'lib/fear/future.rb', line 122

def on_failure
  on_complete do |result|
    if result.failure?
      yield result.exception
    end
  end
end

#on_failure_match {|m| ... } ⇒ self

When this future is completed with a failure match against 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.

Examples:

Fear.future { }.on_failure_match do |m|
  m.case(HTTPError) { |error| ... }
end

Yield Parameters:

Returns:

  • (self)


144
145
146
147
148
# File 'lib/fear/future.rb', line 144

def on_failure_match
  on_failure do |error|
    Fear.matcher { |m| yield(m) }.call_or_else(error, &:itself)
  end
end

#on_success {|value| ... } ⇒ self Also known as: each

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.

Examples:

Fear.future { }.on_success do |value|
  # ...
end

Yield Parameters:

  • value (any)

Returns:

  • (self)

See Also:



82
83
84
85
86
# File 'lib/fear/future.rb', line 82

def on_success(&block)
  on_complete do |result|
    result.each(&block)
  end
end

#on_success_match {|m| ... } ⇒ self

When this future is completed successfully match against its result

If the future has already been completed with a value, this will either be applied immediately or be scheduled asynchronously.

Examples:

Fear.future { }.on_success_match do |m|
  m.case(42) { ... }
end

Yield Parameters:

Returns:

  • (self)


100
101
102
103
104
# File 'lib/fear/future.rb', line 100

def on_success_match
  on_success do |value|
    Fear.matcher { |m| yield(m) }.call_or_else(value, &:itself)
  end
end

#recover(&block) ⇒ Fear::Future

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.

Examples:

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

Returns:



344
345
346
347
348
349
350
351
# File 'lib/fear/future.rb', line 344

def recover(&block)
  promise = Promise.new(**@options)
  on_complete do |try|
    promise.complete!(try.recover(&block))
  end

  promise.to_future
end

#select {|| ... } ⇒ Fear::Future

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.

Examples:

f = Fear.future { 5 }
f.select { |value| value % 2 == 1 } # evaluates to 5
f.select { |value| value % 2 == 0 } # fail with NoSuchElementError

Yield Parameters:

  • (#get)

Returns:



320
321
322
323
324
325
326
327
328
# File 'lib/fear/future.rb', line 320

def select
  map do |result|
    if yield(result)
      result
    else
      raise NoSuchElementError, "#select predicate is not satisfied"
    end
  end
end

#transform(success, failure) {|success, failure| ... } ⇒ Fear::Future

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.

Examples:

Fear.future { open('http://example.com').read }
  .transform(
     ->(value) { ... },
     ->(error) { ... },
  )

Yield Parameters:

  • success (#get)

    function that transforms a successful result of the receiver into a successful result of the returned future

  • failure (#exception)

    function that transforms a failure of the receiver into a failure of the returned future

Returns:

  • (Fear::Future)

    a future that will be completed with the transformed value



245
246
247
248
249
250
251
252
# File 'lib/fear/future.rb', line 245

def transform(success, failure)
  promise = Promise.new(**@options)
  on_complete_match do |m|
    m.success { |value| promise.success(success.(value)) }
    m.failure { |error| promise.failure(failure.(error)) }
  end
  promise.to_future
end

#valueFear::Option<Fear::Try>

The value of this Future.

Returns:

  • (Fear::Option<Fear::Try>)

    if the future is not completed the returned value will be Fear::None. If the future is completed the value will be Fear::Some<Fear::Success> if it contains a valid result, or Fear::Some<Fear::Failure> if it contains an error.



213
214
215
# File 'lib/fear/future.rb', line 213

def value
  Fear.option(promise.value(0))
end

#zip(other) ⇒ Fear::Future

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.

Examples:

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

Parameters:

Returns:



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/fear/future.rb', line 370

def zip(other)
  promise = Promise.new(**@options)
  on_complete_match do |m|
    m.success do |value|
      other.on_complete do |other_try|
        promise.complete!(
          other_try.map do |other_value|
            if block_given?
              yield(value, other_value)
            else
              [value, other_value]
            end
          end,
        )
      end
    end
    m.failure do |error|
      promise.failure!(error)
    end
  end

  promise.to_future
end