Skip to content
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

Require extra parentheses to disambiguate calling a function pointer from a container field #6405

Closed
tadeokondrak opened this issue Sep 23, 2020 · 4 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@tadeokondrak
Copy link
Contributor

tadeokondrak commented Sep 23, 2020

I propose that <expr>.field(<args>) become special syntax for calling @TypeOf(<expr>).field(expr, <args>).
No other syntax is changed, so (<expr>.field)(<args>) can still be used to call a function pointer.

const Foo = struct { bar: fn () void };
const foo: Foo = ...;
foo.bar(); // Error, no function bar in Foo's namespace
(foo.bar)(); // No error
const baz = foo.bar; // No error
const Foo = struct { fn bar() void {} };
const foo: Foo = ...;
foo.bar(); // No error
(foo.bar)(); // Error, no field bar in Foo
const baz = foo.bar; // Error, no field bar in Foo

This has two benefits:

  • You can now tell without looking at the struct definition if a call is to a method with first argument passed or to a field which is a function pointer.
  • BoundFn can be removed from the language.

Rust does this (or something very similar).

Note that this is not UFCS (#148):

fn add(a: i32, b: i32) i32 { return a + b; }
_ = add(1, 2); // Normal function call
_ = 1.add(2); // UFCS function call, not proposed here
@Vexu Vexu added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Sep 23, 2020
@Vexu Vexu added this to the 0.8.0 milestone Sep 23, 2020
@SpexGuy
Copy link
Contributor

SpexGuy commented Sep 23, 2020

I am definitely in favor of removing BoundFn, but the extra parens aren't necessary for disambiguation. I like that they make function pointer calls explicit, but they could be a little awkward lexically. For example, if foo.bar is a function pointer, what about the following?

const func = foo.bar;
func(); // is this allowed?
(func)(); // or does it need to be this?

If the parens are only required when the function pointer is a struct member, that seems like a really special case. What about a function pointer as a global var? Does that need parens? But if the parens are required always, then what about (funcNotPointer)()? This is a valid expression, since (funcNotPointer) is evaluated at comptime to the function, and () is allowed on the function. So it allows parens around non-pointer function calls, and you can't really use it to distinguish pointer calls from non-pointer calls.


Since we're planning to make a distinction in the type system between function pointers and functions (#1717), we can almost force this distinction in a more straightforward way by requiring .* when calling function pointers:

const Foo = struct { bar: *fn () void };
const foo: Foo = ...;
foo.bar(); // Error, bar is a function pointer, must dereference to call
foo.bar.*(); // No error
const baz = foo.bar; // No error
baz(); // Error: baz is not a function, use baz.*
baz.*();

But this doesn't actually work. .* on a function pointer yields a comptime-only function type, and since it's a runtime pointer this cannot be evaluated at compile time. So I think we need something else. Maybe just .(...)? So we would have

const Foo = struct { bar: *fn () void };
const foo: Foo = ...;
foo.bar(); // Error, bar is a function pointer, use .()
foo.bar.(); // No error
const baz = foo.bar; // No error
baz(); // Error: baz is not a function, use baz.()
baz.(); // No error

x.( is not currently a valid sequence in any zig code as far as I'm aware, so I think this is unambiguous. foo.bar. is not a complete expression, so we can still get rid of bound functions.

@tadeokondrak
Copy link
Contributor Author

I am definitely in favor of removing BoundFn, but the extra parens aren't necessary for disambiguation. I like that they make function pointer calls explicit, but they could be a little awkward lexically. For example, if foo.bar is a function pointer, what about the following?

To be clear, the only special case is <expr>.field(<args>). The fact that you have to put parentheses around a function pointer call in a container is just a result of it fitting this pattern but not being a method call.
I don't think the issue title is great, but I don't know what to put there instead.

foo.bar. is not a complete expression, so we can still get rid of bound functions.

I don't understand how you can still get rid of BoundFn, because:

const Foo = struct { fn bar() void {} };
const foo: Foo = ...;
const baz = foo.bar; // Is this an error?

@SpexGuy
Copy link
Contributor

SpexGuy commented Sep 23, 2020

I don't think the issue title is great, but I don't know what to put there instead.

What I meant to point out is that the extra parens are not necessary for the compiler, we are choosing to enforce them as an extra step in order to make the code easier to read. This is fine, but I question whether it really makes the code easier to read if the extra syntax is only required in specific cases that are dependent on surrounding context, and the extra syntax is valid in other cases but has a different meaning.

To be clear, the only special case is <expr>.field(<args>).

This could be weird when <expr> evaluates to a type. For example:

const ptrs = struct {
    // global runtime function pointer
    pub var globalPtr: *fn() void;

    // instance runtime function pointer
    instPtr: *fn() void;

    const globalFunc = fn() void {
        globalPtr(); // case 1: no x.y, call to function pointer looks like normal call
    };

    const instFunc = fn(self: *@This()) void {
        (self.instPtr)();
    };
};

const foo2 = fn() void {
    ptrs.globalPtr(); // case 2: fails compile? function pointer
    (ptrs.globalPtr)(); // case 3: success
    ptrs.globalFunc(); // case 4: success?  If the remapping is unilateral, this would fail.
    (ptrs.globalFunc)(); // case 5: success?

    var x: ptrs = undefined;
    x.instPtr(); // case 6: fails compile, function pointer
    (x.instPtr)(); // case 7: success
    x.instFunc(); // case 8: success
    (x.instFunc)(); // case 9: fails compile, x has no field instFunc
};

This has the desirable behavior that cases 6 and 9 both fail, but the undesirable behavior that cases 2 and 5 are different, and that case 2 is different from case 1. You could instead decide that 2 and 5 both succeed, but now parens no longer tell you that the function may be a function pointer. I think if you can't easily make this distinction, it's not worth enforcing the parens. In other words, if we're going to go to the trouble of requiring special syntax for invoking a function pointer, we should require that for all function pointers, regardless of the surrounding context.


const baz = foo.bar; // Is this an error?

Yes, this would parse successfully but fail to compile, just like in your proposal.

@tadeokondrak
Copy link
Contributor Author

This could be weird when evaluates to a type.

Hmm. I hadn't considered this, and I think this basically kills this specific syntax-based proposal.
I think requiring different syntax for calling function pointers can be part of #1717, so I'm going to close this.

@Vexu Vexu modified the milestones: 0.8.0, 0.7.0 Oct 3, 2020
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

3 participants