Select types at runtime

Hi,

given the following function signature

pub fn process(S: type, T: type, samples: [] const S) void {
   //process samples using T as result type
}

and an application, where the user can choose among a set of types for both S and T independently through command line arguments. What options are available to select the types at runtime?

At the moment I have two enums that provide the possible options the user can choose for both types, e.g. like these:

const SampleType = enum { i16, i32};
const OutputType = enum { f32, f64};

and then have nested switches in function wrappers

pub SwitchOnInputType(s: InputType, t: OutputType, samples: [] const S) void {
    switch(s) {
        .i16 => SwitchOnOutputType(i16, t, samples),
        .i32 => SwitchOnOutputType(i32, t, samples),
}

pub SwitchOnOutputType(S: type, t: OutputType, samples: [] const S) void {
    switch(t) {
        .f32 => process(S, f32, samples),
        .f64 => process(S, f64, samples),
    }
}

How would you implement this? What are possible pros/cons?

EDIT: somehow I messed up the code sample for the function wrappers.

Use a tagged union ! You will need to specify all the types in advance and give each type an id. Then you can use switch with the tag names instead of raw types.
Relevant section of the langage reference: Documentation - The Zig Programming Language

technically? none. parameters of type type are always comptime. when these functions are compiled, a new version of the function is instantiated for each distinct comptime argument, and that “monomorphized” function contains the code that is actually run.

having satisfied my desire to be technically correct (the worst kind of correct?) i’ll say that there are very many. a tagged union is a great option, as @Etienne_Parmentier mentions. other kinds of runtime polymorphism are also possible, but if you need to reach for them, you’ll know.

1 Like

I can give the worst answer, std.json.Value :smile:

4 Likes

Guess what OP wants

const std = @import("std");
const SampleType = enum { i16, i32 };
const Samples = union(SampleType) {
    i16: []const i16,
    i32: []const i32,
};

const OutputType = enum {
    f32,
    f64,
    pub fn T(comptime self: OutputType) type {
        return switch (self) {
            .f32 => f32,
            .f64 => f64,
        };
    }
};

pub fn process(comptime S: type, comptime T: type, samples: []const S) void {
    //process samples using T as result type
    std.debug.print("{s}{s}", .{ @typeName(S), @typeName(T) });
    _ = samples;
}

pub fn switchOnOutputType(samples: Samples, t: OutputType) void {
    switch (t) {
        inline else => |ctt| {
            const T = ctt.T();
            switch (samples) {
                .i16 => process(i16, T, samples.i16),
                .i32 => process(i32, T, samples.i32),
            }
        },
    }
}

test switchOnOutputType {
    const raw = [_]i16{ -1, 0, 1, 2 };
    var samples: Samples = .{ .i16 = &raw };
    var t: OutputType = .f32;
    _ = &samples;
    _ = &t;
    switchOnOutputType(samples, t);
}

ok ok, you won! :grinning_face_with_smiling_eyes:

Thanks, @Etienne_Parmentier and @npc1054657282 ! I know tagged unions but did not consider them here. I will think about it.

Using a tagged union here can simplify the case, but what’s more noteworthy is the use of inline else, which successfully converts OutputType from a runtime value to a comptime value.
Of course, doing this often comes with code bloat. If there are many possible types, it is best to think more about how to simplify the situation.