Code review: initialization via tuple of tuples

Interested in your feedback on this approach to providing options for a relatively simple task: a command line argument parser. Rather than using a struct with typed fields, I realised that the minimal goals of the library could be achieved by using the comptime types of the initialisation data to understand their meaning:

.{ .{"output"}, .{"verbose", false}, .{"extract", 'x', false}, },

specifies three command line options. The first takes an argument, the second does not. It will recognise “–output filename” or “-o filename”, etc. The third takes no arguments and will recognise “–extract” or “-x”. And so on.

The argument order doesn’t matter, since there’s an inline for loop that looks to the type to figure out the intent.

Now, I fully realise this is not well suited for code that’s going to expand with more features. For instance, if the library wants to support error handling, help/usage reporting, required fields, etc., this approach probably wouldn’t scale. But there are several good full-featured libraries already, so that’s not my goal.

The comptime processing was super-simple. There’s a guard at the top of the function:

        const ti = @typeInfo(@TypeOf(options));
        if (ti != .Struct or !ti.Struct.is_tuple) return error.BadArgument;

And a loop to extract the arguments and put them into a struct for further validation:

            inline for (one) |x| {
                const x_ty = @TypeOf(x);
                const x_ti = @typeInfo(x_ty);

                if (x_ty == comptime_int) {
                    const c: u8 = x;
                    if (c == 0) {
                        create_opts.short = null;
                    } else {
                        create_opts.short = c;
                    }
                    continue;
                }
                if (x_ty == bool) {
                    create_opts.boolean = true;
                    continue;
                }
                if (x_ty == []const u8) {
                    create_opts.name = x;
                    continue;
                }
                if (x_ti == .Pointer) {
                    if (@typeInfo(x_ti.Pointer.child) == .Array) {
                        create_opts.name = x;
                        continue;
                    }
                }


                // std.debug.print("Expected a slice, bool or comptime_int, got: {}\n", .{@TypeOf(x)});
                return error.BadArgument;
            }

It was really pleasing to be able to do a compile-time type check with a simple statement like if (x_ty == []const u8). However, it was a bit of trial and error to try to understand how to detect a zero-sentinel array, especially since the size is part of the type:

                if (x_ti == .Pointer) {
                    if (@typeInfo(x_ti.Pointer.child) == .Array) {
                        create_opts.name = x;
                        continue;
                    }
                }

so I’m wondering if there’s something even simpler I’ve missed in the standard library. Or, more generally, if I’m doing anything strange. Aside from the simple, non-scalable approach, I mean.

For reference, the whole function is linked below. Thank you for any feedback!

A compile time trick.

As you probably already realized, the type of a string is not []const u8 but a 0 sentinel const array of u8 with the length of the string as size. This is impossible to match as type.
But I devised a trick to detect if a anytype value is an actual string.
@TypeOf can have multiple arguments and returns the type that all the arguments can coerce to.

    fn isString(comptime v: anytype) bool {
        const str: []const u8 = "";
        return @TypeOf(str, v) == []const u8;
    }
3 Likes

Very nice, I will use that: Peer Type Resolution

Ah actually, it’s not working: if v is a bool, it throws an error error: incompatible types: '[]const u8' and 'bool' Makes sense, those are not compatible types.

2 Likes

It does make sense, but it might be nice if Zig had a bottom type for this purpose. I was going to suggest noreturn but I’m not sure that works, conceptually speaking. It’s compatible with every other type, and we want the opposite of that. It doesn’t need to be expressible outside of type reflection contexts, so it could probably just be a Type.Bottom union variant.

Makes me think of Common Lisp, where T (meaning true) is the root of the type system. It would be nice if this comparison could be tested at compile time without a compile error.

1 Like