Skip to content

Conversation

jjtolton
Copy link

@jjtolton jjtolton commented Aug 9, 2025

Asynchronous Programming in Scryer Prolog

Motivated by a desire to use Scryer as the command and control center for coordinating multiple external processes, I managed to cobble together some async semantics that I think others will find useful, as they are fairly unobtrusive.

There are only two exposed predicates: async_event_loop/1 and await/1. Goals called with async_event_loop/1 can use await/1 predicate to invoke asynchronous flow control.

I think looking at the testing file provides best overall view of usage, but you can also see the discussions for more flavor.

Feedback welcome.

Edit: to WIP until I figure out the meta predicate declarations

src/lib/async.pl Outdated
:- use_module(library(time)).
main :-
Copy link
Contributor

Choose a reason for hiding this comment

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

Also here and the following cases: I have made good experiences with the name run/0.

Copy link
Author

Choose a reason for hiding this comment

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

652e52c

Thank you!

@triska
Copy link
Contributor

triska commented Aug 10, 2025

First of all, this looks awesome and impressive, thank you so much for working on what appears to quickly become an invaluable contribution!

Second, could you please explain what this exactly is to someone who is not familiar with the used terminology and computational model? I read the documentation, for example this:

%% async_event_loop(+Goals)
% Goals is a list of goals that may be asyncronous, meaning that that `await/1`
% will unify within this context and fail otherwise.

What is "may be asyncronous" (i.e., under what conditions are they asyncronous), and even more urgently: What exactly is "asyncronous"? Only from "meaning that that await/1 will unify within this context and fail otherwise", I could not deduce it. (I suppose "that that" was meant to read "that"?)

The documentation states "asyncronous but not concurrent or parallel", from which I also could not deduce it, but I see that there must be an important difference, what exactly is it? (And is "concurrent" synonymous with "parallel"?) "Coroutine" also occurs in the documentation, exactly once, is that synonymous with any of the other concepts?

