language icon indicating copy to clipboard operation
language copied to clipboard

Multiple return values

Open mit-mit opened this issue 5 years ago • 108 comments

A partner team has requested support for multiple return values for functions, for example:

Future<double> lat, long = mapService.geoCode('Aarhus');
double lat, long = await mapService.geoCode('Aarhus');

mit-mit avatar Oct 31 '18 14:10 mit-mit

This feature is related to parallel assignments, which has been raised a couple of times before.

A problem with over-encouraging the use of multiple returns (with multiple types) is that they're not very self-documenting. In Python, it's easy to creep into patterns like:

def get_box_data(...):
	...
	return (((x1, y1), (x2, y1), (x1, y2), (x2, y2)), (r, g, b, a), z)

This function returns the corners of a rectangle, its color, and its depth buffer value. At this point, it would be better off explicitly stored in an object.

Golang enforces some rules on multiple returns to avoid this behavior (among other reasons).

func test() (int, int) {
  return 1, 2
}

var a, b int
a = test() // ERROR
a, b = test() // Okay; a = 1, b = 2
a, b = test(), 2 // ERROR
a, b = b, a // Okay; a = 2, b = 1
fmt.Println(test()) // Okay; prints '1 2'
fmt.Println(test(), test()) // ERROR

Additionally, Golang doesn't allow you to unpack tuples. This avoids some confusing patterns such as:

def test(): 
  return 1, 2

*(a, b), c = 0, *test()
# a = 0, b = 1, c = 2

which is valid Python.

Adopting a stricter system similar to Golang's may be a reasonable tradeoff:

  1. Multiple return types can only be specified in the function signature. Return statements can only specify multiple returns from within functions with multiple return types (and their counts must be equal).
    // Valid
    (int, List<int>) myfunc() {
        return 0, [1, 2, 3];
    }
    
    // Identical to above
    (int, List<int>) myfunc() {
        return (0, [1, 2, 3]);
    }
    
    // Invalid
    myfunc() {
        return 0, [1, 2, 3];
    }
    
  2. Multiple return types must specify "actual" types (not nested multiple return types).
    // Invalid
    (int, (int, int)) myfunc() {
       ...
    }
    
  3. It is illegal to invoke a function with multiple return types alongside operands. That is, a function with multiple return types must fully satisfy its enclosing function/operation.
    (int, List<int>) myfunc() {
        return 0, [1, 2, 3];
    }
    
    void f1(int x, List<int> y) {
    	  ...
    }
    
    void f2(int x, List<int> y, int z) {
       ...
    }
    
    int a;
    List<int> b;
    int c;
    
    a, b = myfunc(); // Valid
    a, b, c = myfunc(), 0; // Invalid
    f1(myfunc()); // Valid
    f2(myfunc(), 0); // Invalid
    

Markzipan avatar Nov 01 '18 05:11 Markzipan

It would be great if Dart had builtin support for a tuple type, and provided syntactic sugar for destructuring tuples:

typedef LatLng = Tuple(double, double);

class MapService {
  Future<LatLng> geoCode(String name) {
    // ...
  }
}

var mapService = MapService();

// This is valid:
var latng = await mapService.geoCode('Aarhus');
var lat, lng = latlng;

// This is also valid:
var lat, lng = await mapService.geoCode('Aarhus');

This way we could avoid the complexity of multiple returns. The only thing we need now is to enable the syntactic sugar for destructuring tuples. We could also use fixed-length lists instead of tuples here, but I believe tuples provide a better type primitive.

mdebbar avatar Nov 02 '18 03:11 mdebbar

Another way might be going the Standard ML way and just define product types like this: type LatLng = double * double If introducing another keyword doesn't seem worth it: typedef LatLng is double * double; this with #83 could also allow Dart to have full blown pattern matching feature that came up also in #27.

edit: missing column.

GregorySech avatar Nov 05 '18 07:11 GregorySech

Yes! Destructuring! I think leaving the typesystem alone for this is fine. Array seems like a good enough default, (if packing results in an array) and recast to a tupleN type on assignment (as mentioned above).

I would just like to mention the addition of some nicer destructuring patterns (well better than JavaScript's anyway) and more inline with those found in clojure. (https://gist.github.com/john2x/e1dca953548bfdfb9844) specifically the 'rest' like behavior.

bjconlan avatar Dec 16 '18 22:12 bjconlan

Kotlin uses the data class (value class) to implement the destructing and multiple return values (https://kotlinlang.org/docs/reference/data-classes.html#data-classes-and-destructuring-declarations). This is not perfect but works in most of the cases. Would be great if dart has built in support for value types.

sureshg avatar Dec 17 '18 03:12 sureshg

See also #207

andreashaese avatar Feb 07 '19 16:02 andreashaese

I think we really need multiple return values in Dart. How can we say that Dart is type checked if I'm forced to return List and then like in the stone age, do a list lookup one by one and assign it to proper named variables, casting to the right object including all the danger that comes with list array access and casting. I mean surely this is a horrible state of affairs:

    List returnValues = branchValid();
    bool branchValid = returnValues[0] as bool;
    String branchType = oldBranchValidity[1] as String;

Meai1 avatar Apr 15 '19 05:04 Meai1

How can we say that Dart is type checked if I'm forced to return List

I've never been forced to do this - nothing has prevented me from writing a class that contains correctly typed fields for each value I want to return.

natebosch avatar Apr 16 '19 19:04 natebosch

@natebosch crazy to make a class just to return something

Meai1 avatar Apr 16 '19 20:04 Meai1

@Meai1 he probably means a Couple<T, Q> class which can be reused. It's not pretty but it's nothing crazy.

GregorySech avatar Apr 16 '19 21:04 GregorySech

Yeah, if we're doing types I think @gregorysech pointed out the common solution for the scenario (tupleN generics) but destructuring is more about shapes (but I guess have to be realised as types at some point, explicitly or otherwise)

bjconlan avatar Apr 17 '19 00:04 bjconlan

@natebosch a class is probably sufficient for a certain percentage of use-cases. The issue is that you would be allocating an extra object on the heap just to return something. I'm hoping that the request here is to return everything via the stack.

yjbanov avatar Apr 17 '19 15:04 yjbanov

@yjbanov - yup - I think this feature could make things a lot nicer and potentially more efficient, I'd really like to see it happen! The current situation isn't doom and gloom 😄

natebosch avatar Apr 17 '19 15:04 natebosch

I actually think that it would be very nice to get named return values for this as well, so that we dont have to guess what an int, int might be. This would also help with auto generating in the IDE where you could auto create the lefthand side of a function call with parameters that have the right kind of names already. It would also help with when I'm scrolled all the way down in some long method or function and I dont remember which e.g String is supposed to be returned at which position, then if we have named return values I could get autosuggestions right after typing return and then it would tell me the name and obviously the order of parameters that I'm supposed to be returning.

Suggested syntax:

int x, int y blabla() {
   return x: 5, y: 6;
}

int x, y = blabla();

Slightly related: I am always a bit confused when I see a List<String, String> and there is just absolutely no indication of what kind of Strings should be in there. In Typescript they have the advantage of being able to do type MyString = "Blo" | "Bla"; which is very good but what if I just want to document what kind of strings should go inside, so I want to say: these should be database names or phone numbers.

type MyString = databaseNames : "Blo" | "Bla"; or if any String is allowed, just type MyString = databaseNames : String;

Meai1 avatar Apr 23 '19 10:04 Meai1

Destructuring syntax would be a great addition to Dart! Coming to Dart from Kotlin I wish destructuring declarations were available in Dart, they make coding easier.

acherkashyn avatar May 15 '19 17:05 acherkashyn

Destructuring is more than enough to solve that issue. Just give us a nice destructuring with a easier tuple creation. (Kotlin does it using the 'to' method).

(take a look on how kotlin do that in conjunction with Data Classes. They are awesome!)

shinayser avatar May 20 '19 23:05 shinayser

FWIW, I believe Fuchsia would take advantage of this feature.

sethladd avatar May 30 '19 20:05 sethladd

FWIW, I believe Fuchsia would take advantage of this feature.

Tell me more about that 👀

shinayser avatar May 30 '19 20:05 shinayser

I tend to view Tupples as stack-allocated static structures. I tend to view Classes as heap-allocated variant structures. They are both just structures. Isn't there a way to unify them?

We're soon getting NNBD, which introduces the type modifier '?', such as Point? is a nullable reference to an instance of Point. We could introduce the type modifier '!', such as Point! would be an invariant stack-allocated (or inlined in another structure) structure of a Point. Just a way to group structure fields in one logical identifier and couldn't be passed around as reference to a Point.

Tupples would inherit declarations and composability of classes. Classes would inherit destructuring and anonymity of tupples.

return Point(0,0) The way we do it now return Point!(0,0) Return a Point tupple return new (x: 0, y: 0) Heap-allocated instance of an Implicit class return (x: 0, y: 0) Return anonymous tupple

Just an abstract idea, I'm sure we can shoot holes in it :P

fmorgado avatar Jun 03 '19 12:06 fmorgado

I'm not sure that there is such thing as a stack allocated structure in Dart. ...

In my view, "Multiple Return Values" are exactly that.

fmorgado avatar Jun 03 '19 15:06 fmorgado

AFAIK all values are objects in Dart so is memory allocation even involved in this issue at all?

As everyone I'd like multiple return values so my code is more coincise and human readable but in the end it's something I can live without. Anyway, we might want to concentrate on what returning multiple values could bring to the table.

The problem IMO is that the "tuple-like" class work-around will start biting everyone in the ass if over-used by (somewhat successful) third party packages APIs. We will start having X implementations of Tuple<...> coming from external dependencies. So we will start doing adapters but it's more code which is still error-prone both in the making (code generation could help in this) and in the usage (GL HF developers that abuse dynamic).

If the language provides a standard solution to the problem this future's chances of existing are very low + we might get some sweet syntax in the process.

ps: sorry for the previous send, it was a mistake.

GregorySech avatar Jun 03 '19 15:06 GregorySech

class MapService {
  var geoCode(String name) async {
    return tuple(0, 0);
  }

//  auto geoCode(String name) {
//  return tuple(0, 0);
//  }


  tuple<double, double> geoCode2(String name) async {
    ...
  }

}


var mapService = MapService();

// This is valid:
var latng = await mapService.geoCode('Aarhus');
var {a, b} = latng;
var {a, ...} = latng;
var {..., b} = latng;

//var [a, b] = latng;
//var [a, ...] = latng;
//var [..., b] = latng;

tarno0se avatar Jul 15 '19 13:07 tarno0se

Not just 2, multiple return values make lot of sense.

Once this feature is available, first thing I want to do is to write a simple dart implementation equivalent of https://golang.org/src/errors/ such that I can avoid using exception framework ( try-catch rethrow .. ) unless it is required for a specific purpose. This will be help to write relatively elegant code like :

err, valueA, valueB = myFunction(argA, argB); if (err != null) { // handle err }

archanpaul avatar Sep 11 '19 10:09 archanpaul

Golang enforces some rules on multiple returns to avoid this behavior (among other reasons).

Also, Go allows naming the return values for more clarity.

ridcully99 avatar May 28 '20 19:05 ridcully99

While it brings benefit of writing quick code, most of popular languages (C, Java, Python, PHP, Javascript) don't support actual multiple return values. Languages like python and javascript supports destructuring assignment which looks like multiple return values (but actually not because you're returning a single value which is either a tuple, an array or an object (in javascript)).

The only language I know that supports it is golang, which I believe it's due to golang's error handling philosophy (they need to return a err together with actual return value, and they allow push extra function into the stack as clean up functions).

IMO the multiple return values (especially named return parameter) is bad because it makes the number of return values and names of return parameters part of the function signature, which makes a function easier to become backward incompatible (like renaming the return param or adding a new return param).

In most of the cases, it's more proper to make a value object as the single return value when you need to return multiple values. For example, in the case above Future<double> lat, long = mapService.geoCode('Aarhus');, obviously lat long belong to the same concept, which are usually used together to locate something. It makes more sense to make a data class called GeoLocation to contain lat long, especially when you want to return something more things about geolocation.

I agree sometimes it makes thing more tedious amd people don't always want to write a new class for returning something. So in this case, I'm more in favor of a generic tuple class Tuple<T, U> as previously suggested in this ticket.

lapwingcloud avatar Sep 20 '20 06:09 lapwingcloud

A class may work but is far from ideal and doesn't avoid all of the clumsiness that Gos multiple return values were designed to avoid, resulting from the designers extensive experience with C.

"https://golang.org/doc/effective_go.html#multiple-returns"

That doesn't mean that it will be possible for Dart when it has to compile to Javascript, but I believe it is incorrect to suggest that a class or Tuple is a better solution to this issue. Though it may be an acceptable one? Perhaps wasm might make multiple returns easier to accomplish (VM) but I guess Javascript isn't going away any time soon, so perhaps not? I wonder how gopherjs accomplished it?

elansys-kc avatar Sep 25 '20 09:09 elansys-kc

I don't want go's multiple return values. But I want js/ts's returning dict or tuple.

xialvjun avatar Sep 28 '20 11:09 xialvjun

If you just want tuples, you can use package:tuple

Without destructuring it's not perfect, but it works today.

rrousselGit avatar Sep 28 '20 11:09 rrousselGit

We just need:

int, int foo() { 
  return 1, 2;
}
int x, int y = foo();

It will be a great improvement!

NickNevzorov avatar Sep 28 '20 11:09 NickNevzorov

A design with only multiple return values, not tuple values in general, can probably work to some degree, but I predict that it will quickly run into cases where it's not sufficient.

The smallest possible design would be something like (using obvious strawman syntax):

  • Function return "types" can be a product type (like (int, int)). Let's use a generic-type-like syntax, say Tuple<T1, ... ,Tn>, like we did for FutureOr. The Tuple type cannot be used for anything except return types (like void used to).
  • Function types are not related by subtype if they have a different number of return types, and they are covariant in the individual types of the tuple.
  • We need a way to destructure return values (immediately, because we can't store the tuple). Maybe something like Tuple(x, y, var z) = foo();. The "arguments" to the Tuple constructor must be assignable expressions/L-values. You must either destructure after a multi-return-value function call, or directly return the value again.
  • (The zero-tuple, if it exists at all, could probably double as null value).

This avoids introducing tuple types in general, but that also comes with a cost. You can't abstract over the arity of the return values!

  • If you dynamically call a function, then it will throw if you expect the wrong number of values.
  • The Function.apply function won't work for multi-value-return functions, its return type is dynamic which is not a tuple.
  • You can't abstract over all return types using generics, like T Function<T>(). You will need Tuple<S, T> Function<S, T>() for binary returns, Tuple<R, S, T> Function<R< S, T>()for ternary returns, etc. (This is still better than what we had withvoidbecause you *can* abstract over a two-tuple, you couldn't do anything withvoid`).

I predict that we'd quickly run into the sharp edges of such a design, and having tuples in the language would avoid some of those edges. It still depends heavily on whether tuples are objects or not. If there is no subtype relation between (Object, Object) and Object, then it's much easier to implement, but code still can't abstract over tuples of different arity. (The code can be much more efficient, though, because it knows the memory layout at compile-time). All you'd then gain is the ability to store an entire tuple in a variable.

lrhn avatar Sep 28 '20 14:09 lrhn