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 but no actual tuples #208

Open
fsaintjacques opened this Issue Oct 28, 2016 · 36 comments

Comments

Projects
None yet
@fsaintjacques
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

This comment has been minimized.

Member

thejoshwolfe commented Nov 9, 2016

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.

@andrewrk andrewrk changed the title from Tuple type to Add tuples and remove var args Jun 1, 2018

@andrewrk

This comment has been minimized.

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], ...

@binary132

This comment has been minimized.

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

This comment has been minimized.

Contributor

alexnask commented Jun 18, 2018

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

This comment has been minimized.

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

This comment has been minimized.

Contributor

alexnask commented Jun 18, 2018

@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

This comment has been minimized.

Contributor

BarabasGitHub commented Jul 6, 2018

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

This comment has been minimized.

montychen commented Jul 7, 2018

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

This comment has been minimized.

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

This comment has been minimized.

Member

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

This comment has been minimized.

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

This comment has been minimized.

Member

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

This comment has been minimized.

Member

Hejsil commented Oct 17, 2018

@allochi Both

@allochi

This comment has been minimized.

allochi commented Oct 17, 2018

@Hejsil this is cool, thanks!

@binary132

This comment has been minimized.

binary132 commented Oct 17, 2018

Nice proposal @Hejsil!

In my opinion, the return syntax where a struct or array is created and then immediately destructured in the assignment may appear to the user to cause behavior they don't want or expect. Destructuring and pattern matching are really great language features, but this doesn't appear to be that, exactly.

More generally, as a curious hopeful, but not a native Zig user yet, I feel that some of its syntax is getting a little bit ugly and complicated. It's not C++ yet, but it's moving farther away from let's say Python.

I hope that Zig retains its focus on clean, straightforward semantics and doesn't try to get too fancy. I definitely prefer to write a little bit of extra code rather than to study and read a convoluted syntax, especially one which does not lend itself to fast reading and review.

Fast reading and review is one of the greatest strengths of Go, since it is focused on making team usage of the language scalable.

@Hejsil

This comment has been minimized.

Member

Hejsil commented Oct 17, 2018

@binary132 We've talked a lot about this on IRC, and it most of us agree (i think) that destructing structs is a bad idea. You nearly get all the use cases without needing destructuring. The only thing we won't get is the ability to have a per variable const/var ness.

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

// This would be fine
const res = blk: {
    ...
    break :blk .{.min = min, .max = max};
};
assert(res.min == 2 and res.max == 5);

I also agree that we should keep the language syntax as easy to read as possible for both humans and machines. Is there any pacticular syntax you'd wonna point out?

@BarabasGitHub

This comment has been minimized.

Contributor

BarabasGitHub commented Oct 17, 2018

I'm always torn on whether to use a quick tuple to return multiple things from a function or to make an actual struct especially for this case.

The annoying thing about structs is that you have to assign to a new variable and then manually 'deconstruct' it into separate variables. Because often you don't require them to be together in memory, it's just a way to return multiple variables from a function. I also always disliked the asymmetry between input and output, where input are all separate parameters and can be many while there's only one single output (parameter). I quite like how this works in Python (however Python is too flexible) where this syntax is fine

a, b, c = function(1, 2, 3)

and a, b and c can either be new variables or existing ones.

@bheads

This comment has been minimized.

bheads commented Oct 17, 2018

@BarabasGitHub Whats the big gain with them being destructed? The results are related in some fashion.

@BarabasGitHub

This comment has been minimized.

Contributor

BarabasGitHub commented Oct 17, 2018

@Hejsil

This comment has been minimized.

Member

Hejsil commented Oct 18, 2018

@BarabasGitHub
Maybe we should have "multiple return values", and then see how often you actually need to destructure them. If the answer is "rarely", then is it really worth having a feature for it?
I'd argue, that "multiple return values" in them self are pretty rare to find in code, but maybe that's just the way I'm used to programming.

