Recursión genérica

Recursión genérica

A ratos, Manitas venía al taller del inventor y se autoreparaba. Al principio, Juanelo le “curaba” alguna avería menor, pero con el tiempo el autómata aprendió a reparar, e incluso cambiar sus propias piezas, gastadas por tanto esfuerzo y trabajo incesante. Juanelo le preparaba los materiales para ello, así que Manitas tenía todos los repuestos para recomponerse una y otra vez cuando hiciera falta. El inventor, hasta cariño le tenía a su creación. “Caro amico Manite” decía. Era como un aprendiz que le quitaba mucho trabajo para que él pudiera seguir con los demás encargos…

repair.zig

const std = @import("std");

const print = std.debug.print;

const Stats = struct {

    // asignamos un tipo a su valor máximo

    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 };

// función genérica repara cualquier parte

fn repair(p_item: anytype) void {

    const T = @TypeOf(p_item.*);

    const o_tinfo = @typeInfo(T);

    // si es array iteramos los hijos

    if (o_tinfo == .array) {

        if (p_item.len == 0) return;

        for (p_item) |*o_item| {

            switch (o_item.*) {

                // sin inline el compilador crea un único else

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

                // capturamos el puntero al valor

            }

        }

    } 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} tiene avería, está a: {}pts, Manitas lo repara.\n", .{

                @typeName(T),

                p_item.*.o_stats.n_health,

            });

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

        }

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

            // llamada recursiva para arreglar los hijos

            repair(&p_item.a_children);

        }

    } else {

        print("{any} no es una parte reparable\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;

    // reparación genérica

    repair(&o_right_hand);

    repair(&o_proyectile);

}

$ zig run repair.zig

repair.Hand tiene avería, está a: 60pts, Manitas lo repara.

repair.Finger tiene avería, está a: 10pts, Manitas lo repara.

repair.Proyectile no es una parte reparable

En este ejemplo hemos visto cómo la función genérica repair es capaz de reparar cualquier pieza identificada por el campo o_stats. Hacemos un descenso recursivo, pieza por pieza, detectando el campo a_children y vamos reparando aquellos hijos que estén dañados.

Si el autómata por alguna razón se confunde e intenta reparar algo como un proyectil que sujetaba en una mano, se da cuenta de que no es reparable.

Composición de structs

Podemos componer un struct de manera dinámica en tiempo de compilación, usando otros structs previamente definidos, como en el caso de Stats, que usamos para definir el campo o_stats de Finger, HPalm, Hand.

  const Stats = struct {

    // asignamos un tipo a su valor máximo

    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{} };

Usamos las funciones builtin para obtener el tipo como T y la información del tipo:

  const T = @TypeOf(p_item.*);

  const o_tinfo = @typeInfo(T);

Algunas partes del autómata están compuestas de otras, definidas en a_children. Por ejemplo, la mano incluye partes como Finger y HPalm (palma de la mano). Para definir de manera estricta qué tipo de elementos se incluyen en a_children usamos el union HandParts: una posición del array sólo puede ser o dedo o palma de la mano:

  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{} },

    },

  };

El programa representa la reparación de una mano dañada, que tiene un daño general y un dedo bastante averiado, ya quedan solo 10 puntos de health de los posibles 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;

    // reparación genérica

    repair(&o_right_hand);

    repair(&o_proyectile);

El autómata intenta reparar la mano derecha o_right_hand y o_proyectile que tenía en la mano. Ambos objetos son variables porque la función modificará sus valores de n_health. Como queremos hacer cambios sobre los datos originales, pasamos su dirección de memoria como parámetro usando &.

La función repair es una función genérica y recursiva. Acepta cualquier tipo declarando su tipo como anytype. Usamos las siguientes funciones builtin para trabajar con ese dato:

- @TypeOf para obtener el tipo T

- @typeInfo para obtener información estructurada sobre el tipo T

- @hasField para detectar si el objeto tiene el campo a_children

Si el objeto tiene el campo a_children, que contiene elementos hijos, la función repair volverá  a llamarse a sí misma usando esos hijos como parámetro:

 

  fn repair(p_item: anytype) void {

    const T = @TypeOf(p_item.*);

    const o_tinfo = @typeInfo(T);

    // ...

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

      // llamada recursiva para arreglar los hijos

      repair(&p_item.a_children);

    }

Ya hemos visto en el ejemplo previo que o_tinfo es un union y tendrá activo .array cuando la función reciba ese array de hijos. Si lo recibe, iteramos sobre ellos, enviando uno por uno a la misma función.

  // si es array iteramos los hijos

  if (o_tinfo == .array) {

    for (p_item) |*o_item| {

      switch (o_item.*) {

        // sin inline el compilador crea un único else

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

          // capturamos el puntero al valor

El compilador usará todo este código fuente como plantilla para construir versiones de nuestra función para cada tipo que recibe: una para cuando p_item es un array de HandParts, otra si es Finger, otra para HPalm y otra si es Hand.  También hay que tener en cuenta que cuando iteramos un array de HandParts y pasamos por switch para detectar de qué tipo de dato se trata (.finger o .palm) tenemos dos posibilidades: o ponemos a mano cada caso o tenemos que poner inline else en vez de else. Si no usamos inline, el else se será un único caso genérico y no funcionará correctamente. Con inline el compilador crea los casos separados para cada variante.

Funciones Anónimas y Encapsuladas
Comprobar si existen campos con @hasField
© 2025 Zen of Zig