Why aren't tuples as return values used as much?

I recently realized why don’t I use tuples as much, and was wondering if others share the same thoughts?

This is totally based on a feeling I have, for which I don’t have empirical evidence. But strolling trough the std lib for example, the Ziggiest code AFAIK, there aren’t many return values as tuples. This seems like a good idea at first:

var v1, var v2 = function();

I tried using them in my projects, but as I’ve never seen a real life usage for them in public APIs, it kinda looks ugly to me. Especially when you want to be verbose with variable naming, you get into this ugly situation:

var verbosly_named_variable1, var verbosly_named_variable2 = function();

But the function also needs some arguments, so now it looks like this:

var verbosly_named_variable1, var verbosly_named_variable2 = function(argument1, argument2, argument3);

And if you need to handle errors, just don’t do it:

var verbosly_named_variable1, var verbosly_named_variable2 = function(argument1, argument2, argument3) catch |err| {handle(err);};

You can do it like this I guess:

var verbosly_named_variable1, var verbosly_named_variable2 = function(    
     argument1,
     argument2,
     argument3,) catch |err| {handle(err);};

We quickly run into a line of code you’d need a huge buffer for. And there is nothing really you can do. So more often than not I ditch tuples, especially as I use a pretty big font while coding, and have a small code buffer.

Also have in mind that the function definition will have a long return type struct {type1, type2}, and if you’ve came that far, you might as well declare it as a struct instead of a tuple, and use it elsewhere struct {value1: type1, value2: type2}

Is this the reason people don’t use them? Are regular structs almost always more practical?

I think it comes down to library design vs. executable design.
The standard library is necessarily a library, so it’s designed in library-like ways where it’s fairly likely that if you want one function to return a type, you want other functions to also return the same type.
Tuple return types and destructuring are pretty great for one-shot helper functions - this makes it well-suited for executable design where you want a function to do one thing, not have several possible uses and be integrated with many other functions.

Still, it really isn’t much more effort to declare the struct before using it as a return type, as opposed to writing it inline.

IMHO: because the one useful scenario (destructuring structs) isn’t supported :slight_smile:

E.g. coming from TS/JS which has pretty great syntax for working with structs and arrays I would expect a destructuring feature set like this:

// a struct with x,y,z
const bla: MyStruct = .{ .x = 1, .y = 2, .z = 3 };
// destructure bla.x, bla.y, bla.z into consts x, y, z
const { x, y, z } = bla;
// only destructure some elements
const { x, z } = bla;
// destructure struct items into differently named consts
const { x: my_x, y: my_y, z: my_z } = bla;
// nice to have: destructure bla.x, bla.y into const x, y and bla.z into var z
const { x, y }, var { z } = bla;
// ...but no big deal splitting this into two lines:
const { x, y } = bla;
var { z } = bla;
// ...being able to do it as one assignment would be nice
// for destructuring function return values though

…similar for array destructuring - but IMHO that’s not as important as struct destructuring:

const arr: [5]u8 = .{ 1, 2, 3, 4, 5 };
// destructure into five u8
const [ a, b, c, d, e ] = arr;
// destructure first two items, and rest into a slice
const [ a, b, ...cde ] = arr;
11 Likes

Andrew wants to remove this syntax, I’m not sure why.

I often use this syntax when designing APIs that interact with users, especially when the return value includes a pointer for the user to use. In this case, I strongly dislike giving the user a struct which includes a pointer.

1 Like

I use them very occasionally in private code, but they never seem appropriate to me in a public API. The syntax just seems like a lazy hack to avoid declaring a struct. Using out parameters always seem like a better solution when I do need to return multiple values without a dedicated result struct.

4 Likes

TS/JS which has pretty great syntax for working with structs and arrays

I still find it hard to read this kind of stuff. It leads to some crazy syntax imo, just going through the examples from Mozilla’s javascript documentation: Destructuring - JavaScript | MDN

It reminds me a little of in C, with how weird array & pointer syntax can get. Like it can really be “abused”, even if it has some awesome use cases.

1 Like

Small tangent, but I don’t interpret that comment to mean Andrew wants to remove the de-structuring syntax. Rather, I think he’s being adamant about his unwillingness to allow the specific syntax that was proposed in that issue.

I was just a little surprised after reading your comment, so I figured I’d add chime in after reading what you were referring to.

9 Likes

I always thought that “Anyone who wants this is abusing tuples and should be using structs instead” was criticizing “function-level destructuring”, and perhaps there was some misunderstanding here.

However, the first sentence does express the desire to remove “function-level destructuring” rather than just the “container-level destructuring” proposed in this issue.

No, it does not. Consider a phrase like “I would sooner starve than eat broccoli.” The intended meaning is definitely not “I want to starve.”

9 Likes

Oh, I misunderstood all along, I’m so sorry!

5 Likes

It’s an easy mixup. Nothing to apologize for. Cheers!

3 Likes

My 2 cents, I prefer something like this:

const std = @import("std");

fn foo(a: u32, b: u32) struct { a: u32, b: u32, x: u32 } {
    if (a == 0 or b == 0) {
        return .{
            .a = 0,
            .b = 0,
            .x = 0,
        };
    } else {
        return .{
            .a = a,
            .b = b,
            .x = a + b,
        };
    }
}

pub fn main() !void {
    const bar1 = foo(1, 0);
    std.debug.print("{}\n", .{bar1.x});

    const bar2 = foo(1, 2);
    std.debug.print("{}\n", .{bar2.x});
}

I’ts easy to read and understand (at least for me).

10 Likes

I tend to favorise tuple when all fields of the returned struct are important and need to be used.

Eg you return two allocated slices that need freeing.

But there isn’t that many API where this is the case

4 Likes

The pattern of returning tuples as a way to convey “the callee thinks the caller should use all of these values” is the same pattern I was toying with as an alternative Allocator interface here. My example in that thread doesn’t work exactly as I described it (since there are no closures in Zig :face_with_tongue:), but I still think the idea is generally interesting, and I think using tuples to convey “all these values should be used” is a good idea.

4 Likes

I would write that as:

var verbosly_named_variable1, var verbosly_named_variable2 = blk: {
    const intermediate = function(argument1, argument2, argument3,) catch |err| {
        handle(err);
    };

    break :blk intermediate;
};

However, I’m not allergic to long lines or heavy indents since I always program on big monitors. (Laptop keyboards always damaged my hands and laptop screens are annoying with my eyesight).

As a general rule, I tend to use scopes and break :blk val a lot in Zig. Not being able to reuse a variable name pretty much forces that style if you use C libraries that return values for errors. You wind up with chains of:

{
    const retval = c_library_fn1();
    if (retval != 0) {remap_to_zigerror_1();}
}
{
    const retval = c_library_fn2();
    if (retval != 0) {remap_to_zigerror_2();}
}
...
{
    const retval = c_library_fn7();
    if (retval != 0) {remap_to_zigerror_7();}
}

I have personally found tuple destructuring useful when I want to return from a procedure two or more parts of a larger single structure computer within itself, and then assigning one of such value to a dereferenced pointer and the rest of the values to scope bound consts.

I don’t think it’s ever a good idea to return a tuple from a function just to facilitate destructuring. Even if it’s obvious to most readers what something like fn sinCos(x: f32) struct { f32, f32 } returns, it would never hurt to return a struct with explicitly named fields like struct { s: f32, c: f32 } instead.

Using destructuring for returning multiple values from a block however is clearly useful since it avoids polluting the outer scope with an unused identifier.

const s: f32, const c: f32 = sc: {
    const result = sinCos(x);
    break :sc .{ result.s, .result.c };
};
const result = ...; // can use this identifier for something else
3 Likes

If only trailing commas in destructuring assignments were supported, I would format it like this:

var verbosly_named_variable1,
var verbosly_named_variable2,
= function(argument1, argument2, argument3) catch |err| {
    handle(err);
};

I hope the core team will eventually be able to be swayed on this, but for now you can force this formatting with comments:

var verbosly_named_variable1, //
var verbosly_named_variable2 //
= function(argument1, argument2, argument3) catch |err| {
    handle(err);
};
3 Likes

Lol I didn’t know about that loophole, it feels illegal xD

I completely agree. However, in many cases (but not all) when the types of the tuple items are different, there is no confusion or possibility of errors. The best rule for using tuples (in all languages) is to only use them in those cases where there is no possible confusion. This rule is not limited to destructuring of tuples, it is important for almost all uses of tuples. So if a language has tuples at all (which Zig does), this is something to always keep in mind.

2 Likes