@winksaville

This comment has been minimized.

Contributor

winksaville commented Oct 18, 2018

@Hejsil, I'd say zig error handling, error!XXX, syntax is a form of multiple return and used very often.

@Hejsil

This comment has been minimized.

Member

Hejsil commented Oct 18, 2018

@winksaville Well, I was referring to Zig code, where the "multiple return value" feature would be useful. In my 5000 line code project, I've only written code where "multiple return values" would be useful once.

@winksaville

This comment has been minimized.

Contributor

winksaville commented Oct 18, 2018

@Hejsil

This comment has been minimized.

Member

Hejsil commented Oct 18, 2018

@winksaville I get what you're saying, but I'm talking about the language feature, and not about the concept of returning more than one value. I'm saying, that the language feature "multiple return values" would be rarely used, because most of the times when you wanna return multiple values, it's either an error and a value (we have errorsets for that) or related things that can have a common name (use a named struct). My example showed a case, where it was neither, but it is one case in 5000 lines of code.

Edit: Ofc, if you are a library author, especially for lib with generic functions, you need for "multiple return values" would probably be higher. This is why we need more code being written in Zig, so we can evaluate if a feature is really worth maintaining.

@winksaville

This comment has been minimized.

Contributor

winksaville commented Oct 18, 2018

@andrewrk

This comment has been minimized.

Member

andrewrk commented Oct 18, 2018

Here's one use case for multiple return values: @addWithOverflow

@addWithOverflow(comptime T: type, a: T, b: T, result: *T) bool

Ideally this wouldn't need a result pointer argument. Really it wants to return struct {result: T, overflow: u1. Although I don't think any language changes are really needed for that. But the status quo usage is:

pub fn parseU64(buf: []const u8, radix: u8) !u64 {
    var x: u64 = 0;

    for (buf) |c| {
        const digit = charToDigit(c);

        if (digit >= radix) {
            return error.InvalidChar;
        }

        // x *= radix
        if (@mulWithOverflow(u64, x, radix, &x)) {
            return error.Overflow;
        }

        // x += digit
        if (@addWithOverflow(u64, x, digit, &x)) {
            return error.Overflow;
        }
    }

    return x;
}

Somehow this actually looks cleaner than if there were multiple return values:

pub fn parseU64(buf: []const u8, radix: u8) !u64 {
    var x: u64 = 0;

    for (buf) |c| {
        const digit = charToDigit(c);

        if (digit >= radix) {
            return error.InvalidChar;
        }

        // x *= radix
        const mul = @mulWithOverflow(u64, x, radix);
        if (mul.overflow) return error.Overflow;
        x = mul.result;

        // x += digit
        const add = @addWithOverflow(u64, x, digit);
        if (add.overflow != 0) return error.Overflow;
        x = add.result;
    }

    return x;
}

And special unpacking syntax doesn't really make it better:

pub fn parseU64(buf: []const u8, radix: u8) !u64 {
    var x: u64 = 0;

    for (buf) |c| {
        const digit = charToDigit(c);

        if (digit >= radix) {
            return error.InvalidChar;
        }

        // x *= radix
        x, const overflow_bool = @mulWithOverflow(u64, x, radix);
        if (overflow_bool) return error.Overflow;

        // x += digit
        x, const overflow_bit = @addWithOverflow(u64, x, digit);
        if (overflow_bit != 0) return error.Overflow;
    }

    return x;
}
@BarabasGitHub

This comment has been minimized.

Contributor

BarabasGitHub commented Oct 18, 2018

@Hejsil

This comment has been minimized.

Member

Hejsil commented Oct 23, 2018

Or! We could just implement tuples in userland :)

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

fn sizeOfTypes(comptime types: []const type) comptime_int {
    var res = 0;
    for (types) |T|
        res += @sizeOf(T);

    return res;
}

fn append(comptime arr: var, comptime item: var) [arr.len + 1]@typeOf(item) {
    return arr ++ []@typeOf(item).{item};
}

