I bought that as Zig's M.O., but then came the automatic "async/IO" colorless functions which is a huge chunk of implicit and hidden logic.
That's more implicit logic than what's in say JavaScript async AFAICT. Arbitrary int sizes with implicit casting rules, etc. More recently was the attempts of memory safety via an some sort of (optional) implicit GC mode. Personally I don't get it.
[–] Mond_ 2 days ago
[–] simonask 1 day ago
This has a lot of implications, especially for function pointers and other forms of dynamic dispatch.
My personal take is that async code is fundamentally different from sync code, because the weights of various tradeoffs change dramatically. In my view, "colorless" functions are not really a worthwhile goal in the first place. But I've been wrong before, so it may turn out to be great, we'll see.
[–] lukaslalinsky 1 day ago
[–] elcritch 1 day ago
Yeah so that's a lot of hidden magic and implicit logic. ¯\_(ツ)_/¯
[–] Zambyte 1 day ago
[–] tialaramex 1 day ago
If you look at how it's implemented you can see that the classic "XOR a register with itself to zero that register" trick is in fact basically free in a modern CPU rather than using up the ALU because of how register renaming is implemented. But hopefully we're not pretending that "You could buy the CPU vendor's technical manuals" makes this now explicit.
[–] Zambyte 1 day ago
[–] lukaslalinsky 1 day ago
[–] imtringued 1 day ago
const std = @import("std");
fn performTasks(io: std.Io) !void {
var group: std.Io.Group = .init;
errdefer group.cancel(io);
// Launch concurrent background jobs
try group.async(io, simulateWorker, .{ io, 1 });
try group.async(io, simulateWorker, .{ io, 2 });
// Wait for all group tasks to complete
try group.await(io);
}
fn simulateWorker(io: std.Io, id: usize) void {
std.log.info("Worker {d} started...", .{id});
// Any blocking operation (like sleeping) routes through 'io'
// Under the hood, this will either block an OS thread or yield an event loop stack.
io.sleep(.fromSeconds(1), .awake) catch {};
std.log.info("Worker {d} finished!", .{id});
}
pub fn main() !void {
var gpa_alloc = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa_alloc.deinit();
const allocator = gpa_alloc.allocator();
// Initialize the Threaded I/O backend
var threaded = std.Io.Threaded.init(allocator, .{
.async_limit = std.Io.Limit.limited(4), // Max concurrent tasks in the pool
});
defer threaded.deinit();
// Extract the generic I/O interface pointer
const io = threaded.io();
std.log.info("Running on Threaded backend:", .{});
try performTasks(io);
}
pub fn main() !void {
var gpa_alloc = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa_alloc.deinit();
const allocator = gpa_alloc.allocator();
// Initialize the Evented (non-blocking) backend
var evented: std.Io.Evented = undefined;
try evented.init(allocator, .{
.thread_limit = 4, // Worker threads running the event loop
});
defer evented.deinit();
// Extract the exact same generic interface
const io = evented.io();
std.log.info("Running on Evented backend:", .{});
try performTasks(io);
}
std.Io.Threaded and std.Io.Evented are two types of IO backends. std.Io.Threaded uses blocking IO primitives and Evented uses asynchronous IO primitives. Switching between backends is done via dynamic dispatch, the used backend is passed in via an explicit function parameter.The only real question you should have is, how does the "non-blocking" case work when the interface is still a synchronous call? Well, the answer is quite boring... You just call into the scheduler and it runs the other task inside the blocked operation.
I was promised hidden magic and implicit logic! Now I want a refund!
The async design changed multiple times, and the current approach is that of requiring passing an I/O interface that encapsulates the logic, same approach as for passing around an allocator.
This effectively makes all I/O bound code "quasi-async", and it's all explicit and the caller is in control.