Skip to content

Proposal: Optional argument names in function calls #982

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
alexnask opened this issue May 3, 2018 · 9 comments
Closed

Proposal: Optional argument names in function calls #982

alexnask opened this issue May 3, 2018 · 9 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@alexnask
Copy link
Contributor

alexnask commented May 3, 2018

Proposed syntax:

fn foo(a: u8, b: usize, c: []const u8) void {}

test "optional argument names" {
    foo(c: "Hi!", 0, 42); // -> foo(0, 42, "Hi!");
    foo(a: 0, c: "bar", 10); // -> foo(0, 10, "bar");
}

Invalid argument names obviously cause a compile error.
To match the arguments, we simply look at the named arguments first, then match the rest in the same order they appear in the function definition.

@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label May 3, 2018
@andrewrk andrewrk added this to the 0.4.0 milestone May 3, 2018
@tiehuis
Copy link
Member

tiehuis commented May 3, 2018

Minor note, this would make argument names part of a functions public api.

How important do you consider argument reordering to be? I would much prefer this without it personally.

@alexnask
Copy link
Contributor Author

alexnask commented May 3, 2018

I don't really have a problem with not having argument reordering, I guess I can see an argument for how it could be confusing.

@isaachier
Copy link
Contributor

What I've seen suggested in 21st Century C to do this without actually supporting it in the language is just to use a throwaway struct. At least in C, this allows for unnamed parameters to be passed in order, and designated initializers to be passed out of order. If the Zig compiler generates this struct on the fly, you can avoid adding entirely new semantics to the parser.

void some_c_function(int x, float y, const char* z);  // Original function

struct better_c_function_arg { int x; float y; const char* z; };

void better_c_function(struct better_c_function_arg arg);
#define call_better_c_function(...) \
better_c_function((struct better_c_function_arg){ __VA_ARGS__ })

call_better_c_function(0, 1.0, "hello");  // Calling in order
better_c_function((struct better_c_function_arg){ 0, 1.0, "hello" });  // Expanded to struct initializer

call_better_c_function(.z = "hello", .x = 0, .y = 1.0);  // Calling by name
better_c_function((struct better_c_function_arg){.z = "hello", .x = 0, .y = 1.0});  // Expanded to struct initializer

@BraedonWooding
Copy link
Contributor

Adding onto this, We could add a 'swift' like optional parameter name; for reference it looks like this;

func foo(a b: i32) i32 {
    // We use 'b'
    return b;
}

// You HAVE to use names to access it, I believe
foo(a: i32);

This is because often if you want to do names you want nice names to form an api like;
find("a", in: "bob"), or copy(from: obj, to: obj) which are great calling names but aren't great 'using names' (though from and to are actually okay in that instance).

I'm pretty unsure about how it would work in Zig but felt it might be worth bringing up.

@andrewrk
Copy link
Member

andrewrk commented May 4, 2018

Adding another idea into the mix here:

One of the thing Zig brings to the table is the comptime integer type. This is where, instead of specifying, e.g. 1234UL like you would in C and getting unsigned long as a type, you can use 1234 and the default type is a comptime integer. This is unlimited precision, and will implicitly cast to any integer type that you end up wanting it to be.

Related issue: #556 (rename int literal and float literal types)

It's proposed (See #683) to add syntax so that one can use, e.g. .SeqCst instead of @import("builtin").AtomicOrder.SeqCst. This would be especially useful in switch expressions, and we want to use it for calling convention in function prototype syntax (See #661).

The way this would work is that the expression .Foo would be a new type - in the same way that an undefined literal and a null literal have their own types. Just like the number literal 1234, .Foo would be a comptime type. It would be usable directly at comptime, or one could implicitly cast it to an enum type which had a matching tag name.

Taking this a step further, we could introduce a comptime type for struct literals:

fn foo(args: struct {a: u8, b: usize, c: []const u8}) void {}

test "optional argument names" {
    foo(.{.c = "Hi!", .a = 0, .b = 42});
    foo(.{.a = 0, .c = "bar", .b = 10});
}

This would be the struct equivalent of tuples in the proposal to remove var args and add tuples:

test "foo" {
    var x: i32 = 1234;
    var y = "blah";
    std.debug.warn("int = {}, str = {}\n", [x, y]);
}

Here whether the tuple syntax is .[a, b] or [a, b] is up for debate.

@PavelVozenilek
Copy link

For that comptime integer type wraparound arithmetic operations should be disabled (to avoid confusion like this: #964 (comment)).

Also, what if I have my own function foo taking i32 parameter, and use it with comptime integer:

const x : i32 = foo(123);

The compiler would need to keep track on every immediate value, whether it still fits within valid range.

@bheads
Copy link

bheads commented May 7, 2018

+1 for named arguments. I agree I find mixing named and position (rearranging) argument gets confusing when reading code and should be avoided. One use of named arguments that I really like if dealing with boolean flags:

foo(true, true, false, true);
// vs
foo(fullScreen: true, showMouse: true, logErrors: false, displayFps: true);

For flags it would be nice to have comptime enum literials, but thats a different topic.

pub fn foo(useAllTheMemory: enum { Yes, No}) {}
...
foo(useAllTheMemory.No);

@BraedonWooding The optional argument name concept is interesting, but if you can give an argument a better name, then I would think thats what the argument should be. Self documenting code and such..

Also off topic for named arguments but, @andrewrk It would be interesting if the idea of comptime type for struct literals could also work for return types. Tuples may solve this as well, they would be better if they also support unpacking but thats a different discussion.

fun foo() struct { x: int, y: int} (
  return .{ .x = 1, .y = 5};
)

var result = foo(); // yay! Simple version of multiple return types. 
bar(result.y, result.x); 

var temp : @typeOf(foo).ReturnType;  // <-- should add this to std.meta something like: pub fn returnType(compttime T: type) { .. test if function.. yadada.. }
temp = foo();

@floooh
Copy link
Contributor

floooh commented Jun 1, 2018

Just a random but related thought: in C99, I'm using structs with designated-init as a sort-of-optional-named arguments feature. It looks a bit weird and inefficient at first, but it's actually more powerful for complex cases, and the generated code doesn't look worse than having a function call with many arguments (which wouldn't fit into registers):

// the function prototype would look like this:
sg_image sg_make_image(const sg_image_desc* desc);

// now in C99 you can embed the struct initialization into the function call, 
// and only partially initialize the sg_image_desc struct (missing members will
// be set to 0, which I need to treat as 'default value' in the function call, because 
// C99 doesn't let me define default values in the struct declaration):
sg_image img = sg_make_image(&(sg_image_desc) {
    .width = 512,
    .height = 256,
    .num_mip_maps = 1,
    .pixel_format = SG_PIXELFORMAT_RGBA8
});

The nice thing is that this also works for complex cases (e.g. arrays or embedded structs):

    sg_pipeline pip = sg_make_pipeline(&(sg_pipeline_desc){
        .layout = {
            .buffers[0].stride = 28,
            .attrs = {
                [0] = { .name="position", .format=SG_VERTEXFORMAT_FLOAT3 },
                [1] = { .name="color0", .format=SG_VERTEXFORMAT_FLOAT4 }
            }
        },
        .shader = shd,
        .index_type = SG_INDEXTYPE_UINT16,
        .depth_stencil = {
            .depth_compare_func = SG_COMPAREFUNC_LESS_EQUAL,
            .depth_write_enabled = true,
        },
        .rasterizer.cull_mode = SG_CULLMODE_BACK,
    });

Of course on one hand this is just a workaround because C is missing optional, named args as well, but in the end I even prefer this solution. All in all, C99's designated initialization is the most convenient way for cases where a struct has many members, but usually only a few of them must be set to a non-default value. For instance, C++ has many ways for initialization, but none are as convenient for this case as C99 (at least not with tons of boilerplate code).

In nim I was able to get close, but had to add a custom operator to partially initialize arrays, and additional 'constructor' functions:

https://github.com/floooh/sokol-nim-samples/blob/master/cube.nim#L114

@andrewrk
Copy link
Member

Solved by #485 (comment) + #685

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

8 participants