Posted on

Zig Hack: TypedGet

The return type of a Zig function allows arbitrary TypeExpr expressions.

fn byteArray(comptime n: comptime_int) [n]u8 {...}
fn noOp(x: anytype) @TypeOf(x) {...}
fn deref(x: anytype) (switch(@typeInfo(@TypeOf(x))) {
  else => @TypeOf(x),
  .Pointer => |p| p.child,
}) {...}
fn reasonableThing(comptime T: type, a: u22) R: {...} {...}
Enter fullscreen mode Exit fullscreen mode

At some point these expressions get unwieldy, so we really want to encapsulate the return type into a separate function. From std.mem:

fn Span(comptime T: type) type {
    switch (@typeInfo(T)) {
        .Optional => |optional_info| {
            return ?Span(optional_info.child);
        .Pointer => |ptr_info| {...},
        else => {},
    @compileError("invalid type given: " ++ @typeName(T));
pub fn span(ptr: anytype) Span(@TypeOf(ptr)) {...}
Enter fullscreen mode Exit fullscreen mode

"This is good and well,", we say, "for the title case capitalization of the same function name indicates its nature of being the type of the corresponding non- title case entity.
Also it pleases us that callsites can now inquire the result type by calling Span(T) instead of @TypeOf(span(ptr)), or the even more unbecoming @TypeOf(span(@as(T, undefined)))."

Next, when implementing span, we realize that the logic supplying the value needs to match up with the logic supplying the type, and so we restate that logic:

pub fn span(ptr: anytype) Span(@TypeOf(ptr)) {
    if (@typeInfo(@TypeOf(ptr)) == .Optional) {...}
    const Result = Span(@TypeOf(ptr));
    const ptr_info = @typeInfo(Result).Pointer;
Enter fullscreen mode Exit fullscreen mode

Arguably, not a big deal. It's a bit of repeated code, but as good denizens aiming for code clarity, we wouldn't want too complicated logic dictating the types in our program anyway.
And if the two functions ever got out of sync, we'd just get a compile error.
We would certainly never do anything ridiculous with parameterized nested types to represent comptime state, where anonymous struct initializers .{} would silently coerce to any one of them.

And so as a mere curiosum, I present to you the following pattern Zig hack that you shouldn't use in your codebases at home.

/// documentation only:
/// a type coupled with a way to way to calculate it from given inputs
/// @as(TypedGet, opaque {
///   pub const Result: type;
///   pub const get: fn(...) Result;
/// })
pub const TypedGet = type;

/// snake_case because we're technically returning a namespace
pub fn unreasonable_thing_get(
  comptime A: type,
  comptime B: type,
) TypedGet {
  // terribly complicated logic you should really never need
  if (!needThing(A)) return opaque {
    // return type and calculation in one place
    pub const Result = A;
    pub fn get(a: A, _: B) Result {
      return a;
  if (caseConsidered(B, .maybe)) |fallback| return opaque {
    pub const Result = u8;
    pub fn get(a: A, b: B) Result {
      return ...;
  if (@hasDecl(A, "Next")) return opaque {
    // TypedGet composes pretty cleanly imo
    const next_level_get: TypedGet = unreasonable_thing_get(A.Next, B);
    const NextLevelResult = next_level_get.Result;
    const combine_get: TypedGet = A.combine_get(A, NextLevelResult);

    pub const Result = combine_get.Result;
    pub fn get(a: A, b: B) Result {
      return combine_get.get(a, next_level_get.get(a.next(), b));
  @compileError("TODO: handle " ++ @typeName(A) ++ ", " ++ @typeName(B));
pub fn unreasonableThing(a: anytype, b: anytype)
unreasonable_thing_get(@TypeOf(a), @TypeOf(b)).Result {
  return unreasonable_thing_get(@TypeOf(a), @TypeOf(b)).get(a, b);
Enter fullscreen mode Exit fullscreen mode

Essentially, by moving result type and calculation closer to each other, we've achieved an anytype-returning function.
Certainly, this will come back to bite us later.

Top comments (0)