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

Multiple return values #68

Closed
mit-mit opened this issue Oct 31, 2018 · 116 comments
Closed

Multiple return values #68

mit-mit opened this issue Oct 31, 2018 · 116 comments
Labels
patterns Issues related to pattern matching. records Issues related to records. request Requests to resolve a particular developer problem

Comments

@mit-mit
Copy link
Member

mit-mit commented Oct 31, 2018

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 mit-mit added the request Requests to resolve a particular developer problem label Oct 31, 2018
@Markzipan
Copy link

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
    

@mdebbar
Copy link

mdebbar commented Nov 2, 2018

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.

@GregorySech
Copy link

GregorySech commented Nov 5, 2018

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.

@mit-mit mit-mit added this to Being discussed in Language funnel Nov 30, 2018
@bjconlan
Copy link

bjconlan commented Dec 16, 2018

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.

@sureshg
Copy link

sureshg commented Dec 17, 2018

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.

@andreashaese
Copy link

See also #207

@Meai1
Copy link

Meai1 commented Apr 15, 2019

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;

@natebosch
Copy link
Member

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.

@Meai1
Copy link

Meai1 commented Apr 16, 2019

@natebosch crazy to make a class just to return something

@GregorySech
Copy link

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

@bjconlan
Copy link

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)

@yjbanov
Copy link

yjbanov commented Apr 17, 2019

@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.

@natebosch
Copy link
Member

@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 😄

@Meai1
Copy link

Meai1 commented Apr 23, 2019

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;

@acherkashyn
Copy link

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.

@shinayser
Copy link

shinayser commented May 20, 2019

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!)

@sethladd
Copy link

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

@shinayser
Copy link

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

Tell me more about that 👀

@fmorgado
Copy link

fmorgado commented Jun 3, 2019

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
Copy link

fmorgado commented Jun 3, 2019

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.

@GregorySech
Copy link

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.

@tarno0se
Copy link

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;

@archanpaul
Copy link

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
}

@munificent
Copy link
Member

@munificent is there an issue for us to thumbs up the Records proposal?
I couldn't find it.

Not specifically, but we understand that records/tuples would address this issue too. Language features don't always map perfectly to user requests. :)

@chikega
Copy link

chikega commented Dec 25, 2021

I believe GingerBill has an interesting take on returning multiple return values using his creation, the Odin language:

@kevlar700
Copy link

Ada provides in out parameter arguments as a better interface than C pointers. The out parameters can be thought of as additional return values.

@abcxcba
Copy link

abcxcba commented Dec 31, 2021

Ada provides in out parameter arguments as a better interface than C pointers. The out parameters can be thought of as additional return values.

That's OK too.

@abcxcba
Copy link

abcxcba commented Dec 31, 2021

I believe GingerBill has an interesting take on returning multiple return values using his creation, the Odin language:

I couldn't agree more with gingerBill.
https://odin-lang.org/docs/faq/#why-does-odin-not-have-exceptions
https://www.gingerbill.org/article/2018/09/05/exceptions-and-why-odin-will-never-have-them/

@bsutton
Copy link

bsutton commented Feb 4, 2022

I have to say I'm inclined to vote against this.

How often do I need to return multiple values and I don't have a class handy?

The answer is not that often.

This week I had one occurrence where it would have been convenient.

The down side is that we make the language more complex for something that only gets used occasionally and for which there are perfectly adequate alternatives.

@ElteHupkes
Copy link

How often do I need to return multiple values and I don't have a class handy?

As a counter: the answer for me is really rather often!

Most recently I had a method that had a "success" return value and a couple of "expected" error cases that needed to be returned. The options are:

  • Wrap them in an object. That's usually what I'll do. It's annoying though, because that object is only relevant to that particular method and is defined somewhere else entirely.
  • Use something like tuple. That's slightly more convenient, but now I can only extract item1 and item2, which isn't very descriptive. It's basically watered down multiple return values.
  • Return a map? Probably don't need to explain why this isn't great.
  • ...refactor to do something else? Throw an exception instead? Nothing good comes to mind.

In C# I found the option to define named inline tuples very convenient. Having a way to extract them at the call site all the better, though I guess that's more of a marginal gain. Being able to say return (valueA: "a", valueB: "b") without having to go somewhere else to define that as a separate, ahum, data class, would definitely be something I'd like to see.

@bsutton
Copy link

bsutton commented Feb 6, 2022

