Let us consider a basic API which has functions which have flags. e.g.
fn action(..., enable_x: bool, enable_y: bool) {
...
if (enable_x) { ... }
if (enable_y) { ... }
}
and things of that variation.
My mind came up with three forms to approach this.
One is just the above, which is the clearest in terms of how the API behaves, but, without knowing how the compiler does its thing, makes it unclear how all those flags may be represented. E.g., naively, each of those flags could be its own u8 on the stack.
By contrast, we have the ol’ C approach where you would use bitmasks. We define a bunch of flags of the form
const ENABLE_X = 0x0000_0001;
const ENABLE_Y = 0x0000_0010;
and then can just define our command as const command = ENABLE_X | ENABLE_Y if we wanted both, with the logic in the function doing & ENABLE_X or & ENABLE_Y to check if it should execute the conditional branches. This makes it clearer how the data is represented, but then the function signature becomes
fn action(..., flags: u8) { ... }
which is less ergonomic in terms of API design. Of course, this could also be a bitset from the standard lib, but the point remains.
The third form I thought was, ok, just use a packed structed with boolean fields. Then, although the function signature is slightly less ergonomic, at least each possible flag is encoded into the type itself. E.g.
const ActionFlags = packed struct {
enable_x: bool,
enable_y: bool,
};
fn action(..., flags: ActionFlags) { ... }
Between these three, I view it as deciding between clearest API for a user vs. clearest representation of data. Does anyone have any opinions on which methodology is the best (if there is one), for API design?