lgierth / promise.rb

Promises/A+ for Ruby

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ES6-alike initializer

sheerun opened this issue · comments

Do you mind introducing initialize that looks similar to ES6 version?

Promise.new do |resolve, reject|
  # ...
end

Why? It sounds like Object#tap does something similar and would be more familiar to ruby developers

Promise.new.tap do |promise|
  # ...
    promise.fulfill(some_value)
  # ...
end

It's to create similar API to ES6 promises. And A+ promises are used primarily in JS world.

This would also allow for easy concurrency with non-blocking IO: the IO action would be captured and could run lazily.

MyPromise
  .all([
    MyPromise.new { |resolve| resolve.(open("http://google.com")) },
    MyPromise.new { |resolve| resolve.(open("http://apple.com")) },
  ])
  .then { |google, apple| ... }

This would make it easy to run Active Record queries, web requests, etc., concurrently. Looking at the source, though, we'd need some plumbing to make this happen.

@stephencelis javascript's Promise constructor takes a function that gets executed immediately. So it doesn't sound like what you want.

@dylanahsmith You're right. I'm using a library that relies on Promise.rb and was hopeful that I could extend things easily to get the benefit of non-blocking IO, though it seems inappropriate if the intent of this library is to behave exactly like the current A+ promise spec.

Read the usage, the library definitely allows you to get the benefits of non-blocking IO.

I don't mean to further derail, but those examples all center around EventMachine, which assumes a specific kind of app. Any examples with a vanilla Thread?

EDIT: I tinkered a bit but it's unclear how Group#wait comes into the equation:

require 'promise.rb'
class ConcurrentPromise < Promise
  def initialize(&f)
    super
    @thread = Thread.new do
      begin
        fulfill(f.(self))
      rescue => e
        reject(e)
      end
    end
  end
  def wait
    @thread.join
  end
end

require 'open-uri'
get = -> url {
  ConcurrentPromise.new {
    puts "getting #{url}..."
    res = open(url)
    puts "got #{url}!"
    res
  }
}

ConcurrentPromise
  .all([
    get.('http://google.com'),
    get.('http://apple.com'),
  ])
  .then { |google, apple|
    puts(google[0..10], apple[0..10])
  }
  .sync

# sync doesn't wait for threads to join: exits early

I can move these comments to a new thread if you have a suggestion, whether it's just a request for documentation, etc.

I'm going to close this issue, since the original issue is about extending the initializer to support a block, but doesn't really provide a compelling use case. It make more sense to just let derived classes provide extensions.

I think there is definitely missing documentation on thread-safety, since Promise isn't thread-safe so shouldn't be fulfilled from another thread.

Instead, I would take advantage of the source attribute that was added to Promise to allow Promise.all to know how to wait for an array of promises of different types, but wasn't documented. Here is a thread-safe example of concurrent execution of a promise:

require 'promise'

module ConcurrentPromise
  def self.execute
    promise = Promise.new
    promise.source = Source.new(promise) { yield }
    promise
  end

  class Source
    def initialize(promise)
      @promise = promise

      # Promise isn't thread-safe, so the thread needs to store the result
      # on a separate object
      @thread_result = Promise.new
      @thread = Thread.new(@thread_result) do |result|
        begin
          result.fulfill(yield)
        rescue => err
          result.reject(err)
        end
      end
    end

    def wait
      @thread.join
      @promise.fulfill(@thread_result)
    end
  end
  private_constant :Source
end

puts ConcurrentPromise.execute { 1 + 1 }.sync