@ElteHupkes
These occasional use cases can be annoying but they are easy to solve ( a local class in the dart library). My argument is that they don't warrant an increase in complexity of the language.

I'm also concerned that they are going to promote bad practices and fragment the package eco system.

Fragmented package eco system.
Currently the convention is to use an exception to return an error.
With multiple returns we have now given the community two ways to return errors.
The result is that we are going to get some packages that throw and some that return.
I don't believe this will add any value to the eco system and just make integrating packages into an app that much harder.

Poor coding practices.

Good practice is that a function does one thing and returns one thing.
Functions that need to return multiple items I consider a smell.
In my code it tends to happen because I'm trying to optimise a process. I have two unrelated items that are costly to obtain (e.g. a db query). I can obtain them in the same query so I lump them into a single function.
The don't actually belong together but I need the optimisation. The trade off is that I've just made the code harder to understand.

Returning error codes.
I'm going to suggest that I'm a little older than most of the people posting here.
My first decade or so was spent coding C.

Error returns were a blight.
The convention was that most programmers just ignored them.

When exceptions arrived it made a massive difference to code quality. Programmers could no longer ignore failed method calls.
Exceptions resulted in bugs being found sooner in the development cycle.

Exceptions to errors are what types are to variables.

Encouraging the use of error returns is a dis-service to the Dart community.

Over a beer one day I will tell you the story of 150 hours spent finding a bug in the asterisk source code caused by a dev failing to check the return on a call to lock().

@kevlar700
Copy link

kevlar700 commented Feb 6, 2022

You have a good point but I believe there is room for both. In Ada, exceptions are used for exceptional errors and out parameters akin to multiple return for status messages.

@bsutton
Copy link

bsutton commented Feb 6, 2022

@kevlar700
I'm not certain the 'there is room for both' argument holds water.

There was an pre-internet standard for exchanging invoices etc.
One of its 'features' was that it allowed you to pass a date in about 15 different formats.
The result was that everyone chose a different format.
Every time you encountered a date you had to be prepared to support everyone of the formats.
The flexibility of 'room for both' just caused unnecessary complexity.

Any new feature we add to the language needs to answer two questions:

Does this feature make using the language less complex.
Does this feature promote good practices.

I think this proposal fails on both.

I measure complexity here as the trade off between users having to learn a new language feature vs the reduction in complexity of arriving at a solution. The existing solutions are simply to implement and simple to understand.

One of the concerns is that the addition of this feature will encourage users to be 'creative' with the feature in ways that just make code harder to understand.

My belief is that the reason Ruby failed was that it was too flexible. It encourage users to essentially write DSLs so you had to learn the DSL of every ruby program. I've had first hand experience of the type of nonsense that Ruby encourages and its fun to implement, impossible to maintain. Building a language that is somewhat dogmatic in style actually makes for a better package eco system.

The short story is don't introduce features:

  • for which existing solutions exist
  • that have a low frequency of use
  • that are likely to fragment the way error handling occurs
  • that promote poor coding practices
  • when there are lots of other issues that will have a greater impact on the language.

@vandadnp
Copy link

vandadnp commented Feb 6, 2022

Hello and thank you for Dart!

I just wanted to chime in out of nowhere and add my comments here:

If you look at some of the top programming languages used these days for both front and backend development, you'll see that they support returning tuples:

  • Rust allows tuples
  • Swift consequently allows tuples since it's like a child of Rust
  • Python allows returning multiple values
  • C++ I think now allows that too (TR1, C++0x)
  • C# I believe as somebody else named allows named tuples as well
  • TypeScript allows this as well though I confess I haven't used it personally

JavaScript surprisingly apparently doesn't allow tuples 🤔 or am I mistaken?

So just to wrap it up, I think it's a matter of time before people expect tuple support from Dart given how popular it has become now!

@jodinathan
Copy link

I think you should know the Records proposal.

Some examples:

(int, String, bool) foo() => (1, 'yes', true);

(int, String, bool) triple = foo();

var namedRecord = (1, 2, a: 3, b: 4);

var a = (1, 2);
var b = (1, 2);
print(a == b); // true.

(num, Object) pair = (1, 2.3);

print(pair is (int, double)); // "true".

@leafpetersen leafpetersen removed this from Being discussed in Language funnel Mar 16, 2022
@edwin-alvarez
Copy link

@jodinathan, when will records be available?

@lrhn
Copy link
Member

lrhn commented Sep 13, 2022

