ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
35k stars 2.56k forks source link

design flaw: function argument with generic error require that function to possibly fail #2635

Open GreyDodger opened 5 years ago

GreyDodger commented 5 years ago

This is more a standard library design flaw than a language design flaw I think.

std\fmt.zig contains this function for rendering formatted text

pub fn format(context: var, comptime Errors: type, output: fn (@typeOf(context), []const u8) Errors!void, comptime fmt: []const u8, args: ...) Errors!void

I would like to call this function twice, once with a function that counts the length of the resulting string, and again after I've allocated the buffer of the right size to store to store the resulting string

(This might not be the best approach but I'm assuming this is a reasonable thing to do that the language should fully accommodate)

This issue is that neither of these calls would fail, but the output function you pass in has to be able to fail. So I'm required to write functions that have to pretend like they might fail even though they never will. This issue isn't the end of the world, but it feels a little gross and I'd prefer a more elegant solution.

For reference, here's what I wrote as functions/structs to pass into format()

    const FmtCountContext = struct {
        count: usize,
    };
    const FmtCountError = error{Full};
    fn fmtCountOutput(context: *FmtCountContext, bytes: []const u8) FmtCountError!void {
        context.count += bytes.len;
        if (context.count == std.math.maxInt(usize)) {
            return FmtCountError.Full;
        }
    }

    // essentially just fmt.bufPrintWrite
    const FmtInsertContext = struct {
        remaining: []u8,
    };
    const FmtInsertError = error{BufferTooSmall};
    fn fmtInsertOutput(context: *FmtInsertContext, bytes: []const u8) FmtInsertError!void {
        if (context.remaining.len < bytes.len) {
            return error.BufferTooSmall;
        }
        std.mem.copy(u8, context.remaining, bytes);
        context.remaining = context.remaining[bytes.len..];
    }

Again, not too bad of an issue because the errors seem reasonable, but I'd rather not have to pretend. Perhaps functions should never take in a function and dictate that failure must be a possibility when called?

daurnimator commented 5 years ago

Can you not pass error{} as the error type? (and have your function return error{}!void)

GreyDodger commented 5 years ago

Turns out you can, that turns my above code into this...

const FmtCountContext = struct {
    count: usize,
};
fn fmtCountOutput(context: *FmtCountContext, bytes: []const u8) error{}!void {
    context.count += bytes.len;
}

const FmtInsertContext = struct {
    remaining: []u8,
};
fn fmtInsertOutput(context: *FmtInsertContext, bytes: []const u8) error{}!void {
    std.mem.copy(u8, context.remaining, bytes);
    context.remaining = context.remaining[bytes.len..];
}

That's definitely an improvement, but I think it's still a design flaw. Those functions return void, adding that error{}! feels like nasty language cruft. Ideally they would just return void, and there'd be some kind of builtin function like @castThisFunctionToSomethingTryable(fmtInsertOutput) when I pass that as an argument to fmt.format. That or fmt.format shouldn't require that the passed in function to possibly fail. Either would make things just a bit more clear I think.