If your language has block expressions, why not use block expressions for all grouping! be free, parentheses! `{ x + 10 } * y`. parens can be unambiguously tuples, just like they always wanted.
@dotstdy I showed this to @ianh and he managed to temporarily convince me (without necessarily holding this opinion himself) that function calls and tuples should use `[...]` instead of `(...)`, and then `(...)` can be used for blocks, after which your same idea applies. Were Lisp M-expressions right all along?

@zwarich @ianh instead of using `[..]` for calls you can use the Lua trick of having functions accept an anonymous structure. `do_stuff { 1, 2, 3 }`

Thus `(..)` is grouping / block, `{..}` is a struct / tuple, and `[..]` is an array.

now we're cooking

@dotstdy @ianh Has any recent language adopted uncurried single arg style where product types are used for multiple args rather than implicit currying? I know that Swift originally started in this direction, but I forget the actual reason why they backed away from this. Can you refresh my memory, @joe ?

@zwarich @dotstdy @ianh @joe Related, but why isn't first class multiple return values a thing more often?

For example, the C++26 standard library has senders/receivers which essentially work via continuations, so you can write async functions that take N inputs and M outputs natively without resorting to tuples. You can even send outputs of different types without having to box them into a sum type (because they just statically dispatch to different overloads).

@foonathan @dotstdy @ianh @joe My guess has always been that first-class multiple return values are overlooked because tuples serve their use case fairly well. When I think of use cases for them, my mind always goes to alternate ABIs like returning bools in status flags, etc.
@zwarich @foonathan @dotstdy @ianh I’m finding that ownership and lifetime dependent values really stretch the equivocation between tuples and multiple values. a tuple of borrows can’t always be used as a borrowed tuple, for instance

@joe @foonathan @dotstdy @ianh I think most languages that would allow you to return borrows would let you also store them as struct fields, etc. and just add a borrowed pointer type, in which case you could just make a tuple of borrows. I guess you could take a purist "parameter modes" approach and define borrowed struct fields via parameter modes on the struct's constructor? However, I think it might be tricky to make this work well with generics.

Another place where a similar distinction comes up is in-place construction of return values. Rust doesn't have this, but I assume that some successor language will want this to support internal self-reference, e.g. for efficient containers with inline capacity

@zwarich @foonathan @dotstdy @ianh yeah return value emplacement was the other thing i had in mind where a tuple (in its naive unexploded representation) isn't the same thing as multiple values.

even with first-class borrows, the way swift tries to allow for tuples to be magically exploded and imploded by the implementation fights against the very concept of a borrow-of-tuple ever existing, since you really want a contiguous representation for that borrow to refer to

@joe @foonathan @dotstdy @ianh Another funny realization is that if all return values are returned by writing to a passed reference, then you could have functions with no actual return values and only out-params (with the appropriate pointer type that must be written before returning). It's the use of resources like registers (which may be implicitly used by other code in the function) that necessitates presenting a value at the point of return.

You could take this to the next level and actually have out-params that "steal" registers when written to, but at that point you're probably in meme language territory.

@zwarich @joe @foonathan @dotstdy @ianh My prediction is that at some point in the next 5 years this whole “systems language” fad will blow over as programmers get tired of constantly being forced to deal with value semantics and unique ownership, and they’re going to rediscover the simplicity and elegance of garbage collection with uniform tagged pointers, at which point they’ll give this paradigm some really dumb name and pretend it’s brand new
@slava @joe @foonathan @dotstdy @ianh I thought about this some more the past few days and one of the things that I keep coming back to is that if you add mutation to your language you’re faced with the problem of data races, and the type system features for the elimination of data races don’t look all the different from ownership systems used by safe systems languages. There is Midori (https://www.cs.drexel.edu/~csg63/papers/oopsla12.pdf), Pony (https://www.ponylang.io/media/papers/fast-cheap.pdf), Verona (https://www.microsoft.com/en-us/research/publication/reference-capabilities-for-flexible-memory-management/), etc. Of course, you could take the Java/C# copout of making data races memory safe (and maybe tack on the Go approach of optional dynamic race detection), but the issues that arise from this model are not very easy to debug.
@zwarich @slava @foonathan @dotstdy @ianh yeah we tried the cop-out approach in Swift for a minute and didn't get too far either. debugging aside, aliasable mutations also pretty much totally kill any non-global optimization. i think "mutation requires exclusivity" is a good default, with Atomic/Cell/RefCell/etc. on the side for cases where you need less stricture, but maybe you can integrate those alternative mutations more smoothly into the language
@joe @zwarich @foonathan @dotstdy @ianh As far as I understand it, Swift's data race safety prevents two kinds of problems:
- the race where you load a reference counted pointer, and someone else releases the last reference and frees the object before you retain it
- tearing when you load or store a value that's larger than a word
In Java, GC prevents the first bug and all values fit in a machine word so they define away the second bug. You might still have a "high level" data race in your application logic, but Swift doesn't prevent those either, right? What data race safety problems does Java have that Swift doesn't, which are difficult to debug?
@slava @zwarich @foonathan @dotstdy @ianh you can never define away logical races, of course, but at least in theory, defining away even the potential for races within code that should never be subject to concurrent accesses reduces the number of places you would have to reason about concurrency correctness. just like having less unsafety is better than everything-is-unsafe, even if you don't eliminate unsafe code entirely