belozierov / SwiftCoroutine

Swift coroutines for iOS, macOS and Linux.
https://belozierov.github.io/SwiftCoroutine
MIT License
836 stars 51 forks source link

Extending Generator to include Input and Output #4

Closed DanielAsher closed 4 years ago

DanielAsher commented 4 years ago

Javascript Generators allow for both input and output arguments in their yield:

  `[input] = yield [output]`

I have found this incredibly useful in using coroutines that typically either:

Is it possible to extend Generator in this way?

Let me say, I love the ambition of this project! Having been a swift dev for a few years, I recently jumped into javascript and kotlin, and have found async function generators , coroutines and structured concurrency to be the single most important revolution in language design in a generation. Having to go back to using RxFeedback with switch yard state-machines fills me with dread.


Some motivating examples:

Notice in the examples below how coroutines allow for much more elegant way to store state and correctly consume a sequence (i.e. a grammar) of events.

An async state machine can also be beautifully coded as a Generator<Event, State> with events sent to the machine using let newState = stateflow.next(event)

I have a server-side generator that creates websocket events:

async function* videoDownloader({
  localVideoFilePath,
  videoURL,
  videoID
}) {
  yield State.Initial
  const localFileStream = fs.createWriteStream(localVideoFilePath)
  const localFileCompleted = new Promise(resolve => localFileStream.once('close', () => resolve()))
  const videoPipe = videoStream
    .pipe(localFileStream)
  const opened = new Promise(resolve => videoPipe.once('open', () => resolve()))
  const closed = new Promise(resolve => videoPipe.once('close', () => resolve({
    event: 'close'
  })))
  const error = new Promise(resolve => videoPipe.once('error', error => resolve({
    event: 'close',
    error: error
  })))
  await opened
  const result = await Promise.race([closed, error])
  switch (result.event) {
  case 'closed': 
     yield State.Complete
     break
  case 'error':
    yield State.Error(result.message) 
  }
}

with a client-side consumer that monitors so:

  function* processDownload() {
    var next = yield
    expect(next, 'Initial')
    next = yield
    expect(next, 'ReceivedResponse')
    processMonitor.videoMetadata = next['0']
    processMonitor.videoMetadata.json = processMonitor.videoMetadataJson
    processMonitor.videoDuration = parseInt(processMonitor.videoMetadata.length.slice(0, -2))
    next = yield
    expect(next, 'Downloading')
    do {
      const percent = next['0']
      downloadProgress.progress('set percent', percent)
      downloadProgress.progress('set label', `Downloading ${percent.toFixed(0)}%`)
      downloadProgress.progress('set bar label', `${percent.toFixed(0)}%`)
      next = yield
    } while (next._name === 'Downloading')

    expect(next, 'Complete')
    downloadProgress.progress('set success', 'Video download complete')
    processMonitor.download.isActive = false
    processMonitor.download.isCompleted = true
    processMonitor.conversion.isActive = true
  }

Or for android / kotlin I use a builder function to implement a coroutine-based state machine so:

@ExperimentalCoroutinesApi
@FlowPreview
private val stateFlow = StateFlow<Mode, Event> {

    uninitialized@ while (true) {
        yield(Mode.Uninitialized) thenRequire Command.Initialize
        yield(Mode.Initializing) thenRequire Response.Initialized
        when (yield(Mode.Initialized)) {
            is Command.Prepare   -> {
                yield(Mode.Preparing) thenRequire Response.Prepared
            }
            Command.Uninitialize -> {
                yield(Mode.Uninitializing) thenRequire Response.Uninitialized
                continue@uninitialized
            }
            else                 -> exception()
        }
        ready@ while (true) {
            when (yield(Mode.Ready)) {
                Command.Play         -> {
                    when (yield(Mode.Playing)) {
                        Command.Stop     -> {
                            yield(Mode.Stopping) thenRequire Response.Stopped
                            continue@ready
                        }
                        Response.Stopped -> continue@ready
                        else             -> exception()
                    }
                }
                is Command.Prepare   -> {
                    yield(Mode.Preparing) thenRequire Response.Prepared
                    continue@ready
                }
                Command.Uninitialize -> {
                    yield(Mode.Uninitializing) thenRequire Response.Uninitialized
                    continue@uninitialized
                }
                else                 -> exception()
            }
        }
    }
    Response.Completed
}

Please note the examples above are chopped down and thus prototype code only.

belozierov commented 4 years ago

@DanielAsher Yes, you can use the Generator in this way, here is an example:

var items = [2, 3, 1, 4, 2, 4]
let generator = Generator<((Int, Int) -> Bool) -> Void> { yield in
   items.sort { left, right in
      var result = false
      yield { result = $0(left, right) }
      return result
   }
}
while let next = generator.next() { next(>) }
XCTAssertEqual(items, items.sorted(by: >))

Maybe Generator api is not that elegant as in Javascript, but that can definitely be modified. Also, I’d like to draw your attention that Generator is marked as alpha version in the description, because this implementation was made rather quickly and requires very careful use. I recommend not to use Generator in your products, but only for own curiosity.

Also, I’d like to inform you that I am now finishing a new version of the framework, where I focused primarily on the performance and safe of using the library. Because of this, I plan to remove a lot of functionality that is not completely refined or not safe, including Generator. Maybe I'll come back to this api later, but for now it's not a priority for me. Thanks for understanding.

DanielAsher commented 4 years ago

@belozierov This is very interesting information. Perhaps a fork is required to really understand the corner cases. I believe this is the future of Swift, and you are leading the way!