Skip to content

Latest commit

 

History

History
172 lines (157 loc) · 4.73 KB

File metadata and controls

172 lines (157 loc) · 4.73 KB

Why Zimpl?

Suppose that we have a function to poll for events in a Server struct that manages TCP connections. The caller passes in a generic handler argument to provide event callbacks.

const Server = struct {
    // ...
    pub fn poll(self: *@This(), handler: anytype) !void {
        try self.pollSockets();
        while (self.getEvent()) |evt| {
            switch (evt) {
                .open => |handle| handler.onOpen(handle),
                .msg => |msg| handler.onMessage(msg.handle, msg.bytes),
                .close => |handle| handler.onClose(handle),
            }
        }
    }
    // ...
};
// using the server library
var server = Server{};
var handler = MyHandler{};
try server.listen(8080);
while (true) {
    try server.poll(&handler);
}

A complaint with the above implementation is that it is not clear from the function signature of poll, or even the full implementation, what the exact requirements are for the handler argument.

A guaranteed way to make requirements clear is to never rely on duck typing and have the caller pass in the handler callback functions directly.

const Server = struct {
    // ...
    pub fn poll(
        self: *@This(),
        handler: anytype, 
        comptime onOpen: fn (@TypeOf(handler), Handle) void,
        comptime onMessage: fn (@TypeOf(handler), Handle, []const u8) void,
        comptime onClose: fn (@TypeOf(handler), Handle) void
    ) void {
        try self.pollSockets();
        while (self.getEvent()) |evt| {
            switch (evt) {
                .open => |handle| onOpen(handler, handle),
                .msg => |msg| onMessage(handler, msg.handle, msg.bytes),
                .close => |handle| onClose(handler, handle),
            }
        }
    }
    // ...
};
// using the server library
var server = Server{};
var handler = MyHandler{};
try server.listen(8080);
while (true) {
    try server.poll(
        &handler,
        MyHandler.onOpen,
        MyHandler.onMessage,
        MyHandler.onClose
    );
}

Unfortunately, now the function signature is long and the call site is verbose. Additionally, if we need to take a handler somewhere else, then the callback parameters will need to be defined again separately.

The logical next step would be to create a struct type to hold all of the callback functions.

const Server = struct {
    // ...
    pub fn Handler(comptime T: type) type {
        return struct {
            onOpen: fn (T, Handle) void,
            onMessage: fn (T, Handle, []const u8) void,
            onClose: fn (T, Handle) void,
        };
    }

    pub fn poll(
        self: *Self,
        handler_ctx: anytype,
        handler_impl: Handler(@TypeOf(handler_ctx)),
    ) void {
        try self.pollSockets();
        while (self.getEvent()) |evt| {
            switch (evt) {
                .open => |handle| handler_impl.onOpen(handler_ctx, handle),
                .msg => |msg| handler_impl.onMessage(handler_ctx, msg.handle, msg.bytes),
                .close => |handle| handler_impl.onClose(handler_ctx, handle),
            }
        }
    }
    // ...
};

This cleaned up the function signature, but calling poll is still needlessly verbose when the type already has valid callback member functions.

// using the server library
var server = Server{};
var handler = MyHandler{};
try server.listen(8080);
while (true) {
    try server.poll(&handler, .{
        .onOpen = MyHandler.onOpen,
        .onMessage = MyHandler.onMessage,
        .onClose = MyHandler.onClose,
    });
}

The Zimpl library provides the function Impl that infers the default value for each member of Handler(T) from the declarations of T.

const Server = struct {
    // ...
    pub fn poll(
        self: *Self,
        handler_ctx: anytype,
        handler_impl: Impl(Handler, @TypeOf(handler_ctx))
    ) void {
        try self.pollSockets();
        while (self.getEvent()) |evt| {
            switch (evt) {
                .open => |handle| handler_impl.onOpen(handler_ctx, handle),
                .msg => |msg| handler_impl.onMessage(handler_ctx, msg.handle, msg.bytes),
                .close => |handle| handler_impl.onClose(handler_ctx, handle),
            }
        }
    }
    // ...
// using the server library
var server = Server{};
var handler = MyHandler{};
try server.listen(8080);
while (true) {
    // Impl(Handler, *MyHandler) can be default constructed because MyHandler
    // has onOpen, onMessage, and onClose member functions
    try server.poll(&handler, .{});
}

For a longer discussion on the above example see this article. Go to the Zimpl page for more examples.