pub fn Tuple(comptime types: var) type {
    return struct.{
        data: [sizeOfTypes(types)]u8,

        pub fn at(t: @This(), comptime i: usize) types[i] {
            const T = types[i];
            const offset = sizeOfTypes(types[0..i]);
            return @bytesToSlice(T, t.data[offset..][0..@sizeOf(T)])[0];
        }

        pub fn add(t: @This(), item: var) Tuple(append(types, @typeOf(item))) {
            const T = @typeOf(item);
            var res = Tuple(append(types, @typeOf(item))).{ .data = undefined };
            mem.copy(u8, res.data[0..], t.data);
            mem.copy(u8, res.data[t.data.len..], @sliceToBytes(([]T.{item})[0..]));
            return res;
        }
    };
}

const Zero = Tuple([]type.{}).{.data = []u8.{}};

test "" {
    const t = Zero.add(u64(22)).add(f32(33)).add("333");
    const a = t.at(0);
    const b = t.at(1);
    const c = t.at(2);
    debug.assert(a == 22);
    debug.assert(b == 33.0);
    debug.assert(mem.eql(u8, c, "333"));
}
@bheads

This comment has been minimized.

bheads commented Oct 23, 2018

@Hejsil D implemented tuples in user land, and it was a mess and failure. Also @andrewrk's addWithOverflow example wouldn't work as it would require the usage of the stdlib . I really think inferred initialization will give us varargs, multiple return types, and better enum syntax.

@Hejsil

This comment has been minimized.

Member

Hejsil commented Oct 23, 2018

I'll throw another idea into the mix. This one does not argue for or against tuples in any way, but only argues that we probably don't need var args (or a feature that emulates it).

Let's take a step back and look at the use cases for var args, so we have a better idea of how to replace it.

If we grep std, these are the places var args are used:

// Formatted output.
buffer.zig
57:pub fn allocPrint(allocator: *Allocator, comptime format: []const u8, args: ...) !Buffer {

build.zig
205:pub fn addLog(self: *Builder, comptime format: []const u8, args: ...) *LogStep {
649:pub fn fmt(self: *Builder, comptime format: []const u8, args: ...) []u8 {

io.zig
202:pub fn print(self: *Self, comptime format: []const u8, args: ...) !void {

debug/index.zig
42:pub fn warn(comptime fmt: []const u8, args: ...) void {
143:pub fn panic(comptime format: []const u8, args: ...) noreturn {
152:pub fn panicExtra(trace: ?*const builtin.StackTrace, first_trace_addr: ?usize, comptime format: []const u8, args: ...) noreturn {

zig/parser_test.zig
1267:\\fn print(args: ...) void {}

fmt/index.zig
15:pub fn format(context: var, comptime Errors: type, output: fn (@typeOf(context), []const u8) Errors!void, comptime fmt: []const u8, args: ...) Errors!void {
849:pub fn bufPrint(buf: []u8, comptime fmt: []const u8, args: ...) ![]u8 {
857:pub fn allocPrint(allocator: *mem.Allocator, comptime fmt: []const u8, args: ...) AllocPrintError![]u8 {
1301:fn testFmt(expected: []const u8, comptime template: []const u8, args: ...) !void {


// Pass args to child call.
event/channel.zig
160://pub async fn select(comptime EnumUnion: type, channels: ...) EnumUnion {

event/group.zig
64:pub fn call(self: *Self, comptime func: var, args: ...) (error.{OutOfMemory}!void) {
66:async fn asyncFunc(node: **Stack.Node, args2: ...) ReturnType {

event/loop.zig
560:pub fn call(self: *Loop, comptime func: var, args: ...) !(promise->@typeOf(func).ReturnType) {
562:async fn asyncFunc(loop: *Loop, handle: *promise->@typeOf(func).ReturnType, args2: ...) @typeOf(func).ReturnType {


// There is no reason these should be var args functions.
mem.zig
586:pub fn join(allocator: *Allocator, sep: u8, strings: ...) ![]u8 {

os/zen.zig
25:pub fn to(mailbox_id: *const MailboxId, msg_code: usize, args: ...) Message {

os/path.zig
38:pub fn join(allocator: *Allocator, paths: ...) ![]u8 {
46:pub fn joinWindows(allocator: *Allocator, paths: ...) ![]u8 {
50:pub fn joinPosix(allocator: *Allocator, paths: ...) ![]u8 {
316:pub fn resolve(allocator: *Allocator, args: ...) ![]u8 {

As we can see std gives us 2 use cases:

  • Formatted output
  • Pass args to a child call

These patterns are not very common in code. Formatted output is the most common, but do we really want a feature that is mostly used for formattings text? There are other ways we could implement std.format. One way is to construct a type, simular to my tuple example, that is specific to formatting. Another way, would be to pass a Formatter array, where a Formatter knows how to output the value it wraps (this would be made easier with #130).

As for passing args along, this is probably more related to #1048.

So is there really a use case for a var args like feature? Idk, but I can imagine other solutions where var args is used currently.

@Hejsil

This comment has been minimized.

Member

Hejsil commented Oct 23, 2018

@bheads The example I showed was mostly meant to show, that we have a way of generating a type that groups values of different types (aka a replacement for var args, not multiple return values). With good design, we could, therefore, implement something similar, that is meant specifically for formatting.

@nodefish

This comment has been minimized.

nodefish commented Oct 23, 2018

Tuples are really just an indexed shorthand for a struct. Coolness notwithstanding, a userland implementation is inevitably gonna be more verbose than the status quo of using a struct.

@andrewrk

This comment has been minimized.

Member

andrewrk commented Nov 21, 2018

Update: No Tuples

  • Remove var args is still happening
  • Do the anonymous enum initialization proposal (#683)
  • Also do anonymous struct initialization proposal (#685)
  • Also introduce anonymous array initialization syntax: .{a, b, c}. This one will implicitly cast to arrays, if all the types are the same, otherwise it is syntactic sugar for .{._0 = a, ._1 = b, ._2 = c}.
  • structs are allowed to contain a mix of comptime and runtime values.
  • std.fmt.format does something like this: format(comptime fmt: []const u8, args: var) and then uses compile-time reflection to iterate over args, as if it were a struct.
    std.debug.warn("foo: {} bar: {}\n", .{"hi", i32});
@tgschultz

This comment has been minimized.

Contributor

tgschultz commented Nov 21, 2018

Also introduce anonymous array initialization syntax: .{a, b, c}. This one will implicitly cast to arrays, if all the types are the same, otherwise it is syntactic sugar for .{._0 = a, ._1 = b, ._2 = c}.

In the format example, if you instead do:
std.debug.warn("foo: {} bar: {}\n", .{"hi", "bye"});
you get an array instead of a struct, which means you'll need to iterate over it differently, which means you'll need to switch on @typeOf(args) .

@andrewrk

This comment has been minimized.

Member

andrewrk commented Nov 21, 2018

This type would implicitly cast to an array, but when supplied for a var parameter or if it otherwise never gets casted, then it would become an anonymous struct initialization .{._0 = a, ._1 = b, ._2 = c}. If the implicit casting to array thing becomes an issue for some other reason we can drop that aspect of it.

@andrewrk andrewrk changed the title from Add tuples and remove var args to remove var args and add anon list initialization syntax but no actual tuples Nov 21, 2018

@kyle-github

This comment has been minimized.

kyle-github commented Nov 22, 2018

There's something about the idea that the type of .{a, b, c} is context/content dependent that I find a little troubling. Am I missing some part of this?

I think the idea of the anonymous array idea is good. I am just not sure that having an implicit cast/coercion is a good idea. It seems to violate Zig's point about being explicit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment