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

remove var args and add anon list initialization syntax #208

Closed
fsaintjacques opened this issue Oct 28, 2016 · 44 comments
Closed

remove var args and add anon list initialization syntax #208

fsaintjacques opened this issue Oct 28, 2016 · 44 comments
Labels
accepted This proposal is planned. breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@fsaintjacques
Copy link
Contributor

fsaintjacques commented Oct 28, 2016

Accepted Proposal


Tuple is a powerful construct in modern language when one doesn't want to define a struct simply to return/pass grouped information. I'd like to be able to do:

val (div, rem) = divWithRemainder(25, 9);

Not sure if this should be part of the language or simply baked in the standard library, e.g like scala where they define Tuple2(inline A: type , inline B: type). They do have sugaring that can extract in the val (x, y) = expr construct though.

@thejoshwolfe thejoshwolfe added the enhancement Solving this issue will likely involve adding new logic or components to the codebase. label Nov 8, 2016
@thejoshwolfe
Copy link
Sponsor Contributor

The topic of tuples was one of the inspirations for the discussion that went into #83 (comment) . Here's what it means for tuples:

Zig is resistant to the idea of having general-purpose tuples simply because they don't seem useful enough to justify including the in the language. But here's what is useful that's like tuples, and we want to include in Zig:

Returning multiple things from a function at once

This is done with named return types. See #83.

Expressions initializing multiple values at once

This is done with all expressions and return types having a number of expressions returned, possibly 0. See #83.

Example:

const a, const b = if (something) {
    1, 2
} else {
    printf("second case\n");
    2, 1
};

Variadic functions (varargs)

See #77.

It's still to be seen how varargs will affect the existence and features of tuples. Maybe we will have tuples as a consequence of the []var type, or maybe not.

@tiehuis tiehuis added proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. rejected labels Sep 15, 2017
@andrewrk andrewrk removed enhancement Solving this issue will likely involve adding new logic or components to the codebase. rejected labels Jun 1, 2018
@andrewrk andrewrk changed the title Tuple type Add tuples and remove var args Jun 1, 2018
@andrewrk
Copy link
Member

andrewrk commented Jun 1, 2018

Proposal: Add tuples and remove var args

Var args turned out to be rather messy and bug prone:

Further, it can be abused to achieve default function parameters (rejected in #484) and we don't want that. Even for functions such as std.debug.warn, it is more in the spirit of Zig to pay the cost of a few extra characters of typing in return for the human reading the code to know exactly what's going on at the function definition.

Var args right now is essentially a special-case tuple, with a bunch of arbitrary rules and special cases. So this proposal is to add tuples to the language and remove var args. Calling std.debug.warn would look like this:

pub fn main() void {
    const x: i32 = 1234;
    const y = "aeou";
    std.debug.warn("number: {}, string: {}\n", [x, y]);
}

[a, b, c] is an example of the tuple literal syntax. A trailing comma is optional.

A tuple has comptime-known length (via the .len property) and comptime-known types for each item. The type of each item does not have to be homogeneous, like it does for arrays. The item values are runtime known.

Tuple literals implicitly cast to arrays, when all of the elements of the tuple implicitly cast to the array type, and the length matches.

I also propose @call which can be used to "explode" a tuple into the function arguments: @call(function, tuple). foo(x, y, z) would be equivalent to @call(foo, [x, y, z]).

The syntax for a tuple type is tuple{T1, T2, T3}

The definition of a function which accepts a tuple as an argument would likely use var:

fn print(comptime format: []const u8, args: var) void {
    inline for (args) |arg| {
        // `for` works with tuples
    }
}

You would need the var because you want to accept a tuple{i32, []const u8} as well as tuple{i32, i32, f32}, etc.

Here's an example from another issue, division that wants to report error.DivisionByZero:

fn div(numerator: i32, denominator: i32) !tuple{i32, i23} {
    if (denominator == 0) return error.DivideByZero;
    // ...
    return [a, b];
}

One more note on the literal syntax. I have an idea for an untyped struct literal that would look like (notice the dot in front):

.{.field = value, .field2 = value2}

This is to match untyped enum literal syntax, useful in switch cases:

.EnumValue

If we kept these in mind, then it might make sense, instead of [x, y, z] to use .{x, y, z} for tuple literal syntax.

Tuple instances can be accessed with array access syntax: a_tuple[0], a_tuple[1], ...

@andrewrk andrewrk reopened this Jun 1, 2018
@andrewrk andrewrk added this to the 0.3.0 milestone Jun 1, 2018
@andrewrk andrewrk added the accepted This proposal is planned. label Jun 4, 2018
@binary132
Copy link

binary132 commented Jun 18, 2018

I like the [t0, t1, t2, ...] syntax for tuples. I think it's orthogonal with anonymous inline structs (or should be.) It also appeases my sense of tuples as being a more array-like or set-like thing.

@alexnask
Copy link
Contributor

I personally prefer the { a, b, c, } syntax, I feel it looks like an anonymous struct literal more than the [ a, b, c, ] syntax does.

I feel like thinking of a tuple as a struct with anonymous fields makes more sense than thinking about it as an array like type.

@binary132
Copy link

binary132 commented Jun 18, 2018

@alexnask --

https://en.wikipedia.org/wiki/Tuple

In mathematics, a tuple is a finite ordered list (sequence) of elements. An n-tuple is a sequence (or ordered list) of n elements, where n is a non-negative integer. There is only one 0-tuple, an empty sequence, or empty tuple, as it is referred to. An n-tuple is defined inductively using the construction of an ordered pair.

But I'm open to the possibility that Zig is more interested in the machine representation than in type theory.

Or, maybe this isn't a tuple at all and should be conceived of in a different way entirely.

@alexnask
Copy link
Contributor

@binary132

I feel like using array literal syntax may lead people to believe the fields are contiguous in memory, while tuple will probably be equivalent to the default layout of a struct with the fields of the types.

At the end of the day it's just a question of syntax :P

@BarabasGitHub
Copy link
Contributor

What about using (a, b, c) for tuples like in Python? I know in Python you have the confusing case of a one element tuple which you have to write as (a,), because (a) is just seen as grouping. With the square brackets like [a, b, c] wouldn't it be easy to confuse it with arrays?

@montychen
Copy link

I personally prefer the ( a, b, c, ) syntax for tuples。With the square brackets like [a, b, c] would i be easy to confuse it with arrays。

@bheads
Copy link

bheads commented Jul 31, 2018

Part of this proposal breaks the usage of var as a function param.

For example this function would now be accepting a tuple and not a single argument..

pub fn abs(x: var) @typeOf(x) { .. } 

so maybe the type should just be tuple

pub fn writeln(fmt: []u8, args: tuple) !void { ... }

It also seems like there are two kinds of tuple, named and index tuples.

named: [x = 1, name = "hello", pi = 3.14]
index: [1, "hello", 3.14];

The named tuples look like anonymous structs and might be better with a different syntax:

fn foo(config:  struct{index: i32, name: []u8, offset: u8})  struct{x: i32, y: u64} { 
   return @typeOf(foo).Fn.return_type{.x = 0, .y = 1};
   // or 
      return .{.x = 0, .y = 1};
} 

var point = foo( .{ .index = 1, .name = "hammer time", .offset = 0} );
point.x  + point.y;

I would also suggest anonymous enums and unions as well.

There are a lot of possible tuple syntax options:

var a: i32 = 1;
#(a, 3, 4) // tuple (1, 3, 4)
#{1, 2, 3}
tuple.{1, 2, 3}
tuple.{x: 1, y: 2, z: 3} // named tuple?

@Hejsil
Copy link
Sponsor Contributor

Hejsil commented Oct 16, 2018

I think I'll dig into Zig and implement tuples soon(ish). Here is how I imagine tuples working:

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

test "Tuples!" {
    // Like enums, structs and unions, tuples will have their own keyword for declaring
    // tuple types.
    const Tuple  = tuple.{i32, []const u8};
    const Struct = struct.{a: i32, b: []const u8};
    const Enum   = enum.{A, B};
    const Union  = union.{A: i32, B:[]const u8};

    // Two tuples types are the same, as long as all their members are the same. This
    // is different from how structs, enums and unions work,
    assert(tuple.{u8}    == tuple.{u8}   );
    assert(struct.{a:u8} != struct.{a:u8});
    assert(enum.{A}      != enum.{A}     );
    assert(union.{A:u8}  != union.{A:u8} );

    // Tuples use the array initializer syntax, but each element can be of different
    // types. No new initializer syntax.
    const t = tuple.{i32, []const u8}.{0, ""};
    const a = [2]u8.{0, 1};

    // To access the items of a tuple, the index operator is used.
    // This allows tuples to be used with inline while, to iterate over all members.
    const a = t[0]; // Here, the index have to be comptime known

    // Tuples can be destructured into multible variables.
    // This allows for multible return types from both functions and labled blocks:
    const min, const max = blk: {
        var arr = []u8{2,3,5,2};
        var min = arr[0];
        var max = arr[0];

        for (arr[1..]) |i| {
            if (i < min)
                min = i;
            if (max < i)
                max = i;
        }

        break :blk tuple.{u8, u8}.{min, max};
    };
    assert(min == 2 and max == 5);

    // We might even allow assignment into already declared variables:
    var a: u8 = undefined;
    a, const b = tuple.{u8, []const u8}.{0, ""};

    // *** None essential features ***
    // We might also allow the "++" operator on tuples:
    const t1 = tuple.{u8}.{0};
    const t2 = tuple.{[]const u8}.{""};
    var t: tuple.{u8, []const u8} = t1 ++ t2;

}

test "Inferred initializers" {
    // Inferred initializers are initializers whos types are inferred from the context
    // in which they are used. They allow for shorthand initialization where it does
    // not improve readablilty to append the initializer with a type.

    // Inferred initializers come i 3 syntactic flavors.
    // structs and unions will use "inferred struct initializers"
    const s: struct.{a: i32, b: []const u8} = .{ .a = 0, .b = "" };
    const u: union.{A: i32, B: []const u8}  = .{ .A = 0 };

    // tuples and arrays will use "inferred tuple initializers"
    const t: tuple.{i32, []const u8} = .{0, ""};
    const a: [2]u8                   = .{0, 1};

    // enums will use "inferred enum initializers"
    const e: enum.{A, B} = .A;

    // They will have their own internal temporary type, which implicitly cast to
    // other types using the following rules:
    // * "inferred struct initializers" will implicitly cast to:
    //   * Structs, If:
    //      * The initializer has the same number of fields as the struct.
    //      * All initializer field names are also in the struct.
    //      * All initializer fields implicitly casts to the struct field of the
    //        same name.
    //   * Unions, If:
    //      * The initializer has 1 field.
    //      * The initializers fields name is contained in the union.
    //      * The initializers field implicitly casts to the tag of that name.
    // * "inferred tuple initializers" will implicitly cast to:
    //   * Arrays, If:
    //      * The initializer has the same number of items as the array.
    //      * All initializers items implicitly cast to the arrays item.
    //   * Tuples, If:
    //      * The initializer has the same number of items as the tuple.
    //      * All initializers items implicitly cast to the tuples items.
    //   * They will also cast to a normal tuple, if none of the above is true.
    //      * This is useful for passing inferred tuple initializers to functions
    //        taking "var".
    // * "inferred enum initializers" will implicitly cast to:
    //   * Enum, If:
    //      * The initializers name is contained in the enum.
    //   * Union, If:
    //      * The initializers name is contained in the union.
    //      * The initializers name in the union is "void".

    // In userspace, inferred initializers doesn't have a type. You therefore can't
    // call @typeOf on them. They also can't be stored at runtime because of this.
    // error: enum initializers doesn't have a type.
    _ = @typeOf(.E);

    // error: enum initializers doesn't have a type
    _ = funcTakingVar(.E);

    // error: .E does not have a runtime representation.
    var a = .E;

    // error: struct initializers doesn't have a type.
    _ = @typeOf(.{ .a = 2, .b = 4 });

    // error: struct initializers doesn't have a type.
    _ = funcTakingVar(.{ .a = 2, .b = 4 });

    // error: .{ a: u8, b: u8 } does not have a runtime representation.
    var a = .{ .a = u8(2), .b = u8(4) };

    // The exception here is inferred tuples, which will cast to a normal tuple when
    // it is not inferred.
    _ = @typeOf(.{ 2, 4 });
    _ = funcTakingVar(.{ 2, 4 });
    var a = .{ u8(2), u8(4) };
}

// These two features allows us to have:
// *** Varargs ***
// With these two features, we can remove varargs, and have std.debug.warn be
// used like this:
fn warn(comptime fmt: []const u8, tuple: var) void { ... };

test "warn" {
    // This proposal does not solve passing comptime_int to varargs functions
    //               VVVVV
    warn("{} {}", .{ u8(1), "yo" });
}

// This also allows us to have "typed varargs" functionality:
fn concat(args: [][]const u8) []const u8 { ... };

test "concat" {
    const res = concat(.{"Hello", " ", "World!\n"});
}

// *** Named arguments ***
// related: https://github.com/ziglang/zig/issues/479
const Flags = struct {
    turn_on_nuke: bool,
    go_full_screen: bool,
    take_over_the_univers: bool,
};

fn something(flags: Flags) void { ... }

test "named args" {
    // With https://github.com/ziglang/zig/issues/485, we even get default argument
    // (https://github.com/ziglang/zig/issues/484) values.
    something(.{
        .take_over_the_univers = true,
        .turn_on_nuke = true,
        .go_full_screen = false,
    });
}

related: #479 #484 #485 #683 #685

@allochi
Copy link

allochi commented Oct 17, 2018

Hi @Hejsil

// * "inferred tuple initializers" will implicitly cast to:
// * Arrays, If:

does it mean we would be able to initialize arrays this way?

const Struct = struct.{a: i32, b: []const u8};
const structs = []Struct.{
  .{.a = 0, .b = "0"},
  .{.a = 0, .b = "0"},
  .{.a = 0, .b = "0"},
  ...
};

or this way?

const Struct = struct.{a: i32, b: []const u8};
const structs: []Struct = .{
  .{.a = 0, .b = "0"},
  .{.a = 0, .b = "0"},
  .{.a = 0, .b = "0"},
  ...
};

Or both?

@Hejsil
Copy link
Sponsor Contributor

Hejsil commented Oct 17, 2018

I've discussed this more with @nodefish and @tgschultz on IRC, and @nodefish pointed out the following:

I noticed there's quite a bit of friction in many languages when using a mix of structs/tuples/arrays.
...
Yeah but there's always that problem of, say a library takes an array for something
while another uses tuples and yet another might use a struct to represent the same dataset.

Tuples go against the "Only one obvious way to do things.", and with this in mind we came up with a counter proposal, that gets us most of the same things:

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

test "Multible return values" {
    const S = struct { a: u8, b: []const u8 };

    // Structs and arrays can be desconstructed into their fields/items.
    const a, var b = S.{ .a = 0, .b = "" };
    const a, var b = [2]u16{1, 2};

    // This allows for multible return types from both functions and labled blocks:
    const min, const max = blk: {
        var arr = []u8{2,3,5,2};
        var min = arr[0];
        var max = arr[0];

        for (arr[1..]) |i| {
            if (i < min)
                min = i;
            if (max < i)
                max = i;
        }

        break :blk []u8.{min, max};
    };
    assert(min == 2 and max == 5);
}

test "Inferred initializers" {
    // Inferred initializers are initializers whos types are inferred from the context
    // in which they are used. They allow for shorthand initialization where it does
    // not improve readablilty to append the initializer with a type.

    // Inferred initializers come i 3 syntactic flavors.
    // structs and unions will use "inferred struct initializers"
    const s: struct.{a: i32, b: []const u8} = .{ .a = 0, .b = "" };
    const u: union.{A: i32, B: []const u8}  = .{ .A = 0 };

    // arrays will use "inferred array initializers"
    const a: [2]u8 = .{0, 1};

    // enums will use "inferred enum initializers"
    const e: enum.{A, B} = .A;

    // They will have their own internal temporary type, which implicitly cast to
    // other types using the following rules:
    // * "inferred struct initializers" will implicitly cast to:
    //   * Structs, If:
    //      * The initializer has the same number of fields as the struct.
    //      * All initializer field names are also in the struct.
    //      * All initializer fields implicitly casts to the struct field of the
    //        same name.
    //   * Unions, If:
    //      * The initializer has 1 field.
    //      * The initializers fields name is contained in the union.
    //      * The initializers field implicitly casts to the tag of that name.
    //   * They will also cast to a normal struct, if none of the above is true.
    //      * This is useful for passing inferred struct initializers to functions
    //        taking "var".
    // * "inferred array initializers" will implicitly cast to:
    //   * Arrays, If:
    //      * The initializer has the same number of items as the array.
    //      * All initializers items implicitly cast to the arrays item.
    //   * They will also cast to a normal array, if none of the above is true.
    //      * This is useful for passing inferred arrays initializers to functions
    //        taking "var".
    // * "inferred enum initializers" will implicitly cast to:
    //   * Enum, If:
    //      * The initializers name is contained in the enum.
    //   * Union, If:
    //      * The initializers name is contained in the union.
    //      * The initializers name in the union is "void".
    //   * They will also cast to a normal enum, if none of the above is true.
    //      * This is useful for passing inferred enums initializers to functions
    //        taking "var".

    // Implicitly casts to enum{E}
    _ = @typeOf(.E);
    _ = funcTakingVar(.E);
    var a = .E;

    // Implicitly casts to struct.{a: u8, b: u8}
    _ = @typeOf(.{ .a = u8(2), .b = u8(4) });
    _ = funcTakingVar(.{ .a = u8(2), .b = u8(4) });
    var a = .{ .a = u8(2), .b = u8(4) };

    // Implicitly casts to [2]u16
    _ = @typeOf(.{ u8(2), u16(4) });
    _ = funcTakingVar(.{ u8(2), u16(4) });
    var a = .{ u8(2), u16(4) };
}

// *** Varargs ***
// We can remove varargs, and have std.debug.warn be used like this:
fn warn(comptime fmt: []const u8, args: var) void { ... };

test "warn" {
    // This proposal does not solve passing comptime_int to varargs functions
    //                    VVVVV
    warn("{} {}", .{ .a = u8(1), .b = "yo" });

    // We can also use the names to have order independent fmt
    warn("{b} {a}", .{ .a = u8(1), .b = "yo" });

    // Format params will be passed after the ':'
    //      V     V
    warn("{:x} {a:x}", .{ .a = u8(1) });

    // Printing structs require that it is passed inside the "fmt" struct
    const S = struct.{a: u8, b: u8};
    warn("{}", .{ .a = S{.a = 0, .b = 0} });
}

// This also allows us to have "typed varargs" functionality:
fn concat(args: [][]const u8) []const u8 { ... };

test "concat" {
    const res = concat(.{"Hello", " ", "World!\n"});
}

// *** Named arguments ***
// related: https://github.com/ziglang/zig/issues/479
const Flags = struct {
    turn_on_nuke: bool,
    go_full_screen: bool,
    take_over_the_univers: bool,
};

fn something(flags: Flags) void { ... }

test "named args" {
    // With https://github.com/ziglang/zig/issues/485, we even get default argument
    // (https://github.com/ziglang/zig/issues/484) values.
    something(.{
        .take_over_the_univers = true,
        .turn_on_nuke = true,
        .go_full_screen = false,
    });
}

@Hejsil
Copy link
Sponsor Contributor

Hejsil commented Apr 8, 2019

@andrewrk I saw somewhere on IRC that you where considering added something like this as comptime fields:

const A = struct {
    a: u8,
    b: u16,
    comptime c: u8 = 100,
};

I had another idea i wanted to share. We could have comptime fields by adding a new type to the language. You create it with the builtin @OnePossibleValue(value: var). This type can only have one possible value and has the size of 0. It works like u0 and other 0 bit types, in that all loads and stores are comptime known. Two nice things about this is:

  • It requires no new syntax.
  • It is a more general feature of the language (I have no idea what you would use it for though).

The above example becomes:

const A = struct {
    a: u8,
    b: u16,
    c: @OnePossibleValue(u8(100)),
};

More details on how it would work:

  • @OnePossibleValue(u8(100)) implicitly casts to u8.
  • If a value is known at comptime, and is the one possible value of @OnePossibleValue, then that value implicitly casts to that @OnePossibleValue.
    • var a: @OnePossibleValue(u8(100)) = 100;
  • @OnePossibleValue supports the operations of its base type.
    • const a: @OnePossibleValue(u8(100)) = 100; const b = a + 200; // b is comptime_int

@andrewrk
Copy link
Member

andrewrk commented Apr 8, 2019

@Hejsil ooh, I like this. It solves the problem with a slightly less intrusive modification to the language.

Some observations:

  • @OnePossibleValue({}) == void
  • @OnePossibleValue(u0(0)) == u0

A downside of this is that reflection code iterating over struct fields would have to explicitly be aware of this weird type. With the "comptime fields" thing, the code iterating over struct fields would be able to treat comptime fields and normal fields the same (and just happen to get comptime values sometimes when doing field access).

@andrewrk andrewrk modified the milestones: 0.5.0, 0.6.0 Aug 21, 2019
shawnl added a commit to shawnl/zig that referenced this issue Nov 5, 2019
<andrewrk> tomorrow, end of day, ziglang#3580 will be closed and ziglang#3575 will be
closed, and dimenus's examples will work (except for vector literals which
is not happening, see ziglang#208)
andrewrk added a commit that referenced this issue Nov 11, 2019
This implements stage1 parser support for anonymous struct literal
syntax (see #685), as well as semantic analysis support for anonymous
struct literals and anonymous list literals (see #208). The semantic
analysis works when there is a type coercion in the result location;
inferring the struct type based on the values in the literal is not
implemented yet. Also remaining to do is zig fmt support for this new
syntax and documentation updates.
andrewrk added a commit that referenced this issue Nov 11, 2019
This implements stage1 parser support for anonymous struct literal
syntax (see #685), as well as semantic analysis support for anonymous
struct literals and anonymous list literals (see #208). The semantic
analysis works when there is a type coercion in the result location;
inferring the struct type based on the values in the literal is not
implemented yet. Also remaining to do is zig fmt support for this new
syntax and documentation updates.
@Sahnvour
Copy link
Contributor

Sahnvour commented Nov 13, 2019

So I have some points to make in favor of variadic arguments, or should I say against the actual list initialization syntax replacing it, that is limited in some cases and causes regression in zig's expressiveness.

I know I'm not coming with the most compelling examples ever, but please acknowledge that they can be real usecases, and correct me if I'm wrong or missing something !

I'll call generic function any kind of function that depends on comptime parameter, and instanciation the actual function producing machine code for a specific set of its comptime parameters.
fn min(comptime T: type, a: T, b: T) T is a generic function, and it can be instanciated as min(u32). In the same way, functions with an argument of type var or ... (variadic) are also generic functions that can be instanciated for various types.

Generic functions flaws:

  • instanciations cannot be referenced in the language, even though they are present in the emitted code
  • we cannot take their address, even though that would be totally feasible and usable
  • we cannot for example export a specific instanciation that would be totally eligible
  • and we cannot directly know their type (a workaround is possible here)

Here is a sample zig program trying to illustrate these problems.

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

pub fn main() void {
    {
        // OK
        const a = add(12, 34);
        warn("add is {x}\n", add); // the function can be referenced as is
        const T = @typeOf(add);
        warn("add is of type {}\n\n", @typeName(T));
    }

    {
        const b = addGeneric(u32, 12, 34); // ok
        // warn("addGeneric is {x}\n", addGeneric);
        // Error : argument of type '*const fn(type,var,var)var' requires comptime.
        // Indeed the types of args are not resolved.

        // const T = @typeOf(addGeneric ???) // impossible

        // But we cannot express the instanciation of addGeneric with specific
        // argument types. This kind of function is a second-class citizen.
        // It cannot be referenced.

        // In this case, it can be worked around like this:

        const workaround = addGenericWorkaround(u32);
        const c = workaround(12, 34);
        warn("addGeneric(u32) is worked around by {x}\n", workaround);

        const T = @typeOf(workaround);
        warn("workaround is of type {}\n\n", @typeName(T));
    }

    {
        variadicFn("foo", false); // ok
        // warn("variadicFn is {x}\n", variadicFn);
        // Error : argument of type '*const fn(var)var' requires comptime.
        // Indeed the types of args are not resolved.

        // But we cannot express the instanciation of variadicFn with specific
        // argument types. This kind of function is a second-class citizen.
        // It cannot be referenced.

        const T = fn ([]const u8, bool) @typeOf(variadicFn("foo", false));
        warn("variadicFn([]const u8, u32) is of type {}\n\n", @typeName(T));
    }

    {
        varFn(.{ "foo", false }); // ok
        // warn("varFN is {x}\n", varFn);
        // Error : argument of type '*const fn(var)var' requires comptime.
        // Indeed the types of args' fields are not resolved.

        // But we cannot express the instanciation of varFn with specific
        // argument types. This kind of function is a second-class citizen.
        // It cannot be referenced.

        // In this case, it can be worked around like this:

        // const args = .{ "foo", false }; // assertion in the compiler, is this allowed ?
        // const workaround = varFnWorkaround(@typeOf(args));

        const Args = struct {
            a: []const u8,
            b: bool,
        };
        const args: Args = .{ .a = "foo", .b = false };
        const workaround = varFnWorkaround(Args);
        workaround(args);
        warn("varFn({}) is worked around by {x}\n", @typeName(Args), workaround);
        // This is not particularly elegant, to say the least.

        const T = fn (Args) @typeOf(varFn(.{ "foo", false }));
        warn("workaround is of type {}\n", @typeName(T));
    }
}

comptime {
    @export("add", add, builtin.GlobalLinkage.Strong); // ok
    @export("addGenericU32", addGenericWorkaround(u32), builtin.GlobalLinkage.Strong); // ok-ish

    // Now maybe I am using variadicFn/varFn for a lot of types, and some instanciations
    // are candidates to being exported as a library, if they don't use exclusive zig features.

    // @export("variadicFn_u32_bool", ???, builtin.GlobalLinkage.Strong); // impossible
    // It's possible to declare the function as export but then you can't control
    // which instanciations are exported, if you want only a subset of what exists
    // in your program.
    // Since actual instanciated function's arguments can be accepted by the ccc,
    // it _could_ have been possible to export it.

    const Args = struct {
        a: u32,
        b: bool,
    };
    // @export("varFn_u32_bool", varFn, builtin.GlobalLinkage.Strong); // impossible
    // @export("varFn_u32_bool", varFnWorkaround(Args), builtin.GlobalLinkage.Strong); // also impossible
}

/// Old-style function with variadic arguments
extern fn variadicFn(args: ...) void {
    // ...
}

/// New-style function with variadic arguments
fn varFn(args: var) void {
    // Used like varFn(.{.foo = "foo", .bar = false });
    // Or varFn(.{"foo", false });
    // Number of fields in the anonymous struct/list is unknown.
    // ...
}

/// New-style workaround
fn varFnWorkaround(comptime T: type) fn (T) void {
    return struct {
        pub fn workaround(args: T) void {
            varFn(args);
        }
    }.workaround;
}

/// Generic function
fn addGeneric(comptime T: type, a: T, b: T) T {
    return a + b;
}

/// Workaround
fn addGenericWorkaround(comptime T: type) extern fn (T, T) T {
    return struct {
        pub extern fn add(a: T, b: T) T {
            return addGeneric(T, a, b);
        }
    }.add;
}

/// Usual dummy function
extern fn add(a: u32, b: u32) u32 {
    return a + b;
}

Even though we can construct the type of specific instanciations for each generic function, it's not a general solution and requires it to be hand-written.

In the end, they are functions but with less features compared to traditional ones.


Functions taking anonymous struct/list arguments also have a flaw that didn't exist before, with variadic arguments: they cannot treat the fields of the var argument as a series of variables, for example to forward them to another function (that isn't itself using anonymous struct/list arguments).

Here's a simple program demonstrating what is possible with variadic arguments, but not with anonymous struct/list currently.

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

/// Practical wrapping of function call via variadic arguments
fn time(comptime fun: var, args: ...) void {
    var timer = std.time.Timer.start() catch unreachable;
    fun(args);
    warn("{} ns\n", timer.read());
}

/// Wrapping of function call with anonymous struct/list
fn time2(comptime fun: var, args: var) void {
    var timer = std.time.Timer.start() catch unreachable;
    // fun(args); // error : bad number of arguments
    // Can't forward arguments to fun
    // The only way to make it work is to refactor every function we want to
    // call via time2 and make it use anonymous struct/list for arguments.
    warn("time2 can't call {}\n", fun);
}

fn sleep() void {
    std.time.sleep(88);
}

var res: u32 = undefined;
fn mul(a: u32, b: u32, c: u32) void {
    res = a * b * c;
}

pub fn main() void {
    warn("timing mul ");
    time(mul, @as(u32, 12), @as(u32, 34), @as(u32, 56));
    time2(mul, .{ @as(u32, 12), @as(u32, 34), @as(u32, 56) });

    warn("timing sleep ");
    time(sleep);
    time2(sleep, {});
}

@ghost
Copy link

ghost commented Nov 13, 2019

The second problem could be addressed with a new builtin called @call which takes a function and a list of arguments. So you could use @call(fun, args); in your time2 function.

@marler8997
Copy link
Contributor

marler8997 commented Nov 14, 2019

To help show the difference between comptime and @OneValueType consider this example:

fn foo(value: var) void;

A new foo will be instantiated for every unique argument type passed to it. For example:

foo(0);
foo(1); // this uses the same instantiation of foo as the previous call
foo(2); // still the same instantiation
foo("hello"); // a different instantiation of foo since it's argument is a different type
foo("there"); // same instantiation as the previous type

Since every @OneValueType(...) is a unique type, this means that each of the following will instantiate a new foo:

foo(@OneValueType(0).init());
foo(@OneValueType(1).init()); // this is a different instantiation of foo than the previous call
foo(@OneValueType(2).init()); // yet another instantiation
foo(@OneValueType("hello").init()); // still another instantiation
foo(@OneValueType("there").init()); // still another instantiation

Note that it's a trade-off between code size and argument size. A new foo will be instantiated for every different @OneValueType that is passed to it, however, since @OneValueType is 0 bits, each version of foo instantiated with @OneValueType has no arguments. It essentially "removes" the argument from the function and makes it act more like a constant rather than an argument.

Also note that @OneValueType implies that it is also comptime. However, adding comptime to any data still allows that variable to be any value of it's type, OneValueType is both comptime and implies it can only ever have a single value. It's basically a "stronger" or more restrictive version of comptime.

@ghost ghost mentioned this issue Nov 20, 2019
@BenoitJGirard
Copy link
Contributor

BenoitJGirard commented Nov 25, 2019

I have been reading and rereading this discussion and I cannot quite grasp what will replace var args; will there be a syntax to declare and initialize anonymous regular structs in one go? The "anonymous struct literal" feature does it, obviously, but only for literals; so the resulting struct cannot contain runtime values, correct?

So is the answer that we'll be able do to something like the following?

var x : u32 = 42;
var y : u32 = 9;
var result : u32 = 0;

var anon_struct = .{x, y, &result};
// anon_struct.@"0" == 42, 
// anon_struct.@"1" == 9, 
// anon_struct.@"2" is a pointer to 'result' variable.

If I missed something obvious, many thanks for pointing it out to me.

@andrewrk andrewrk changed the title remove var args and add anon list initialization syntax but no actual tuples remove var args and add anon list initialization syntax Dec 9, 2019
@tecanec
Copy link
Contributor

tecanec commented Jan 3, 2023

Sorry to comment on an already closed issue, but do we have an issue discussing @OnePossibleValue? Or has it been decided against?

@Vexu
Copy link
Member

Vexu commented Jan 3, 2023

It was briefly discussed again in #3677 but since it wasn't used to implement this feature it didn't get much attention. Feel free to make a proposal for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted This proposal is planned. breaking Implementing this issue could cause existing code to no longer compile or have different behavior. 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