Ir al contenido principal

Funciones en Zig

Las funciones son bloques de código reutilizables que realizan tareas específicas. En Zig, las funciones ofrecen una gran flexibilidad y potencia, permitiendo desarrollar código modular, eficiente y seguro. Este artículo explorará las características de las funciones en Zig, desde lo básico hasta conceptos más avanzados.

Declaración de funciones básicas

Para declarar una función en Zig, se utiliza la palabra clave fn seguida del nombre de la función, los parámetros entre paréntesis y el tipo devuelto:

const std = @import("std");

fn sumar(a: i32, b: i32) i32 {
    return a + b;
}

test "función básica" {
    const resultado = sumar(5, 3);
    try std.testing.expect(resultado == 8);
}

Si una función no devuelve ningún valor, se utiliza void como tipo de retorno:

fn imprimirSaludo() void {
    std.debug.print("Hola, mundo!\n", .{});
}

Parámetros de una función

Los parámetros de una función en Zig son inmutables por defecto. Si necesitas modificar un parámetro dentro de la función, debes pasarlo como puntero:

fn incrementar(valor: *i32) void {
    valor.* += 1;
}

test "modificar parámetro por referencia" {
    var x: i32 = 10;
    incrementar(&x);
    try std.testing.expect(x == 11);
}

Retorno de valores

En Zig, una función puede devolver cualquier tipo:

fn obtenerMayor(a: i32, b: i32) i32 {
    if (a > b) return a;
    return b;
}

fn esPositivo(numero: i32) bool {
    return numero > 0;
}

También es posible devolver múltiples valores usando estructuras o tuplas:

fn divMod(a: i32, b: i32) struct { cociente: i32, resto: i32 } {
    return .{
        .cociente = @divTrunc(a, b),
        .resto = @rem(a, b),
    };
}

test "retorno de múltiples valores" {
    const resultado = divMod(10, 3);
    try std.testing.expect(resultado.cociente == 3);
    try std.testing.expect(resultado.resto == 1);
}

Funciones con errores

Zig soporta el manejo de errores a través de tipos de unión de error. Una función que puede fallar retorna un tipo de unión de error, indicado por !:

fn dividir(a: f32, b: f32) !f32 {
    if (b == 0) return error.DivisionPorCero;
    return a / b;
}

test "función con manejo de errores" {
    // Probar caso exitoso
    const resultado = try dividir(10, 2);
    try std.testing.expect(resultado == 5);

    // Probar captura de error
    _ = dividir(10, 0) catch |err| {
        try std.testing.expect(err == error.DivisionPorCero);
        return;
    };

    // Si llegamos aquí, es porque no se capturó el error esperado
    unreachable;
}

Parámetros de tiempo de compilación

Zig permite definir parámetros que se resuelven en tiempo de compilación usando la palabra clave comptime

fn max(comptime T: type, a: T, b: T) T {
    if (a > b) return a;
    return b;
}

test "función con parámetro comptime" {
    const enteroMayor = max(i32, 10, 20);
    try std.testing.expect(enteroMayor == 20);
    
    const flotanteMayor = max(f32, 10.5, 20.5);
    try std.testing.expect(flotanteMayor == 20.5);
}

Este es un ejemplo de "programación genérica" en Zig, donde la función puede trabajar con cualquier tipo que cumpla los requisitos (en este caso, que soporte el operador >).

Inferencia de tipo en los parámetros

Zig permite declarar parámetros con anytype, lo que hace que el tipo se infiera en el momento de la llamada:

fn cuadrado(valor: anytype) @TypeOf(valor) {
    return valor * valor;
}

test "inferencia de tipo de parámetro" {
    const entero = cuadrado(5);
    try std.testing.expect(@TypeOf(entero) == comptime_int);
    try std.testing.expect(entero == 25);

    const flotante = cuadrado(5.0);
    try std.testing.expect(@TypeOf(flotante) == comptime_float);
    try std.testing.expect(flotante == 25.0);
}

Punteros a funciones

En Zig, puedes guardar y pasar referencias a funciones: 

fn sumar(a: i32, b: i32) i32 {
    return a + b;
}

fn restar(a: i32, b: i32) i32 {
    return a - b;
}

fn aplicarOperacion(a: i32, b: i32, operacion: *const fn (i32, i32) i32) i32 {
    return operacion(a, b);
}

test "punteros a funciones" {
    try std.testing.expect(aplicarOperacion(5, 3, sumar) == 8);
    try std.testing.expect(aplicarOperacion(5, 3, restar) == 2);
}

Atributos de funciones

Zig proporciona varios modificadores para funciones:

Export

El modificador export hace que una función sea visible para otros módulos o para código C externo:

export fn sumarExportada(a: i32, b: i32) i32 {
    return a + b;
}

Extern

La palabra clave extern declara una función que será resuelta en tiempo de ejecución:

extern "c" fn printf(format: [*:0]const u8, ...) c_int;

Inline

El modificador inline fuerza a que una función sea insertada en lugar de llamada:

inline fn doble(x: i32) i32 {
    return x * 2;
}

Funciones variádicas

Zig soporta funciones con número un variable de argumentos:

fn sumarTodos(args: []const i32) i32 {
    var suma: i32 = 0;
    for (args) |arg| {
        suma += arg;
    }
    return suma;
}

test "función variádica" {
    const numeros = [_]i32{1, 2, 3, 4, 5};
    try std.testing.expect(sumarTodos(&numeros) == 15);
}

Convenciones de llamada

Zig permite especificar la convención de llamada de una función, lo que es útil para la interoperabilidad con otros lenguajes:

fn funcionNormal() void {}

fn funcionStdcall() callconv(.Stdcall) void {}

fn funcionC() callconv(.C) void {}

Recursión

Las funciones en Zig pueden ser recursivas, llamándose a sí mismas: 

fn factorial(n: u64) u64 {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

test "función recursiva" {
    try std.testing.expect(factorial(5) == 120);
}

Funciones con tipos genéricos

Una característica poderosa de Zig es la capacidad de crear funciones con tipos genéricos:

fn Lista(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,
        
        pub fn init(items: []T) @This() {
            return .{
                .items = items,
                .len = items.len,
            };
        }
        
        pub fn get(self: @This(), index: usize) ?T {
            if (index >= self.len) return null;
            return self.items[index];
        }
    };
}

test "función con tipos genéricos" {
    var numeros = [_]i32{1, 2, 3, 4};
    var lista = Lista(i32).init(&numeros);
    
    try std.testing.expect(lista.len == 4);
    try std.testing.expect(lista.get(2).? == 3);
    try std.testing.expect(lista.get(5) == null);
}