-
-
Notifications
You must be signed in to change notification settings - Fork 274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suggestion to Mark Asynchronous Operations in the Type System #4291
Comments
I experimented with this suggestion to explore its limits concerning the composability of
In other words, This seems like a sensible default, as it also enables distinguishing between synchronous and asynchronous const s1 = Stream.make(1, 2, 3, 4); // : Stream<number>
const s2 = s1.pipe(Stream.schedule(Schedule.spaced("1 second"))); // : Stream<number, never, Async>
const e1 = Stream.runDrain(s1); // : Effect<void>
const e2 = Stream.runDrain(s2); // : Effect<void, never, Async> That said, if for some reason it is preferable not to encode the synchronicity of streams in the type system, we can adopt a simpler approach by making // src/Stream.ts
declare module "./Effect.js" {
interface Effect<A, E, R> extends Stream<A, E, Exclude<R, Async>> {}
} Finally, I wanted to verify whether TypeScript is capable of expressing types that strictly enforce either synchronous or asynchronous effects, regardless of other requirements. It turns out this is entirely feasible: type AsyncEffect<A, E, R> = R extends Async ? E.Effect<A, E, R> : never;
type SyncEffect<A, E, R> = E.Effect<A, E, Exclude<R, Async>>;
const anyAsyncEffect = <A, E, R>(e: AsyncEffect<A, E, R>) => e;
const anySyncEffect = <A, E, R>(e: SyncEffect<A, E, R>) => e;
const anyEffect = <A, E, R>(e: E.Effect<A, E, R>) => e;
anyAsyncEffect(testAsync);
anyAsyncEffect(testSync); // Fails.
anySyncEffect(testAsync); // Fails.
anySyncEffect(testSync);
anyEffect(testAsync);
anyEffect(testSync); These type aliases, |
Yes it is possible to have some kind of But it adds more complexity than what it is worth. In the majority of cases you don't need to think about the nature of an Effect, and in my opinion the extra noise in the types is not worth the type safety benefits. |
I don’t see these so-called “false positives” as problematic. To me, This is analogous to saying that That said, I can appreciate the argument about complexity potentially outweighing the benefits. However, as you mentioned, in the majority of cases, we don’t need to concern ourselves with the synchronicity of an effect. A key advantage of this proposal is that it aligns with this principle: if you don’t care about the synchronicity, the default typing |
To draw an analogy, In most cases, we don’t need to explicitly work with From a terminology perspective, it might make sense to rename the current type Effect<A, E, R> = NonAsyncEffect<A, E, R | Async>; This adjustment would better reflect the distinction between the base synchronous type and its asynchronous superset. Internal functions wouldn’t need to make this distinction immediately. We could gradually migrate functions known to be synchronous to use Even if only half of the types are migrated initially, while the other half remain loose, it would still represent a significant improvement in clarity and safety. The gradual adoption of more precise typings would allow for incremental refinements without imposing an immediate overhaul on the library or its users. |
I'm still not convinced it is worth it. It also draws too much attention to async vs sync effects which the developer shouldn't really care about. I'll let another contributor weigh in before moving any further with this issue. I have a feeling this design has already been attempted before. |
I see. Well, anyway, I do agree—although it's a nice-to-have, I wouldn't consider it crucial, so maybe it's just not worth it indeed 😕 |
Given that Async would be a service it is possible to provide such service and erase it from the signature, effectively getting to This topic has already been discussed quite a few times if you want to see the previous discussions feel free to search on our Discord server, in short we don't believe in and we won't evaluate an addition of an Closing as not planned |
Re attempting the design before I can confirm that it was attempted, at some point Effect was |
It’s perfectly fine to aim for being "as precise as possible." For example, with functions like Effect.retry or Effect.async, we are compelled to mark them as Async, and that’s entirely okay. Async doesn’t mean the effect must be asynchronous—just that it can be. This is why I believe renaming Effect to NonAsyncEffect and introducing a new Effect type alias would make more sense. Similar to the relationship between NonEmptyArray and Array, it’s reasonable to return Array by default when compile-time precision isn’t possible. Additionally, I previously mentioned not exporting the Async service to prevent accidental provision of asynchronous behavior. However, even if users were to explicitly provide it, that would clearly be intentional and therefore not problematic, in my opinion. That said, I appreciate the rationale behind your explanation—thank you for taking the time to elaborate! |
You also need to remember that Effect can yield in order to avoid stack overflows and starving the thread. |
Oh I did not know that, that explains a lot thank you! 🙏 |
In the case of Effect.runSync, it uses a sync scheduler to ensure yields happen synchronously too. |
Oh, I see! That means a That still seems useful, even if it’s only for very specific cases |
Before proceeding, I want to acknowledge that I have read the rationale provided in the documentation regarding the decision not to implement the synchronicity of operations in the type system. For reference, here is the relevant explanation from the documentation:
That said, I have not been able to locate discussions on this topic and would like to reopen the debate. My intention is twofold: first, to gain a deeper understanding of the reasons behind this decision, and second, to explore whether it could be reconsidered if viable solutions are proposed.
Motivations for This Feature
We should not have to rely on internal knowledge of the library to use it safely. One example illustrating this point is the creation of a cache in the global scope. A common approach might look like this:
This works because
Effect.cached
is synchronous. However, the type system does not guarantee this behavior, and an update could just make it asynchronous. If such a change were introduced, the type system would not flag the issue, increasing the likelihood of pushing a bug to production.Regarding the safety concerns mentioned in the documentation, I believe these can be addressed. For instance, the
fromCallback
function could default to being marked as asynchronous, while an alternativefromSyncCallback
function could explicitly handle synchronous callbacks. Users would then be responsible for ensuring that they only usefromSyncCallback
with genuinely synchronous operations. Although misuse would remain possible, such errors would be easier to detect, aligning with existing Effect library conventions—e.g.,promise
vs.tryPromise
:Similarly, the documentation for a synchronous variant could state:
Proposal to Address Complexity
I have experimented with a method for marking asynchronous effects that, in my view, neither increases library complexity nor compromises composability. However, I recognize that I may have overlooked some aspects, so I welcome feedback. The approach involves introducing an async service to mark asynchronous effects:
I apologize if this specific approach has already been considered and rejected for reasons I have not yet encountered. Nonetheless, I am genuinely interested in hearing thoughts on this proposal. If there are compelling arguments against this approach, I would greatly appreciate the opportunity to learn from them. Thank you for your time!
The text was updated successfully, but these errors were encountered: