Ad hoc polymorphism in Zig

6/30/2025
5 min read

Lets say we have a function such as the following.

fn dot(args: anytype) i32 {
    // compute dot product...
}

We want this function to operate on two

Point2D
or two
Point3D
structs. But we do not want to expose 2 different functions for this, i.e something like the following.

pub fn dot2d(p1: Point2D, p2: Point2D) i32 { // ... }

pub fn dot3d(p1: Point3D, p2: Point3D) i32 { // ... }

Rather, we would like our function

dot
to select the correct implementation based on the type and length of
args
. The more technical term for this is "Ad hoc polymorphism". Zig does not support ad hoc polymorphism, but using
comptime
and compile time type reflection, we can sort of mimick this. What we essentially want is a function that takes two arguments, the arguments we are calling our function with, and a list of functions which are our different implementations of this method. The function signature should look like something like this
fn adHocPoly(args: anytype, fns: anytype) fn () typed.ReturnType(fns[0])
typed.ReturnType
is a utility type part of a small library I made, I have provided the implementation below if you want to use it in your own project.
pub inline fn ReturnType(comptime func: anytype) type {
    comptime {
        const type_info: Type = @typeInfo(@TypeOf(func));

        return type_info.@"fn".return_type orelse {
            @compileError("Function has no return type. This should not be possible but is possible due to the current Zig spec.");
        };
    }
}
You might notice a limitation on our function signature, we assume that every implementation has the same return type. Lets lay out what we want this function to do. Given some arguments of any type, we want to pick the function in the list of functions supplied, whose parameters match the arguments, in order, type and length. If no functions parameters is a match, we want to error (specifially compile error). At this point I would recommend the reader to try to implement this function themselves as an exercise, nevertheless, below is the implementation of this function.

pub fn adHocPoly(args: anytype, fns: anytype) fn () typed.ReturnType(fns[0]) {
  const RT = typed.ReturnType(fns[0]);
  for (fns) |func| {
      if (typed.ReturnType(func) != RT) {
          @compileError("all functions must have the same return type");
      }
  }

  const args_ty = @typeInfo(@TypeOf(args));
  switch (args_ty) {
      inline .@"struct" => |s| {
          const fields = s.fields;
          const fns_ty = @typeInfo(@TypeOf(fns));
          switch (fns_ty) {
              inline .@"struct" => |fns_struc| {
                  for (fns_struc.fields, 0..) |func, fn_idx| {
                      const fn_ty = @typeInfo(func.type);
                      switch (fn_ty) {
                          inline .@"fn" => |fn_ty_info| {
                              if (fn_ty_info.params.len != fields.len) {
                                  continue;
                              }

                              var found = true;
                              for (fn_ty_info.params, 0..) |param, i| {
                                  if (param.type) |param_ty| {
                                      if (param_ty != fields[i].type) {
                                          found = false;
                                          break;
                                      }
                                  } else {
                                      @compileError("generic fn parameter of anytype found, can not deduce which fn to call");
                                  }
                              }
                              if (found) {
                                  return (struct {
                                      fn e() RT {
                                          return @call(.auto, fns[fn_idx], args);
                                      }
                                  }).e;
                              }
                          },
                          else => @compileError("functions params was not an array of functions, was " ++ @typeName(@Type(fn_ty))),
                      }
                  }

                  @compileError("no function which fulfills type signature of args was provided");
              },
              else => @compileError("functions was not a struct of fn, was " ++ @typeName(@Type(fns_ty))),
          }
      },
      else => @compileError("expected struct for args, was " ++ @typeName(@Type(args))),
  }
}

Now we can implement our ad hoc polymorphic

dot
function.

const Point2D = struct {
    x: i32,
    y: i32,
};

fn dot2d(p1: Point2D, p2: Point2D) i32 {
    return p1.x * p2.x + p1.y * p2.y;
}

const Point3D = struct {
    x: i32,
    y: i32,
    z: i32,
};

fn dot3d(p1: Point3D, p2: Point3D) i32 {
    return p1.x * p2.x + p1.y * p2.y + p1.z * p2.z;
}

fn dot(args: anytype) i32 {
    return adHocPoly(
        args,
        .{ dot2d, dot3d },
    )();
}

test "ad_hoc_polymorphism" {
    try testing.expectEqual(11, dot(.{
        Point2D{ .x = 1, .y = 2 },
        Point2D{ .x = 3, .y = 4 },
    }));

    try testing.expectEqual(32, dot(.{
        Point3D{ .x = 1, .y = 2, .z = 3 },
        Point3D{ .x = 4, .y = 5, .z = 6 },
    }));
}

Suppose we would like to also support multiple return types as well. To do that we would have to modify the API of our polymorphic function to take the expected return type as an argument.

pub fn adHocPoly(args: anytype, fns: anytype) fn () typed.ReturnType(fns[0]) {
pub fn adHocPoly(comptime T: type, args: anytype, fns: anytype) fn () T {
  const RT = typed.ReturnType(fns[0]);
  for (fns) |func| {
      if (typed.ReturnType(func) != RT) {
          @compileError("all functions must have the same return type");
      }
  }

  const args_ty = @typeInfo(@TypeOf(args));
  switch (args_ty) {
      inline .@"struct" => |s| {
          const fields = s.fields;
          const fns_ty = @typeInfo(@TypeOf(fns));
          switch (fns_ty) {
              inline .@"struct" => |fns_struc| {
                  for (fns_struc.fields, 0..) |func, fn_idx| {
                      const fn_ty = @typeInfo(func.type);
                      switch (fn_ty) {
                          inline .@"fn" => |fn_ty_info| {
                              if (fn_ty_info.params.len != fields.len) {
                                continue;
                              }
                              if (fn_ty_info.return_type) |RT| {
                                if (RT != T) {
                                  continue;
                                }
                              } else {
                                unreachable;
                              }

                              var found = true;
                              for (fn_ty_info.params, 0..) |param, i| {
                                  if (param.type) |param_ty| {
                                      if (param_ty != fields[i].type) {
                                          found = false;
                                          break;
                                      }
                                  } else {
                                      @compileError("generic fn parameter of anytype found, can not deduce which fn to call");
                                  }
                              }
                              if (found) {
                                  return (struct {
                                      fn e() RT {
                                          return @call(.auto, fns[fn_idx], args);
                                      }
                                  }).e;
                              }
                          },
                          else => @compileError("functions params was not an array of functions, was " ++ @typeName(@Type(fn_ty))),
                      }
                  }

                  @compileError("no function which fulfills type signature of args was provided");
              },
              else => @compileError("functions was not a struct of fn, was " ++ @typeName(@Type(fns_ty))),
          }
      },
      else => @compileError("expected struct for args, was " ++ @typeName(@Type(args))),
  }
}

You can find this entire exercise on my github for reference.