Generic Recursion

for Zig 0.15.2 Buy

Generic Recursion

From time to time, Manitas would come to the inventor’s workshop and repair himself. At first, Juanelo would “heal” minor malfunctions, but over time, the automaton learned to fix and even replace his own parts, worn out by the relentless effort. Juanelo would prepare the materials for him, so Manitas had all the spare parts he needed to rebuild himself whenever necessary. The inventor had even grown fond of his creation. “Caro amico Manite,” he would say. It was like having an apprentice who took a lot of work off his hands so he could focus on the other commissions…

repair.zig

const std = @import("std");

const print = std.debug.print;

const Stats = struct {

    // assign a type to its max value

    n_health: u7 = std.math.maxInt(u7), //... n_strength etc

};

const Finger = struct { o_stats: Stats = Stats{} };

const HPalm = struct { o_stats: Stats = Stats{} };

const HandParts = union(enum) { finger: Finger, palm: HPalm };

const Hand = struct {

    o_stats: Stats = Stats{},

    b_right: bool = false,

    a_children: [3]HandParts = .{

        .{ .finger = Finger{} },

        .{ .finger = Finger{} },

        .{ .palm = HPalm{} },

    },

};

const Proyectile = enum { Wood, Rock };

// generic function repairs any part

fn repair(p_item: anytype) void {

    const T = @TypeOf(p_item.*);

    const o_tinfo = @typeInfo(T);

    // if it's an array, iterate over children

    if (o_tinfo == .array) {

        if (p_item.len == 0) return;

        for (p_item) |*o_item| {

            switch (o_item.*) {

                // without inline, the compiler creates a single else

                inline else => |*o_val| repair(o_val),

                // capture pointer to value

            }

        }

    } else if (@hasField(T, "o_stats")) {

        const n_current_health = p_item.*.o_stats.n_health;

        if (n_current_health < std.math.maxInt(@TypeOf(n_current_health))) {

          print("{s} is damaged, at: {}pts, Manitas repairs it.\n", .{

                @typeName(T),

                p_item.*.o_stats.n_health,

            });

            p_item.*.o_stats.n_health +|= 100;

        }

        if (@hasField(T, "a_children")) {

            // recursive call to repair children

            repair(&p_item.a_children);

        }

    } else {

        print("{any} is not a repairable part\n", .{T});

    }

}

pub fn main() !void {

    var o_right_hand = Hand{

        .o_stats = Stats{

            .n_health = 60,

        },

        .b_right = true,

    };

    o_right_hand.a_children[1]

        .finger.o_stats.n_health = 10;

    var o_proyectile = Proyectile.Rock;

    // generic repair

    repair(&o_right_hand);

    repair(&o_proyectile);

}

$ zig run repair.zig

repair.Hand is damaged, at: 60pts, Manitas repairs it.

repair.Finger is damaged, at: 10pts, Manitas repairs it.

repair.Proyectile is not a repairable part

In this example, we've seen how the generic repair function can fix any part identified by the o_stats field. We perform a recursive descent, piece by piece, detecting the a_children field and repairing any damaged children.

If the automaton, for some reason, gets confused and tries to repair something like a projectile it's holding in its hand, it realizes it's not repairable.

Struct Composition

We can compose a struct dynamically at compile time using other previously defined structs, like in the case of Stats, which we use to define the o_stats field of Finger, HPalm, and Hand.

  const Stats = struct {

    // assign a type to its maximum value

    n_health: u7 = std.math.maxInt(u7), //... n_strength etc

  };

  const Finger = struct { o_stats: Stats = Stats{} };

  const HPalm = struct { o_stats: Stats = Stats{} };

  const Hand = struct { o_stats: Stats = Stats{} };

We use builtin functions to get the type as T and the type info:

  const T = @TypeOf(p_item.*);

  const o_tinfo = @typeInfo(T);

Some parts of the automaton are composed of others, defined in a_children. For example, the hand includes parts like Finger and HPalm (the palm of the hand). To strictly define what types of elements can be included in a_children, we use the union HandParts: each position in the array can only be either a finger or a palm:

  const HandParts = union(enum) { finger: Finger, palm: HPalm };

  const Hand = struct {

    o_stats: Stats = Stats{},

    b_right: bool = false,

    a_children: [3]HandParts = .{

      .{ .finger = Finger{} },

      .{ .finger = Finger{} },

      .{ .palm = HPalm{} },

    },

  };

The program represents the repair of a damaged hand, which has general damage and a finger that’s badly worn out, with only 10 health points left out of a possible 127 (n_health: u7 = std.math.maxInt(u7)):

 

  var o_right_hand = Hand{

    .o_stats = Stats{

      .n_health = 60,

    },

    // ...

   .finger.o_stats.n_health = 10;

    // generic repair

    repair(&o_right_hand);

    repair(&o_proyectile);

The automaton tries to repair the right hand o_right_hand and the o_proyectile it was holding. Both objects are variables because the function will modify their n_health values. Since we want to modify the original data, we pass the memory address of the original data as a parameter using &.

The repair function is generic and recursive. It accepts any type by declaring its parameter as anytype.

We use the following builtin functions to work with that value:

- @TypeOf to get the type T

- @typeInfo to get structured information about type T

- @hasField to check if the object has the a_children field

If the object has the a_children field, which contains child elements, the repair function will call itself again using those children as the parameter:

 

  fn repair(p_item: anytype) void {

    const T = @TypeOf(p_item.*);

    const o_tinfo = @typeInfo(T);

    // ...

    if (@hasField(T, "a_children")) {

      // recursive call to repair the children

      repair(&p_item.a_children);

    }

As we saw in the previous example, o_tinfo is a union, and it will have .array active when the function receives that array of children. If it does, we iterate over them and send each to the same function.

  // if it’s an array, iterate over the children

  if (o_tinfo == .array) {

    for (p_item) |*o_item| {

      switch (o_item.*) {

        // without inline, the compiler creates a single else

        inline else => |*o_val| repair(o_val),

          // capture the pointer to the value

The compiler will use this entire source code as a template to generate versions of our function for each type it receives: one for when p_item is an array of HandParts, another if it’s Finger, another for HPalm, and another for Hand.

Also, keep in mind that when iterating over an array of HandParts and passing through a switch to detect which type of data we’re dealing with (.finger or .palm), we have two options: either write each case manually, or use an inline else instead of else. If we don’t use inline, the else becomes a single generic case and won’t work properly. With inline, the compiler generates separate cases for each variant.

Anonymous and Encapsulated Functions
Checking for Fields with @hasField
© 2025 - 2026 Zen of Zig