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

Function overloading - a different approach #2828

Closed
andersfr opened this issue Jul 6, 2019 · 14 comments
Closed

Function overloading - a different approach #2828

andersfr opened this issue Jul 6, 2019 · 14 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@andersfr
Copy link
Contributor

andersfr commented Jul 6, 2019

Function overloading in the general sense doesn't feel very Zig-like and previous issues have also been closed for this reason.

Here I present a proposal that could work for Zig:

const Example = struct {
    str: []const u8,

    const Self = this;

    pub const init = overload {
        initWithString,
        initWithSelf,
    };

    fn initWithString(str: []const u8) Self {
        return Self{ .str = str };
    }

    fn initWithSelf(other: Self) Self {
        return Self{ .str = other.str };
    }
};

var ex1 = Example.init("Hello, world!");
var ex2 = Example.init(ex1);

The general idea is that we provide the overload set explicitly and the compiler picks the first one that can be instantiated with the provided parameters. It is quite clear to the user what is happening.
Also, this doesn't require weird name mangling as everything is forwarded to a properly named function that can participate in extern linkage.

@ikskuh
Copy link
Contributor

ikskuh commented Jul 6, 2019

I like that approach as it could be done as pure syntactic sugar. I'm not sure if it could be done fully in user space, but coding a function like init could be done:

const Example = struct {
    str: []const u8,

    const Self = @This();

    fn init(val : var) Self {
      if(@typeOf(val) == Self) {
        return initWithSelf(val);
      }
      else {
        return initWithString(val);
      }
    }

    fn initWithString(str: []const u8) Self {
        return Self{ .str = str };
    }

    fn initWithSelf(other: Self) Self {
        return Self{ .str = other.str };
    }
};


pub fn main() void {
    var ex1 = Example.init("Hello, world!");
    var ex2 = Example.init(ex1);
    std.debug.warn("{} {}\n", ex1.str, ex2.str);
}

But i think overloading would contradict the Zig zen "Only one obvious way to do things." as it is not obvious what parameter set the function init would actually take

@mikdusan
Copy link
Member

mikdusan commented Jul 6, 2019

as it is not obvious what parameter set the function init would actually take

That's my biggest gripe with using a comptime approach. On the one hand it's infinitely more flexible, but ultimately buries the variations and all clarity is lost.

@andersfr
Copy link
Contributor Author

andersfr commented Jul 6, 2019

The comptime approach breaks down if the function signatures don't match up exactly. This poses no problem for my proposal.
It is possible to mimic default values by providing an overload with fewer parameters and letting that explicitly call the implementation with all parameters. That use case is not the purpose of this proposal and I will not recommend abusing it in this way.

pub const init = overload {
    initDefault,
    initWithParams,
};

fn initDefault() void {
    initWithParams(1337);
}

fn initWithParams(param1: usize) void {}

@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jul 6, 2019
@andrewrk andrewrk added this to the 0.6.0 milestone Jul 6, 2019
@SamTebbs33
Copy link
Contributor

SamTebbs33 commented Jul 8, 2019

Function overloading is one of the things I miss from Java (my second language after PHP...) when writing C and Zig, so I would certainly like to have it in Zig. Although I do like @andersfr 's proposal, I believe that the various declarations for init should be localised together, so I propose syntax similar to the following:

fn init(str: []const u8) {
    return Foo { .str = str };
} (foo: Foo) {
    return Foo { .str = foo.str };
} Foo

The downside with this would be that name mangling is required, so an adjustment to the syntax would be required to fix that. Alternatively a new keyword could be used instead of fn to denote an overloaded function which would make the syntax less prone to name mangling.

EDIT: Alteration of @andersfr 's proposal to make the function definitions more localised.

overload init {
    initWithStr(str: []const u8) {
        return Foo { .str = str };
    }
    initWithSelf(ex: Foo) {
        return Foo { .str = ex.str };
    }
} Foo;

Downside here is that the braces imply some kind of scoping where there really is none.

@ikskuh
Copy link
Contributor

ikskuh commented Jul 8, 2019

If we assume #1717 is done, we could also use a pack of anonymous functions instead of named ones:

const Example = struct {
    str: []const u8,

    const Self = this;

    pub const init = overload {
        fn(str: []const u8) Self {
            return Self{ .str = str };
        },
        fn(other: Self) Self {
            return Self{ .str = other.str };
        }
    };
};

var ex1 = Example.init("Hello, world!");
var ex2 = Example.init(ex1);

This would work with named functions as well, but i think this follows more the "one obvious way to do things" than "should i call init or initWithSelf".

Problem would be still that we probably could still use overload{} with named functions

As much as i'd like to see overloading, it should fit to the languages concept

@andersfr
Copy link
Contributor Author

