Edited: Thanks to everyone who has contributed to this idea. After a lot of collaboration, this is the version we’re currently on:
- Using this structure, you can tie together multiple functions under one call.
- Can tie together functions with different names and numbers of arguments.
- Functions do not need to have the same or common return types.
- The call is automatically dispatched based on the set of arguments.
Using the OverloadSet
:
const std = @import("std");
// our three functions to combine into an overload set
pub fn sqr16(x: i16) i16 { return x * x; }
pub fn sqr32(x: i32) i32 { return x * x; }
pub fn sqr64(x: i64) i64 { return x * x; }
// make the overload set
const sqr = OverloadSet(.{ sqr16, sqr32, sqr64 });
pub fn main() !void {
const x: i32 = 42;
const y: i32 = sqr.call(.{ x });
std.debug.print("\nResult: {}\n\n", .{ y });
}
@Sze’s version with Implicit Conversion control: Overloaded Function Sets Refactored + TypeConverters + implicitTypeConverter + implicitArrayToSlice + implicitComptimeInt · GitHub
Source Code:
pub fn isTuple(comptime T: type) bool {
return switch (@typeInfo(T)) {
.Struct => |s| s.is_tuple,
else => false,
};
}
fn detectOverloadError(args: anytype) ?[]const u8 {
inline for (args) |arg| {
switch (@typeInfo(@TypeOf(arg))) {
.Fn => {},
else => {
return "Non-function argument in overload set.";
},
}
}
inline for (args) |arg| {
for (@typeInfo(@TypeOf(arg)).Fn.params) |param| {
if (param.type == null) {
return "Generic parameter types in overload set.";
}
}
}
inline for (0..args.len) |i| {
const params0 = @typeInfo(@TypeOf(args[i])).Fn.params;
inline for (i + 1..args.len) |j| {
const params1 = @typeInfo(@TypeOf(args[j])).Fn.params;
const signatures_are_identical = params0.len == params1.len and
for (params0, params1) |param0, param1|
{
if (param0.type != param1.type) {
break false;
}
} else true;
if (signatures_are_identical) {
return "Identical function signatures in overload set.";
}
}
}
return null;
}
pub fn OverloadSet(comptime functions: anytype) type {
if (comptime detectOverloadError(functions)) |error_message| {
@compileError(error_message);
}
return struct {
fn findMatchingFunctionIndex(comptime args_type: type) comptime_int {
const args_fields = @typeInfo(args_type).Struct.fields;
inline for (functions, 0..) |function, i| {
const function_type_info = @typeInfo(@TypeOf(function)).Fn;
const params = function_type_info.params;
const match = params.len == args_fields.len and
inline for (params, args_fields) |param, field| {
if (param.type.? != field.type) break false;
}
else true;
if (match) return i;
}
return -1;
}
fn candidatesMessage() []const u8 {
var msg: []const u8 = "";
inline for (functions) |f| {
msg = msg ++ " " ++ @typeName(@TypeOf(f)) ++ "\n";
}
return msg;
}
fn formatArguments(comptime args_type: type) []const u8 {
const params = @typeInfo(args_type).Struct.fields;
var msg: []const u8 = "{ ";
inline for (params, 0..) |arg, i| {
msg = msg ++ @typeName(arg.type) ++ if (i < params.len - 1) ", " else "";
}
return msg ++ " }";
}
fn OverloadSetReturnType(comptime args_type: type) type {
const NoMatchingOverload = struct { };
if (comptime !isTuple(args_type)) {
return NoMatchingOverload;
}
const function_index = comptime findMatchingFunctionIndex(args_type);
if (comptime function_index < 0) {
return NoMatchingOverload;
}
const function = functions[function_index];
return @typeInfo(@TypeOf(function)).Fn.return_type.?;
}
pub fn call(args: anytype) OverloadSetReturnType(@TypeOf(args)) {
const args_type = @TypeOf(args);
if (comptime !isTuple(args_type)) {
@compileError("OverloadSet's call argument must be a tuple.");
}
const function_index = comptime findMatchingFunctionIndex(args_type);
if (comptime function_index < 0) {
@compileError("No overload for " ++ formatArguments(args_type) ++ "\n" ++ "Candidates are:\n" ++ candidatesMessage());
}
const function = functions[function_index];
return @call(.always_inline, function, args);
}
};
}
Here’s where the original post begins.
I’m playing around with different dispatch mechanisms and came across this interesting example. This trick allows us to generate structs using comptime that act like overload sets.
The idea is if I create N
different functions, can I generate an object that will dispatch to them automatically using a single call
function? Surprisingly, yes - I have only made this example for unary functions, but I’m sure it could be extended.
Ultimately, the end product of this is used like so:
// our three functions to combine into an overload set
pub fn sqr16(x: i16) i16 {
return x * x;
}
pub fn sqr32(x: i32) i32 {
return x * x;
}
pub fn sqr64(x: i64) i64 {
return x * x;
}
// make the overload set
const sqr = OverloadSet(.{ sqr16, sqr32, sqr64 }){ };
pub fn main() !void {
const x: i32 = 42;
std.debug.print("\nResult: {}\n\n", .{ sqr.call(x) });
}
Here’s the code for the OverloadSet:
// turn the first argument into a name for generating unique unary functions members
fn argTypeName(func: anytype) []const u8 {
return @typeName(@typeInfo(@TypeOf(func)).Fn.params[0].type.?);
}
pub fn OverloadSet(comptime args: anytype) type {
comptime var fields: [args.len]std.builtin.Type.StructField = undefined;
// Create a field for each function in the tuple
inline for (0..args.len) |i| {
fields[i] = .{
.name = "func_" ++ argTypeName(args[i]),
.type = @TypeOf(args[i]),
.default_value = args[i],
.is_comptime = true,
.alignment = 0,
};
}
// Create an internal struct type with the function fields
const Internal = @Type(.{
.Struct = .{
.layout = .Auto,
.fields = fields[0..],
.decls = &.{},
.is_tuple = false,
.backing_integer = null
},
});
// Return a wrapper struct with a call function
return struct {
internal: Internal = .{ },
pub fn call(comptime self: @This(), x: anytype) @TypeOf(x) {
return @field(self.internal, "func_" ++ @typeName(@TypeOf(x)))(x);
}
};
}
This is actually more generic than name overloading by itself - you can combine any functions into a set that have different parameter types.