When they're ready!

Records (and pattern matching) are being actively worked on by the Dart language team, and are getting close to where the designs are ready for implementation. They'll then be implemented, and released when ready. Basically, as soon as possible, but no sooner. Unless something happens, and we decide to postpone or drop the feature. We probably won't, but there are no promises until the feature has actually launched.
Can I be any more vague?:grin:

But they are next on our list of non-trivial features, so as to whether records will be ready before "any other feature", the answer is "likely yes".

@TimWhiting
Copy link

For those that want to experiment with unreleased experimental features, note that you can enable the records feature on the master branch of flutter with:
dart --enable-experiment=records your_file.dart

void main(List<String> arguments) async {
  print(returnValue(1).$0);

  final newValue = (1, 2, a: 1);
  print(newValue);
  print(newValue.a);
  print(swap((1, 2)));
}

(int, int) returnValue(int value){
  print('hi');
  print((a: 1, b: 2));
  return (value, value);
}

(int, int) swap((int, int) value){
  return (value.$1, value.$0);
}

=>

hi
Record (a: 1, b: 2)
1
Record (1, 2, a: 1)
1
Record (2, 1)

Note that I do not think AOT support or JS support is working yet.

Just realize that the feature could be canceled or changed at any time while in the experimental phase.

Thank you dart team for making this happen!! You all are the best!

@shinayser
Copy link

@TimWhiting I was wondering if we could access the values of the record in a array way. Like, instead of value.$1 we could do value[1] ?

@TimWhiting
Copy link

@shinayser No. This was discussed on the language repo, and you can see the reasoning process here: #2388

@holocronweaver
Copy link

Any chance we could have Python-like syntax for creating local variables from record elements?

Example:

final a, b = ("word", "bird");
print(a);    => "word"
print(b);    => "bird"

I can mostly achieve what records seem to be aiming at using the tuple package, but it doesn't provide that concise syntax Python has for unpacking multiple elements in a single line.

Example using tuple package, which requires three lines:

final words = Tuple2("word", "bird");
final a = words.item1;
final b = words.item2;

From what I have read, records would be similar:

final words = ("word", "bird");
final a = words.$1;
final b = words.$2;

If records could also support Python-like unpacking as a language feature as in the first example, that would be a real value add over the tuple package.

@lrhn
Copy link
Member

lrhn commented Oct 19, 2022

Yes. The currently proposed syntax is final (a, b) = ("word", "bird");.
That's a destructuring declaration. You can also do the .$1/.$2 access for positional fields, but you should consider that a fallback, and use destructuring as the primary way to access record fields.

@mit-mit
Copy link
Member Author

mit-mit commented Oct 24, 2022

@jodinathan
Copy link

For those that want to experiment with unreleased experimental features, note that you can enable the records feature on the master branch of flutter with: dart --enable-experiment=records your_file.dart

Is Records enable to test with dart2js and in console apps?

@vsmenon
Copy link
Member

vsmenon commented Jan 17, 2023

@munificent munificent added records Issues related to records. patterns Issues related to pattern matching. labels Jan 17, 2023
@Quijx
Copy link

Quijx commented Feb 17, 2023

Are records going to be more performant than custom classes with final fields (and == and hashCode)?
For example the feature specification says the following:

We expect records to often be used for multiple return values. In that case, and in others, we would like compilers to be able to easily optimize away the heap allocation and initialization of the record object.

Is this optimization planned for Dart 3 or just left open as a possible optimization later?
Is this optimization only possible for record return values of functions, or does this for example also prevent initialization of an object when records are used as map values (This is the case I am actually interested in :) )?

Also if one were to write a package like vector_math only using records for the vectors and matrices where the operations are extensions for example on (double, double), could that be faster?

@munificent
Copy link
Member

Is this optimization planned for Dart 3 or just left open as a possible optimization later?

In general, the language doesn't specify which optimizations are done. We just try to design features such that the semantics don't prevent optimizations. For Dart, we have two compilers to JavaScript, a JIT that targets several CPU architectures, as well as an ahead-of-time compiler that also supports several CPU architectures. So the answer "is this optimization" done can be complex. It may be optimized on some targets under some contexts but not others.

In general, the most accurate way to reason about performance of your program is to profile it.

@munificent
Copy link
Member

Closing this because records and patterns are now enabled by default on the main branch of the Dart SDK! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
patterns Issues related to pattern matching. records Issues related to records. request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests