Skip to content

✨for yield* each loops#741

Merged
cowboyd merged 1 commit intov3from
v3-for-yield-each
Aug 10, 2023
Merged

✨for yield* each loops#741
cowboyd merged 1 commit intov3from
v3-for-yield-each

Conversation

@cowboyd
Copy link
Copy Markdown
Member

@cowboyd cowboyd commented Aug 9, 2023

Note: supersedes #727

Motivation

One of the design goals of Effection is to provide structured concurrency primitives that are very aligned with ordinary JavaScript and let libraries provide the higher abstractions based on those primitives. This is why we have Streams for AsyncIterables, and Subscription for AsyncIterator. What was missing was a natural way to consume streams that felt like the for await of loop.

Approach

This introduces the each() and each.next operations that let you consume any stream completely naturally.

for (let value of yield* each(stream)) {
  console.log(value);
  yield* each.next;
}

@cowboyd cowboyd requested a review from neurosnap August 9, 2023 22:11
Copy link
Copy Markdown
Collaborator

@neurosnap neurosnap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me! You mentioned previously that we might need to get the last item out of the iterable but I don't see a test/API for that in this PR?

Comment thread lib/each.ts

const EachStack = createContext<EachLoop<unknown>[]>("each");

function iterate<T>(stream: Stream<T, unknown>): Operation<Iterable<T>> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change the iterate API to return Operation<AsyncIterable<T>> so that the consumer of each can do this

for await (const value of yield* each(stream)) {
   console.log(value)
}

It also removes the error-prone requirement of yield* each.next

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For implementation, we may move let current = yield* subscription.next() into the async iterator, and let [Symbol.asyncIterator]().next await for subscription.next() for each iteration.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While, for await requires async keyword, which might be a huge pollution :(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think you hit the nail on the head. One of the problems that Effection solves is it heals the scar of sync/async present in everything in JavaScript. Function vs AsyncFunction, Symbol.iterator vs Symbol.asyncIterator, Symbol.dispose vs Symbol.asyncDispose

In Effection, there is no difference between how a synchronous operation in Effection and an async one because they all boil down to a continuation. Some continuations are resumed immediately, others after some ticks of runtime, but from the correctness of the program, it is immaterial.

@cowboyd
Copy link
Copy Markdown
Member Author

cowboyd commented Aug 10, 2023

looks good to me! You mentioned previously that we might need to get the last item out of the iterable but I don't see a test/API for that in this PR?

So unfortunately, there is no way to access the close value of an iterator or an async iterator via a for loop, so I was thinking we'd just replicate this behavior for now, and if we want to add an each.close operation later we can do it as a non-breaking change, but I was a bit nervous about it because right now, the iteration context only lives inside the for loop and nothing "leaks" out.

One of the design goals of Effection is to provide structured
concurrency primitives that are very aligned with ordinary JavaScript
and let libraries provide the higher abstractions based on those
primitives. This is why we have `Streams` for AsyncIterables, and
`Subscription` for `AsyncIterator`. What was missing was a natural way
to consume streams that felt like the `for await of` loop.

This introduces the `each()` and `each.next` operations that let you
consume any stream completely naturally.

```javascript
for (let value of yield* each(stream)) {
  console.log(value);
  yield* each.next;
}
```
@cowboyd cowboyd force-pushed the v3-for-yield-each branch from 2c05e70 to 6383ba6 Compare August 10, 2023 20:55
@cowboyd cowboyd merged commit ce08c90 into v3 Aug 10, 2023
@cowboyd cowboyd deleted the v3-for-yield-each branch August 10, 2023 20:58
taras pushed a commit that referenced this pull request Nov 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants