Unsure about how to handle function parameters

Hey everyone,

I’m refactoring the API for my S3 client lib (zig-s3) and want to see what people prefer regarding ergonomics and Zig conventions.

Initially, functions used plain positional arguments. As parameters started piling up, I considered switching everything to struct literals, but wrapping mandatory parameters in a struct feels like unnecessary friction for simple calls.

Comparing both approaches:

Everything in a struct

try client.createBucket(.{ .bucket_name = "my-bucket" });

var uploader = client.uploader(.{});
try uploader.uploadString(.{
    .bucket_name = "my-bucket",
    .key = "hello.txt",
    .content = "Hello, S3!",
});

const objects = try client.listObjects(.{
    .bucket_name = "my-bucket",
    .prefix = "folder1/",
});

Positional for required args and trailing struct for options

try client.createBucket("my-bucket");

var uploader = client.uploader(.{});
try uploader.uploadString("my-bucket", "hello.txt", "Hello, S3!");

const objects = try client.listObjects("my-bucket", .{ .prefix = "folder1/" });

What do you think? How do you usually handle this in your libraries?

Usually used like this.

“Everything in a struct” is somewhat ergonomic, but it’s not great for optimization when it comes to parameter passing. Also, this kind of ‘ergonomics’ can be covered by the IDE when reading, but from a writing perspective, it adds repetition of parameter names.

A ‘trailing struct for options’ is a pretty decent compromise.

Generaly I prefer positional arguments for required arguments and structs for optional arguments or configurable ones eg union enum for different use cases.

For example:

fn my_fn(req1: type, req2: anytype, opts: MyFnOpts) anytype {
    ...
}

const MyFnOpts = struct {
    opt1: ?anytype = null,
    ...
};

fn myFnWithUseCase(req1: anytype, useCase: Usecases) anytype {
   ...
}

const UsesCases = union(enum) {
    use_case1: struct { opt: anytype, ... },
    ...,
};

Every arguments I would pass to a call each time is a req otherwise it’s in the opts/usecases.

I agree. With structs, I ended up with a lot of parameter repetition, thanks a lot for your perspective!

What you said makes a lot of sense! I initially added a struct just for options, but ended up putting all parameters inside it. I guess I need to roll that back :sweat_smile:. Keeping the struct just for optional params will definitely make the function calls a lot cleaner.

An added benefit of the struct is for “required” arguments with a sane default value you can set. Those are often leaved as is but you give a little bit more control to the caller about the behavior of the function if they need it.

However you have to be careful with those because changing the default value is a “hidden” breaking change in your api. It is hidden because the behavior of the function changes without any compiler error on the caller side. And can be missed easily by your users.