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');
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:
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];
}
// Invalid
(int, (int, int)) myfunc() {
...
}
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
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.
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.
Yes! Destructuring! I think leaving the typesystem alone for this is fine. Array
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.
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.
See also #207
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;
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 crazy to make a class just to return something
@Meai1 he probably means a Couple
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)
@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 - 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 馃槃
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 Listtype 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;
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.
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!)
FWIW, I believe Fuchsia would take advantage of this feature.
FWIW, I believe Fuchsia would take advantage of this feature.
Tell me more about that 馃憖
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
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.
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.
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;
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
}
Golang enforces some rules on multiple returns to avoid this behavior (among other reasons).
Also, Go allows naming the return values for more clarity.
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.
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?
I don't want go's multiple return values. But I want js/ts's returning dict or tuple.
If you just want tuples, you can use package:tuple
Without destructuring it's not perfect, but it works today.
We just need:
int, int foo() {
return 1, 2;
}
int x, int y = foo();
It will be a great improvement!
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):
(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).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.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!
Function.apply function won't work for multi-value-return functions, its return type is dynamic which is not a tuple.T Function<T>(). You will need Tuple<S, T> Function<S, T>() for binary returns, Tuplefor 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.
Instead of generic Tuple In practice, I think Duo and Trio are enough for most practical purposes.Duo<T1, T2>
Trio<T1, T2, T3>
Quartet<T1, T2, T3, T4>
Quintet<T1, T2, T3, T4, T5>
// etc
The syntax for destructuing is straightforward, e.g.var [x, y, z] = Trio("hello", 1, true);
Apparently, my Duo/Trio idea didn't fly. Not sure why. Maybe the names are too long? Then how about U2, U3, U4, etc?
E.g.
class U2<T1, T2> {
T1 _v0;
T2 _v1;
U2(this._v0, this._v1);
T1 get v0 => _v0;
T2 get v1 => _v1;
// add toString, hashCode
}
That's all we need to define a tuple. Basically, it's just a matter of finding good short names. U2 is the best I could come up with. It has a number of advantages:
In March 1978, the group changed their name to "U2".[12] Steve Averill, a punk rock musician (with the Radiators) and family friend of Clayton's, had suggested six potential names from which the band chose "U2" for its ambiguity and open-ended interpretations, and because it was the name that they disliked the least.
- mnemonic: U stands for Union
Union would probably cause confusion with union types which are sum types, in oposition to tuples as product types.
FIne. Let U stand for tUple. BTW, we don't have to say what it stands for - let user guess. In math, some names often don't stand for anything recognizable, e.g. Z2 (the cyclic group of order 2). Why Z? Probably, there's some (German?) word for it, but who cares?
FIne. Let U stand for
tUple. BTW, we don't have to say what it stands for - let user guess.
That doesn't sound like a value-add for me. I think if we do tuples, we'll give them a real type annotation syntax instead of having them be syntactic sugar for a class declaration as they are in some other languages.
I think if we do tuples, we'll give them a real type annotation syntax
It starts with the choice of name. Name is the hardest part.
No, I don't think they'll need a name at all. Instead, it would be something like:
(int, String) returnsTuple() => (1, "s");
No, I don't think they'll need a name at all. Instead, it would be something like:
(int, String) returnsTuple() => (1, "s");
OMG! AMAZING! DO IT!
No, I don't think they'll need a name at all. Instead, it would be something like:
(int, String) returnsTuple() => (1, "s");
But wait... How do you make a Stream of a tuple in this case?
Stream<(int, String)>
Is that right?
(int, String) returnsTuple() => (1, "s");
Sure, this is the first thing that comes to mind. We had a lively discussion on that on these pages, but for some reason (which I don't remember what it was) it failed to impress someone (not sure who). If you go with this syntax, there's a strong temptation to add named parameters. I think the issue should be revisited.
But wait... How do you make a Stream of a tuple in this case?
Stream<(int, String)>
You got it!
If you go with this syntax, there's a strong temptation to add named parameters.
I do think we should named parameters too, yes. Type annotation syntax for that is harder. I believe Lasse proposed (foo: int, bar: String) where foo and bar are the names. I personally think that looks a little too much like positional arguments in a language that uses postfix type annotations (Scala, TypeScript, Kotlin, Swift, etc.).
I haven't had time to really think it through, but one syntax I'm interested in is to follow Dart's parameter syntax and use curly braces: ({int foo, String bar}). In cases where you're defining a record type that only has named parameters, we can probably allow you to elide the outer parentheses: {int foo, String bar}. That's fairly close to dedicated record syntax in other languages, but also extends to support tuples that mix positional and named fields: (num, bool, {int foo, String bar}).
(num, bool, {int foo, String bar})
Yes, this is consistent with the syntax of function parameters and also suggests a natural syntax of destructuring:
var myTuple=(1, false, foo: 0, bar: "xxx");
var (n, b, {foo, bar}) = myTuple; // destructuring
Due to braces, we can tell named parameters from the positional ones, which otherwise would be difficult to accomplish.
Another thing to consider: spread operator:
void func(num n, bool b, {int foo, String bar}) {...}
func(...myTuple);
Maybe we can also somehow allow defining a tuple structure based on the parameter structure of the existing function - something like
typedef MyTuple = func.parameters;
No, I don't think they'll need a name at all. Instead, it would be something like:
(int, String) returnsTuple() => (1, "s");
If we support anonymous tuple type, should we also support anonymous dict type
An argument can be made that the most natural spelling for a tuple type would be not
(num n, bool b, {int foo, String bar})
but
Tuple(num n, bool b, {int foo, String bar})
For a long time, dart lived without a word for "function", but eventually, the word emerged: Function. Having a word Tuple for tuple right from the start looks like a reasonable choice :-)
The type should not include names for positional parts, so the type would be Tuple(num, bool, {int foo, String bar}). The extra names can perhaps be added for documentation, but they have no effect.
I think what works best that depends very much on how it works. A syntax like Tuple(...) suggests that the tuple is really an object. If tuples are not subtypes of Object, then that would be confusing.
Making tuples subtypes of Object (making them reference types, effectively) means that they need to be boxed when used as such, but also that you can make a list of a tuple type.
Making them non-objects means that they can always be kept unboxed. and won't have identity (and likely not have a reified type, they'd work as if Object references were 1-tuples), but also that we'd have to specialize generic types (like C# does) if we want them to work with non-objects, since non-1-tuples have a different memory layout than 1-tuples.
In go, return values can optionally be named for use within the function itself but the caller sets the names of the returned values. I have moved the return brackets from the end to the front like Dart.
:= also automatically creates the variable for you
func (height, weight int, name string) returnsMultiple(height, weight int, name string){
name = "Name: " + name
return height, weight, name
}
height, weight, name := returnsMultiple(10, 20, "bob")
optionally instead of:
int height; int weight; string name; height, weight, name = returnsMultiple(10, 20, "bob")
@lrhn: could you provide a rationale for not making tuple an Object? For whatever arguments I managed to come up with, I could easily find the counter-arguments. And generally, wouldn't it be strange for a language where everything (including int) is an object to suddenly introduce a thing that is not an object? I'm not sure what it even means to not be an object b/c a mere print(tuple) won't work then :-)
Not being an object would mean being a structural type with no identity. A pair variable is simply two storage locations, a pair value is just two values. Returning a pair means leaving two slots on the stack for the result. It's the most efficient implementation because it's basically no implementation at all.
It's true that it doesn't fit well into Dart. It's not just another kind of value, it's more like something built in top of Dart than something integrated into it. Currently we only have one-tuples, all single values are that. With general tuples, we'd have two-tuples, three-tuples, etc., which are all different and disjoint type hierarchies. Generics which abstract over one-tuples won't work over two-tuples unless we introduce a new way to abstract over type arity (that's non-trivial).
If we make tuples be objects, then we lose the ability to simply implement them as two storage locations. A list of objects is a list of one-tuples, so we need to embed a pair into a single reference, making them reference objects. Which means that they are heap-allocated instead of inlined, and we lose some efficiency. If we give tuple objects identity as well, then we've lost the ability to unbox intermediate values (that would lose the identity, unless we keep the original around).
The one trick we have, which we have used before for numbers, is to lie about identity. Two integers are "identical" if they are equal. That's a lie, but it allows us to ignore the identity of integers, and therefore we can get away with not preserving identity.
If we do that for tuples, we'll have to decide when identical(pair1, pair2) is true. Probably when both have the same arity, and on each position, the values are pairwise identical. Or we could just always say no, treating tuples as value types with copy semantics. We'd have to do something, which is why this is more complicated than just not making tuples be objects. But it allows so many things to just work, like lists of tuples. (We can still optimize internal lists to have a different representation for lists of tuples, as long as you can't tell the difference).
If we make tuples be objects, then we lose the ability to simply implement them as two storage locations
This was one of the things I was thinking about, but the counter-argument is this: tuple is (obviously) an immutable object, so we are free to copy them around like ints or doubles. Initially, it gets allocated on the stack and then can be passed by value, copied into list etc.: whatever we can do with ints, we can do with tuples. Further, since it's a value object, its identity (and hash code) should be computed based on field values (that is, bits). All this must be no principally different from the way dart treats doubles or ints: their distinguishing feature is immutability, not the fact that they are "small". The only difference I can think of is: unlike, say, int, where dart cleverly uses LSB for identification, tuple layout should include a 1-word header with the type identification - so (int, int) tuple will actually occupy 3 words. Looks like no big deal to me, but in exchange, we get a very powerful mechanism. Am I wrong?
Yes, this is consistent with the syntax of function parameters and also suggests a natural syntax of destructuring:
var myTuple=(1, false, foo: 0, bar: "xxx"); var (n, b, {foo, bar}) = myTuple; // destructuring
Maybe. The challenge here is that it would be good to be able to destructure maps too and both of those really want curly braces.
Another thing to consider: spread operator:
void func(num n, bool b, {int foo, String bar}) {...} func(...myTuple);Maybe we can also somehow allow defining a tuple structure based on the parameter structure of the existing function - something like
typedef MyTuple = func.parameters;
Yes, I would like to be able to essentially reify and spread a "first-class argument list". I don't know if it's feasible鈥攖here are many devils in those details鈥攂ut it's on the list of goals.
@tatumizer Yes, having tuples be objects is probably a good idea. It's much more directly usable, and if we fiddle with identity, we can still unbox intermediate values. If we are really clever with function calls, we might even unbox return values when the return type is statically known. There won't be any one-tuples then, or zero tuples for that matter (just use null). It'd be more like an infinite family of generic tuple types, Tuple2<S,T>, Tuple3<R,S,T>, etc., with no getters for the fields, only deconstruction logic. And if we have named elements, then Tuple2$bar$foo<A,B,C,D> would be (A, B, {C foo, D bar}).
I'd still avoid reifying the type of the tuple, so a (num, num) p = (2, 2); print(p is (int, int)); should print true. That's much harder if we have named elements, because then we do need to remember the name-to-memory-layout signature on the object. An (int, {int x}) is not the same as an (int, {int y}).
I like the idea of having a Tuple object.
It is like creating a class with properties on the fly :)
An (int, {int x}) is not the same as an (int, {int y})
This is similar to functions:
typedef F=void Function({int? x, int? y});
typedef G=void Function(int x, int y);
void f1({int? x, int? y}){}
void f2({int? x, int? z}){}
void g1(int x, int y){}
void g2(int x, int z){}
void main() {
print(f1 is F); // true
print(f2 is F); // false: parameter names matter!
print(g1 is G); // true
print(g2 is G); // true: parameter names don't matter
}
I'd still avoid reifying the type of the tuple, so a (num, num) p = (2, 2); print(p is (int, int));
The fact that p is (int, int) is true does not necessarily mean that type information (num, num) is not available in runtime - it simply means that this information is not used while deciding that p is (int, int) - instead, the decision is based on the actual runtime types of the elements. But this type information might be needed for other purposes - e.g., as you observed, the names of named parameters should come from somewhere, otherwise, even print(p) won't work - so the most natural solution seems to be just to include 1-word header of type info in in-memory layout, even in the absence of named parameters - just to keep things simple and uniform. (Some optimizations are possible. e.g. if we have List<(int,int)>, type information is already there in the type descriptor of List, so having a header for each element is not necessary)
But the bigger issue is this: suppose we have a List<(num,num)>. Obviously, all the elements in the list should be of the same size. But the tuple (1, 1) on 32-bit architectures takes 2 words, and the tuple (1.5, 1.5) - 4 words, and both tuples are compatible with the signature (num, num). Not sure how this may work.
We could consider the (T1, ... Tk, {S1 x1, ... Sn xn}) tuple type syntax as an abbreviated notation for the following class (where x1 .. xn are sorted alphabetically):
class Tuple_x1...xn<T1...Tk, S1...Sn> {
final T1 $1; ... final Tk $k;
final S1 x1; ... final Sn xn;
Tuple_x1...xn(required this.$1, ... required this.$k, {required this.x1, ... required this.xn});
... // Various methods, e.g., `operator ==` and `hashCode`.
}
We could consider adding structural subtype relations (such that any tuple with a subset of fields is a supertype), but it would probably be better to keep the subtype relation less massive, and in return having the guarantee that if an expression has a tuple type then it is guaranteed to have exactly the fields that we know statically. Subtyping by covariance is still available and sound, so (int, num) <: (num, Object?).
Tuple values would desugar to constructor invocations.
This would ensure that tuples are full-fledged objects, and at the same time allow simple cases to compile to code where a tuple has no run-time representation:
(int, int) f() => (4, 2);
void main() {
(var x, var y) = f(); // (1)
Object o = f(); // (2)
(int, int) p = f(); // (3)
Object o2 = p; // (4)
}
At (1), we may invoke a (fast) version of f that receives a pointer to the storage location for x and y on the stack and directly stores the values 4 and 2 into those locations. However, at (2) we are calling the more general version of f that returns a full-fledged heap object. The general version of f will be used when the return value may be required to be an object, when f is closurized (that is, when it is passed around as a first class function object), and in other situations where there is too little information available at compile-time to allow the use of the fast version.
With this approach I believe we'd need to reify the type of a tuple when it is turned into a heap object. In the case where a tuple is only used in a manner that allows treatment like the "fast" version of f there wouldn't be an object, but it would always be sound to compute the tuple type based on the run-time type of its components, and that approach would also yield the same run-time type for a given tuple every time it is obtained. (Of course, this is only true because the tuple is immutable, and that's also required for covariance to be sound).
This allows List<(num, num)> to be a completely normal type, passing a regular (but compiler-generated) class as a type argument to another class.
We could start with an approach where nothing is "fast" (so all tuples are turned into heap objects), and then we could optimize one case after the other, allowing a lot of tuples to disappear. Optimizing List<(num, num)> into a specialized list that avoids the need for all those heap-allocated tuples as elements may be hard, but it could very well be worth the cost to allow it even if it's as expensive as other objects.
A Solomonic solution to the problem of "num" would be to totally prohibit the use of the type "num" in tuples. This is the only corner case I can think of, and my speculation is that the users can live very well without it. (Alternatively, "num", when used in tuples, always allocates 64 bits per element)
Option to consider:
instead of typedef, tuple type definition should have its own dedicated syntax:
tuple Q=(int x, {String foo, int bar});
Option to consider:
Named parameter may have a default value, in which case it has to be explicitly marked as "optional" in the tuple definition, e.g. tuple Q=({String x, optional int y=0}). This tuple can be instantiated as Q(x: "hello").
Option to consider:
Extension methods can be applied to tuple types, e.g.
tuple Pair=(int x, int y);
extension on Pair {
int sum() {
var (x, y) = this;
return x+y;
}
}
Pair(1, 2).sum();
This reminds the old (pre-C++) style of programming in C where we could define a struct, and then separately define a number of functions over this struct, with no inheritance.
Option to consider:
Syntax for 1-tuple: (0,) - with trailing comma (like in python). For 0-tuple, there's an obvious syntax ().
On second (third?) thought, I can't see too many benefits in avoiding heap allocation for tuples. The apparent benefits (in terms of memory/performance gains) might be more than offset by the increased complexity of GC and related performance losses.
Also, the idea of "structural type" looks suspect to me. Suppose we have tuple type (double, double) declared in 2 places. Is this the same type? I don't think so. One is a complex number, another is (cost per kg, amount) in the shipping of ice cream. Do these two mean the same thing?
Suppose we have tuple type (double, double) declared in 2 places. Is this the same type? I don't think so
That is already the case for functions. So I don't see why that couldn't be the case for tuples
We won't _extend_ a Tuple, but rather use typedefs:
typedef A = (double, double);
On second (third?) thought, I can't see too many benefits in avoiding heap allocation for tuples. The apparent benefits (in terms of memory/performance gains) might be more than offset by the increased complexity of GC and related performance losses.
I think that can be pretty much an optimization in the compiler. If the Tuple is used as Object, treat as an Object, if not, avoid allocation.
Would a Tuple also able to be used in functions calls?
void foo(String bar, int baz);
var args = ('Bar', 3);
foo.call(args); // something like that
That is already the case for functions. So I don't see why that couldn't be the case for tuples
OK, I concede the point. Indeed, tuple types should be consistent with function types. However, if consistency is the goal, then consider this:
double totalPrice(double costPerKg, double amount) {}
Note that function definition always contains the names of positional parameters (they are not optional!). Meanwhile, the typedef corresponding to the above definitions doesn't require the names:
typedef F=double Function(double, double);
However, we always know the names (and therefore the meaning) of parameters by looking at the definition of the concrete function (e.g. totalPrice(...)).
In the case of tuples, how do we know what each "double" in the definition (double, double) means? There's no "concrete" definition that provides this information: the instance looks like (1, 2.5) - what is what here? What syntax do you propose to document the names?
As an aside: there's a certain amount of weirdness WRT parameter definitions in dart:
typedef F=void Function(double); // "double" is a type
void f(double) {...}; // "double" is the name of parameter! the type of this parameter is "dynamic".
In one case, "double" is a type, in another - parameter name. Probably, this is a remnant of the earlier ("optional typing") versions of dart where most decisions were dictated by javascript, and now it's too late to fix.
If the Tuple is used as Object, treat as an Object, if not, avoid allocation.
Sure, there are cases where heap allocation can be avoided:
void foo(String bar, int baz);
//...
var args = ('Bar', 3);
foo(...args); // note the spread operator! Otherwise who knows what the foo does with the tuple
In most cases, this optimization cannot be done - e.g. if you have a list of tuples like [("Bar", 3), ("Baz", 2)] - the compiler cannot know what happens with this list and its elements after the list gets passed as a parameter, so heap allocation of every tuple looks like the only option. (Main challenge here is to avoid the problems with GC).
Would a Tuple also able to be used in functions calls?
void foo(String bar, int baz); var args = ('Bar', 3); foo.call(args); // something like that
You would have to explicitly unpack the tuple, something like:
foo.call(...args); // something like that
I don't think we want to unpack implicitly because it gets ambiguous in some cases:
oneOrTwo(Object obj, [Object obj2]) print("$obj and $obj2");
main() {
var tuple = (1, 2);
oneOrTwo(tuple);
}
If we try to unpack implicitly, then this could reasonably print either "(1, 2) and null" or "1 and 2".
But, in general, yes, I would like to support this. I don't know if we'll be able to pull it off.
I was wondering... could Tuple introduce something like pointers in Dart?
void foo({int someInt}) {
someInt++;
}
void main() {
var tup = ({someInt: 5});
foo(...tup);
foo(...tup);
print(tup.someInt); // 7
}
I was wondering... could Tuple introduce something like pointers in Dart?
void foo({int someInt}) { someInt++; } void main() { var tup = ({someInt: 5}); foo(...tup); foo(...tup); print(tup.someInt); // 7 }
I really hope they couldn't.
I was wondering... could Tuple introduce something like pointers in Dart?
void foo({int someInt}) { someInt++; } void main() { var tup = ({someInt: 5}); foo(...tup); foo(...tup); print(tup.someInt); // 7 }I really hope they couldn't.
why not?
there could be immutable tuples just like we need immutable objects/maps etc
No, I don't think tuples/records should be mutable, and even if they were, in your example here foo() is destructuring the tuple and pulling the field out into its own local variable. I don't think we'd want tuples to implicitly also give you call by name.
Fortunately, tuples (or multiple return values in general) solve the main case where you do end up using pass by reference in C/C++. The main use case for that is to support output parameters. But if you have actual multiple returns, you don't need to use a parameter for output. You can just return all the values.
Most helpful comment
It would be great if Dart had builtin support for a
tupletype, and provided syntactic sugar for destructuring tuples: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.