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

discussion: tuples to replace ... #533

Closed
kyle-github opened this issue Oct 11, 2017 · 7 comments
Closed

discussion: tuples to replace ... #533

kyle-github opened this issue Oct 11, 2017 · 7 comments
Milestone

Comments

@kyle-github
Copy link

kyle-github commented Oct 11, 2017

This follows from the discussion in #522 and the use of ....

Currently the only way to pass heterogeneous params is to explicitly list them or use the magic ... syntax. But there is no way to use such a thing in a struct field or anywhere else. The magic only works as the last argument of a function.

Borrowing the idea of tuples from other languages may help. Tuples are often accessed as if they were arrays, but the types of each element are different. Tuples' lengths and component types are known at compile time.

Tuples could help with that and remove, I think, the need for ... entirely.

const tup = #<1,"hello", 42.0>;
assert(@typeOf(tup) == tuple);

fn new_fmt(fmt_str: []u8, args:tuple) -> bool

const worked = new_fmt("hello, {}.  It is {} o'clock.", #<"World",4>);

This is just made up syntax.

Using this would allow you to write a printf-like function without any magic other than inline tuple creation. Since the compiler supports inline struct creation, this would be almost the same, only positions instead of names.

For instance, here is the example printf implementation rewritten to use tuples, and you do not need the ... magic.

pub fn printf(self: &OutStream, comptime format: []const u8, args: tuple) -> %void {
    const State = enum {
        Start,
        OpenBrace,
        CloseBrace,
    };

    comptime var start_index: usize = 0;
    comptime var state = State.Start;
    comptime var next_arg: usize = 0;

    inline for (format) |c, i| {
        switch (state) {
            State.Start => switch (c) {
                '{' => {
                    if (start_index < i) %return self.write(format[start_index...i]);
                    state = State.OpenBrace;
                },
                '}' => {
                    if (start_index < i) %return self.write(format[start_index...i]);
                    state = State.CloseBrace;
                },
                else => {},
            },
            State.OpenBrace => switch (c) {
                '{' => {
                    state = State.Start;
                    start_index = i;
                },
                '}' => {
                    if (args.len == next_arg) {                // <--- new, use .len just like an array
                        @compileError("Not enough arguments!");
                    }
                    %return self.printValue(@typeOf(args[next_arg]); 
                    next_arg += 1;
                    state = State.Start;
                    start_index = i + 1;
                },
                else => @compileError("Unknown format character: " ++ c),
            },
            State.CloseBrace => switch (c) {
                '}' => {
                    state = State.Start;
                    start_index = i;
                },
                else => @compileError("Single '}' encountered in format string"),
            },
        }
    }
    comptime {
        if (args.len != next_arg) {
            @compileError("Unused arguments");
        }
        if (state != State.Start) {
            @compileError("Incomplete format string: " ++ format);
        }
    }
    if (start_index < format.len) {
        %return self.write(format[start_index...format.len]);
    }
    %return self.flush();
}

I think there are only two changes. The first is to use the new type tuple for the arguments. The second is the introduction of a test for the argument tuple length in the part that generates the output code for the argument. This allows both argument overflow and underflow to be checked. Underflow was not checked before.

I am not sure that this is otherwise valid syntax, but hopefully the idea carries across anyway.

You can get much the same effect if you allow inline creation of []var. That works as well. I think that it is a little different in that you allow any value at any position whereas a tuple would be implicitly typed at each position. I.e. you could not assign the wrong type to a tuple position.

var foo = #<1.0, "hello">;
assert(foo.len == 2); // OK
foo[0] = 12.3; // OK
foo[1] = 6; // compile error
foo[1] = "bring me a shrubbery!";   // OK

So while a var array gives you some of the flexibility, I do not see how it would be able to give you the compile time type checking. A tuple is like a struct without the names.

@tiehuis
Copy link
Member

tiehuis commented Oct 11, 2017

Previous discussion here about the general tuple idea: #208

@tiehuis tiehuis added proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. and removed proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. labels Oct 11, 2017
@PavelVozenilek
Copy link

const worked = new_fmt("hello, {}. It is {} o'clock.", #<"World",4>);

could be replaced with

const worked = new_fmt("hello, {}. It is {} o'clock.", "World", 4);

where the compiler constructs the tuple from provided parameters, w/o explicit syntax.

@kyle-github
Copy link
Author

@tiehuis, oops, missed that one, thanks!

The use cases I see are:

  • replace the magic of ... with a uniform, explicit method of grouping varying numbers and types of values.
  • use the above above to allow for varying number of arguments in functions.
  • use the above to provide for multiple result returns from functions.
  • use the above to provide for multiple value/type combinations as single values in arrays and structs.
  • use the above to allow for compile time type checking and assignment to single values in tuples.

@PavelVozenilek, the problem with having the compiler do it is that it is still "magic." How would I assign a tuple to a field in a struct using your syntax? How would I pass a tuple as one of the middle arguments in a function, but not the last one?

The problem I see with the current use of ... is that it is an exception to many rules. For it to work, it must be the last argument of a function. It is sort of like an array, but not. JavaScript went down that path and has regretted it ever since. ... does not result in a value with a type. So it is not part of the type system. I cannot pass the result of ... to another function or store it in an array or a struct field. Can I assign values to some element in ...? In short, it is magic. It is nothing but edge cases.

@PavelVozenilek
Copy link

@kyle-github: yes, it is magic, special case, but "natural" and easy to use. People tend to avoid correct but clumsy solutions (e.g. C++ streams).

Ordinary tuple in the middle should be passed via the #<> syntax, only the trailing ... equivalent would have this support.


As an aside, some languages, e.g. Kotlin and Nim, support "trailing inlined function definition", something like:

sort(array) { return _1 < _2 };

where the { return _1 < _2 } part is anonymous function passed as parameter. Trivial syntax, no need to invent yet another name or to use clumsy lambdas.

@kyle-github
Copy link
Author

@PavelVozenilek, err, not sure what you mean by avoiding C++ streams. You do not see many calls to printf in C++ anymore. Everything is "<<".

Here is an alternate proposal that builds on what is already in Zen a little more cleanly.

  • allow []var arrays (if they do not already exist)
  • allow slices of []var arrays.
  • create a built-in @getArgs() that returns a slice of the current arguments to a function that are actually passed on the stack.
  • Allow the use of ... simply as a bare signifier that the function takes variable numbers of args.
fn foo(i:i32, f:f32, ...) -> i32 {
     var args = @getArgs();

     // now process args as a slice of type []var.
     // the first two elements are i and f.
     // args is a completely normal slice.
     // args.len gives you the number of arguments.
}

var result = foo(42, 42.0, "Hello", biscuit+1, a_file_struct_ptr);

var tuple_thingy = []var{"hello, world", rectangle, 16.64};

That would support the variadic case but still provide a less "magic" foundation for it than all the special cases around the current ... use.

Since I know nothing about how the compiler is implemented I cannot comment on how hard this would be. I suspect that what could be done would be to mark functions with ... at the end as variadic and when they are called, the compiler would drop an extra argument indicating the number of arguments and their types on the stack. This argument would be "hidden" in the same way that this is sort of implicit but could be used by @getArgs() to retrieve a slice on the arguments. Clearly I am waving my hands on that part!

@PavelVozenilek
Copy link

PavelVozenilek commented Oct 12, 2017

@kyle-github: C++ streams are not seen as the final solution. I remember many libraries like Boost.Format.

I do not have proposal how to implement varargs internally, only pointed out that their use should be simple, that even single excessive character is one too many.

I use varargs quite a lot: for detailed asserts (like assert(x > y, "x = %d, y = %d", x, y);) and for white-box testing (inspired by this).

@andrewrk
Copy link
Member

andrewrk commented Jun 1, 2018

Re-opening the tuple issue: #208

@andrewrk andrewrk closed this as completed Jun 1, 2018
@andrewrk andrewrk modified the milestones: 0.4.0, 0.3.0 Sep 28, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants