How to specialize generic function

I would like to reproduce in Zig a useful C++ template programming pattern of explicit specialization of generic functions.

Let say I have an enum const State = enum{A,B,C}; and a generic function fn foo(comptime state: State, x: i32) State {…}.

I would like to define by hand an instantiation of foo for state==B. Is it even possible? And if yes than what is the most idiomaatic way to do so?

When you say specialize, do you mean keeping the same name and then dispatching to a special version of it? It sounds like you’re talking about a form of overloading (which in C++ is natively supported).

In your case, you could do:

const State = enum{ A, B, C };

fn foo(comptime state: State, x: i32) State {
    switch(state) {
        // handle code here
    }
}

The issue here is that any individual state isn’t a type, it’s a value.

So, if you wanted to further, you could make specialized instantiations of functions ala “Tag Dispatch”…

const State = enum { A, B, C };

pub fn Dispatch(comptime state: State) type {
    return struct {
        pub fn call(x: i32) void { 
            return inner(state, x);
        }
        fn inner(comptime s: State, x: i32) void {
            switch (s) {
                .A => std.debug.print("\nState: {}, Value: {}", .{ s, x }),
                .B => std.debug.print("\nState: {}, Value: {}", .{ s, x }),
                .C => std.debug.print("\nState: {}, Value: {}", .{ s, x }),
            }
        }
    };
}

pub fn main() !void {
    // now specialized for only state B
    const call = Dispatch(State.B).call;

    call(42);
}

One thing this gains is the ability to define a function above a bunch of call sites. Say foo returned a number instead of a state…

const foo = Dispatch(state).call; // you could parameterize this

// use foo a bunch without having to specify the state each time.
const y = foo(foo(42) + foo(43));

// instead of...
const y = foo(state, foo(state, 42) + foo(state, 43));

Which could be handy in cleaning things up. Maybe could make things more maintainable under certain circumstances? The interface is a little restrictive, and you could make this a lot more complicated, but if you know your other parameters and your return type then a restrictive interface isn’t necessarily a bad thing.

If you took in a second parameter, call it func, you could bind func’s first argument instead of inner. So…

// func's type can be specified more thorougly here...
pub fn Dispatch(comptime state: State, func: anytype) type {
    return struct {
        pub fn call(x: i32) State { 
            return func(state, x);
        }
    };
}

I’m not sure how you intend on using this otherwise, so I maybe need more clarification, but that’s just a few thoughts.

1 Like

To be more specific I am looking for ergonomic ways to implement a state machine. State is a tagged union and functions implement state transitions. I would like implement double dispatch state.move(to: State) in terms of finer granularity functions avoiding giant NxN transition table.

Not sure if this is going to helpful. Oftentimes I’ve found it useful to look up functions by name:

const std = @import("std");

const State = enum { A, B, C };

const StateHandlers = struct {
    fn @"foo() when state = A"(x: i32) State {
        _ = x;
        return .B;
    }

    fn @"foo() when state = B"(x: i32) State {
        _ = x;
        return .C;
    }

    fn @"foo() when state = C"(x: i32) State {
        _ = x;
        return .A;
    }
};

fn foo(state: State, x: i32) State {
    inline for (@typeInfo(State).Enum.fields) |field| {
        if (state == @field(State, field.name)) {
            const f = @field(StateHandlers, "foo() when state = " ++ field.name);
            return f(x);
        }
    }
    unreachable;
}

pub fn main() void {
    var state: State = .A;
    std.debug.print("{any}\n", .{state});
    state = foo(state, 123);
    std.debug.print("{any}\n", .{state});
    state = foo(state, 123);
    std.debug.print("{any}\n", .{state});
    state = foo(state, 123);
    std.debug.print("{any}\n", .{state});
    state = foo(state, 123);
    std.debug.print("{any}\n", .{state});
}

Bit cleaner than using a switch statement, especially in situations when the function involves multiple generic types.

2 Likes

Eh, why is the syntax highlighting all broken? Code looks fine in VS Code.

I suspect it has to do with the @ signs and quotes making Discourse go crazy. I think Discourse tries too hard to interpret any @ sign as a username mention.

Which kind of a machine, a data/text driven (a parser) or an event/message driven one?

In my implementations of finite state automata, when there are few -less than 6- states and transitions I am keeping the state in a variable and the entire automaton is a single function that loops in a switch (state). When there are more states and transitions I am using a function for each state, calling the function means that the state is changed, to start the machine you simply call the function of the initial state. Note that when there is no tail-call guarantee I am using a trampoline - returning the function of the next state instead of calling it and the trampoline loop just calls the state function in a loop.

4 Likes