andersfr commented Jul 8, 2019

There are multiple reasons I chose named over anonymous functions in the overload set:

  1. It is clear and concise what functions participate (not hidden in-between mountains of code)
  2. Named functions have the benefit of participating in DocComments to document the API
  3. The name itself can carry meaning as to what to expect from calling it
  4. IDEs with code introspection can more easily inform the user of the overloads and guide to their implementation.

The idea that it breaks "one obvious way to do things" by having both init and its overloads available is not true in my opinion. If multiple init functions (or any other similarly named/overloaded functions) exist it is because the API of the struct has a need to support all of these use cases. If the documentation cannot disambiguate their uses it is either bad API design or poor documentation.

@shawnl
Copy link
Contributor

shawnl commented Aug 8, 2019

I think this should be more in user-space, by allowing taking @typeOf() on a function pointer (who's type is the same magic keyword fn). Then instead of this overload keyword, you use an array: []fn, and the dispatch matching magic is a comptime user-space function. Also, a fn without a type signature would match anything (or we could reuse naked), and you would need something like @This (not sure if it can be identical) to refer to the type signature that it was called with.

pub const init = naked fn() {
    return std.dispatch.call(@This, []fn{initWithString, initWithStruct});
}

@Tetralux
Copy link
Contributor

Tetralux commented Aug 8, 2019

You could also use fn instead of introducing overload:

pub const init = fn { 
    initWithString,
    initWithSelf 
};

@andersfr
Copy link
Contributor Author

andersfr commented Aug 8, 2019

Before choosing between fn and overload we must carefully consider the implications on @typeOf. I haven't worked out all the details (intend to do so at a later time).

Having a []fn{...} would suggest that an index property exists and that the listing is homogenous. Function overloading is a heterogeneous comptime concept. Both concepts are useful but fundamentally different.

Example of homogenous user-space dispatch:

const warn = @import("std").debug.warn;

fn dispatch0(arg: usize) void {
    warn("dispatch0 called: {}\n", arg);
}

fn dispatch1(arg: usize) void {
    warn("dispatch1 called: {}\n", arg);
}

pub fn main() void {
    const dispatch = [_](fn(usize) void){ dispatch0, dispatch1 };

    dispatch[1](1);
}

@SamTebbs33
Copy link
Contributor

I don't see how @typeOf could be used for overloading as it would mean you couldn't have overloads with a different number of parameters, where as the original proposal would allow this. It's also worse for readability and typeability.

@shawnl
Copy link
Contributor

shawnl commented Aug 8, 2019

You are missing that type would-be expanded to express th type of a whole function, much as vectors and arrays also hold subtypes.

@kyle-github
Copy link

Thinking out loud here, so none of this is at all baked.

I like the tooling aspects of the OP's idea. While I do not like that I cannot tell from a given call site exactly what function is returned, it is not too bad as there is only one place to look. A big +1 due to the explicit names.

If there was some sort of pattern matching possible, you could almost get away with using a sort of varargs plus switch/case thing (and perhaps this is what was intended with the @typeof discussion):

const init = fn (args:var...) switch(args) {
    (x:i32) i32 => return initI32(args[0]);
    (f:f64) void => return initF64(args[0]);
    ...
}

Excuse all the syntax problems. As I said, this is very much not baked.

This would set up multiple signatures for a given function and at compile time, we'd pick the matching one. The function could call out to additional functions as I show here, or you could have the code inline. I would definitely switch on the whole signature, including the return type.

Probably too much magic. And I think varargs were dropped at some point. Can't remember.

@ghost
Copy link

ghost commented Aug 10, 2019

This proposal seems like it could be closely emulated with anonymous struct literals, #685:

const Example = struct {
    str: []const u8,

    const Self = this;

    // pub const init = overload {
    pub const init = .{
        initWithString,
        initWithSelf,
    };

    fn initWithString(str: []const u8) Self {
        return Self{ .str = str };
    }

    fn initWithSelf(other: Self) Self {
        return Self{ .str = other.str };
    }
};

//var ex1 = Example.init("Hello, world!");
//var ex2 = Example.init(ex1);

var ex1 = Example.init._0("Hello, world!");
var ex2 = Example.init._1(ex1);

Now it's not overloading of one single identifier anymore, but you do get to group related functions under one name.

@Tetralux
Copy link
Contributor

@user00e00 Problem being that you rather defeat the point of overloading if you now have to figure out which of _0, _1 you want.
I'd argue that just calling the function you wanted is more than an order of magnitude clearer than that.

Though being able to use a struct literal there could be an interesting idea for how to do this, perhaps.

I think I'd prefer overload {}, fn {}, etc, but still. It's an option.

@andrewrk andrewrk closed this as completed Oct 2, 2019
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