The reason why this looks so immensely impressive to me is that at least from the documentation and examples, I get the impression that this may be a way to implement for example an HTTP server that handles different clients, in such a way that a single hanging client does not block other clients, is that the case? If so, then this may vastly reduce the need for multiple threads (#546), where you @matt2xu also already mentioned asynchronous IO in #546 (comment).

@bakaq
Copy link
Contributor

bakaq commented Aug 10, 2025

The usual meaning is:

  • Asynchronous means something that could be run concurrently.
  • Concurrency is running many things in a way that many of them can make progress independently. They don't have to actually run at the same time, you could just switch between them very fast either on a timer (preemptive concurrency, done by the OS on threads for example) or when they yield explicitly (cooperative concurrency, done my most programming languages with async/await or similar).
  • Parallelism is running many things actually at the same time. If your computer has multiple cores, this can be done with threads or processes. Can also be done with multiple computers.

Some key takeaways:

  • Things can be fundamentally asynchronous (like reading 2 different files) but run in a synchronous way. This can happen if your event loop is just sequential.
  • You can run something concurrently without running it in parallel. This can happen if your event loop is single-threaded. That's how Javascript works: it has async/await but it all happens in the same core. Only a single thing is actually being run at a time, but the Javascript event loop switches the tasks it's running so that it feels like they are all running at the same time. Threads in a computer with only one core is the same idea.

I feel that maybe these words are not being used exactly that that in the documentation mentioned. library(cont) can already give us (cooperative) concurrency just like Javascript. For parallelism you do indeed need either threads or processes though.

[...] an HTTP server that handles different clients, in such a way that a single hanging client does not block other clients [...]

This is an example in which you need parallelism, not just concurrency, at least if you use blocking operations. If some task blocks and you only have cooperative concurrency, then the whole program, including all tasks, will freeze because it has to wait to be unblocked before it can continue working on anything. You can also use non-blocking operations and do this with cooperative concurrency just fine, which is why Javascript is a great language for servers even though it's single threaded, but that is much trickier to get right.

Some resources about this:

@jjtolton
Copy link
Author

The reason why this looks so immensely impressive to me is that at least from the documentation and examples, I get the impression that this may be a way to implement for example an HTTP server that handles different clients, in such a way that a single hanging client does not block other clients, is that the case? If so, then this may vastly reduce the need for multiple threads (#546), where you @matt2xu also already mentioned asynchronous IO in #546 (comment).

This is a primary motivating use case -- the only thing we need now is a form of nonblocking I/O, preferably a "sliceable" I/O. Meaning, rather than a timeout, we have a duration. So, get_n_chars/4 would hypothetically bind as many bytes as it could over a number of milliseconds and then return without closing the stream. This would allow the same stream to be harvested periodically -- allowing for web servers at least as powerful as JavaScript, as @bakaq eloquently described, without the need for implementing threads.

Of course, OS-level threads would allow for true concurrency! So, no need for non-blocking I/O!

@jjtolton
Copy link
Author

The usual meaning is:

@bakaq thank you for this, I was struggling with how to succinctly put this in the documentation, I should just copy/paste this in there!

@jjtolton
Copy link
Author

jjtolton commented Aug 11, 2025

First of all, this looks awesome and impressive, thank you so much for working on what appears to quickly become an invaluable contribution!

Second, could you please explain what this exactly is to someone who is not familiar with the used terminology and computational model? I read the documentation, for example this:

%% async_event_loop(+Goals)

% Goals is a list of goals that may be asyncronous, meaning that that `await/1`

% will unify within this context and fail otherwise.

What is "may be asyncronous" (i.e., under what conditions are they asyncronous), and even more urgently: What exactly is "asyncronous"? Only from "meaning that that await/1 will unify within this context and fail otherwise", I could not deduce it. (I suppose "that that" was meant to read "that"?)

Yes, I failed to explain this properly. "may be asynchronous" means that goals run with await/1 that are invoked in the context of async_event_loop/1 will be executed asynchronously rather than synchronously. Otherwise, the code is synchronous.

This is effectively the same as a Python generator.

This can also be achieved with library(cont) but I would consider library(cont) an implementation detail (and a leaky abstraction) of library(async). In the future we may implement this in some other way.

However one major benefit of programming with library(async) over library(cont) is that the logic is written synchronously. If you read a reset/3 you need to scan for the corresponding shift/1 to reason about how the code works. This means it is unwise to use reset/3 often because it can become very difficult to reason about the code. library(async) alleviates the need to do that. If await/1 is invoked, the goal will be scheduled for execution but there is no need to scan elsewhere in the code to reason about what will happen. You can be guaranteed that if the goal succeeds, await/1 will succeed. You do not need to consider whether this is the only async goal or if there are 10
or 100 other async goals, it can be reasoned about in local isolation.

So, the primary use case is probably for side effects, such as reading from and writing to streams. Otherwise the logical result would be the same in all cases as synchronous code, I believe -- this is a strong claim, but I can't figure out how to disprove it. You will notice than in the tests I needed to use mutation with the blackboard to prove that the code is async.

I will consider what level of detail to include in the documentation.

In general, are the documentation comments for Scryer meant to be a full tutorial? What level of experience is assumed? For instance, the clpz documentation is quite comprehensive, but library(cont) assumes you have read the 2 papers.

@triska
Copy link
Contributor

triska commented Aug 11, 2025

In general, are the documentation comments for Scryer meant to be a full tutorial? What level of experience is assumed?

I would say: You have complete freedom regarding shape and extent of the library documentation. Ideally, the combination of existing material (the text itself and external resources) makes clear what the point of the library is and how one can use it.

For instance, the clpz documentation is quite comprehensive, but library(cont) assumes you have read the 2 papers.

The interface of library(clpz) is quite clear, a major goal was compatibility—where possible—with the interface of library(clpfd) of SICStus Prolog which is well documented. It also became available long before any publications.

I think library(cont) appeared together with publications, and if there is a publication, then it may well be better to link directly to the publication if it better explains the context and remaining design questions.

Another reason to keep the documentation minimal and even lacking could be to encourage discussion about an interface that warrants discussion because it is not yet finalized even though the library is already sufficiently useful to collect initial feedback.

@jjtolton
Copy link
Author

jjtolton commented Aug 11, 2025

Looks like I have some meta_predicate issues. I can't tell if this is because of how I wrote the code, how the test_framework module works, how I'm running the tests, or how I wrote the helper functions in the test modules. But the tests do not work unless I module qualify nearly everything. 🤔

This does not seem to be an issue in user code.

@jjtolton jjtolton changed the title library(async) providing async/await semantics [WIP] library(async) providing async/await semantics Aug 11, 2025
@jjtolton jjtolton marked this pull request as draft August 11, 2025 21:12
% the `Goal` will operate as a coroutine. Note that it is asyncronous
% but NOT concurrent or parallel without invoking an external process.

:- meta_predicate(await(+)).
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be 0? The goal takes no additional arguments.

Copy link
Author

Choose a reason for hiding this comment

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

I made the change and I believe this is correct, and I believe user code works with this change, but the test code is behaving strangely now. I am hesitant to jump to conclusions because I usually jump to the wrong conclusion, will need to investigate further when I have time.

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.

4 participants