Constrain type to generic struct

Hi all, I’m writing a multiplayer game in Zig that I want to run in multiple environments (terminal, web, etc.), so I made a generic Player struct that takes in functions for drawing to the screen and playing sounds. Then, I made a Match struct that manages the interactions between multiple Players.

pub const Sound = enum {
    // Types of sounds that might get played
};

pub fn Player(comptime drawAt: fn (x: u16, y: u16, char: u23) void, comptime playSound: fn (sound: Sound) void) type {
    return struct {
        // Actual usage of `drawAt` and `playSound`
    };
}

pub fn Match(comptime drawAt: fn (x: u16, y: u16, char: u23) void, comptime playSound: fn (sound: Sound) void) type {
    return struct {
        const PlayerType = Player(drawAt, playSound);

        players: []PlayerType,
        // More code...
    };
}

However, Match doesn’t care about drawAt and playSound at all, so really it should only be taking in the Player type as input:

pub fn Match(comptime PlayerType: type) type {
    return struct {
        players: []PlayerType,
        // More code...
    };
}

The problem I have with this is that comptime PlayerType: type says nothing about the type’s interface, and so I don’t get any highlighting, autocomplete, etc. regarding Player while inside Match. Whereas with the first example, I get everything like as if Player were a concrete type.

I’m not familiar with metaprogramming in Zig, but is there someway to solve this by, say, somehow constraining PlayerType to only be an output of Player?

// @ReturnType() isn't a real function
pub fn Match(comptime PlayerType: @ReturnType(Player)) type

There is no way to constrain it, like an interface.
You can use anytype and limit it using duck typing.

pub fn Match(PlayerType: anytype) type {
    return struct {
        players: []PlayerType,
        // More code...
        fn playAllSounds(self: @This()) void {
            for (self.players) |player| {
                // this call requires anytype to have playSound
                player.playSound();
            }
        }
    };
}

See: Function Parameter Type Inference

Thanks for the prompt response! Duck typing does make sure my code is correct, but I was hoping for something that gives a better dev experience, “communicate intent precisely” is part of Zig’s zen after all.

After more searching I found this post which inspired me to try moving the functions into Player’s fields instead:

pub const Player = struct {
    drawAt: fn (x: u16, y: u16, char: u23) void,
    playSound: fn (sound: Sound) void,

    pub fn init(
        comptime drawAt: fn (x: u16, y: u16, char: u23) void,
        comptime playSound: fn (sound: Sound) void,
    ) @This() {
        return .{ .drawAt = drawAt, .playSound = playSound };
    }
    // More code...
};

pub const Match = struct {
    players: []Player,
    // Mode code...
};

As far as I can tell, once compiled it’s functionally equivalent to everything above and IMO conveys intent better (drawAt and playSound are injected dependencies, why should they be baked in at type declaration?).

1 Like

Yes, if you can have a concrete type it is always better.