wren
wren copied to clipboard
[RFC] Adding a `Tuple` with language support
Hi,
The present RFC is about adding a Tuple
class. It would follow the usual syntax:
var tuple = (1, 2)
System.print(tuple.type) // expect: Tuple
It would also act as an array container (non resizable):
var array = Tuple.filled(10)
array[4] = 42
System.print(array[0]) // expect: null
System.print(array[4]) // expect: 42
There are some friction points to take care with the setter syntax. For performance reasons and practicality, the setter syntax may be upgraded to allow many parameter on the right side:
class Foo {
static foo = (a) { System.print("1") }
static foo = (a, b, c) { System.print("3") }
}
Foo.foo = 1, 2, 3 // expect: 3
Foo.foo = (1, 2, 3) // expect: 3
Foo.foo = ((1, 2, 3)) // expect: 1
But, I'm not sure about all the implications. In particular with the variable declaration syntax, so it has to be tough more deeper.
Anonymous tuples (which I think is what you're proposing here) is something I've thought about myself but without coming to any definite conclusions as there are a lot of things to consider.
Now named tuples, which I currently create dynamically (similar to my Data Classes proposal in #912), I've found to be very useful. I can create them in one line (an enormous saving in verbosity) and, if I return them from a method/function, no destructuring is needed because I can access the fields via named properties.
However, with anonymous tuples, the obvious question to ask is what advantages do they have compared to lists? Here are the ones which spring to mind.
Tuples are generally immutable - you can't change either the size or the fields themselves. This is the case in Python, for example, which has both tuples and lists.
Now this would rule out syntax such as the following:
var array = Tuple.filled(10)
array[4] = 42
But you could create a tuple from a pre-existing list by giving it, say, a fromList
method:
var array = (1..10).toList
var tuple = Tuple.fromList(array)
System.print(tuple) //> (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Immutability is a useful property for an object to have. In particular, whilst tuples would need to be reference types, you could give them (like strings) value type semantics. So this would work:
var a = (1, 2)
var b = (1, 2)
System.print(a == b) // true, even though a and b are different objects.
This would mean that as long as all fields were numbers, strings etc., tuples could be used as map keys which would be very useful.
Although having top-level immutable variables are not as useful in Wren as in languages which allow multi-threading and parallel execution, it is still useful to know that something can't be changed. Although for various reasons, it was not thought to be worthwhile supporting constants in Wren, one could nevertheless create a sort of 'backdoor' constant using a 1-tuple:
var a = (1) // can't be changed
The Tuple class would be much simpler than List as we wouldn't need to support adding, inserting or removing elements.
Given their immutable nature, it might to possible to implement them more efficiently in C than lists. However, I think we'd need some additions to the embedding API to read them from Wren and to create and send them back to Wren
The obvious drawback to tuples is that they make the language more complicated and it's a lot of work to implement them.
Do the advantages outweigh this? Possibly, though I'm not entirely sure.
As you said: Tuples are generally immutable
. It is not a requirement, but more a trend towards immutability. So I choose not to follow that trends, because it doesn't make sense for now and the VM, and it can always be solved with an immutable version of that class.
There are few advantages for tuples:
-
In most language, they are related to parameter passing. It doesn't have the connotation of being heterogeneous containers. But functionality wise, it can be used as a decent replacement for an hypothetical
Array
class. -
count
is known at compile time. Implementation wise, it means an indirect call can be avoided to access the data array (less cache misses...). It implies slightly better performance, when one need a container of size that rarely changes, or implementing some of the container to better fit users needs. -
I think it can be interesting to specialize
call
methods like:
class Tuple {
...
foreign call(receiver) // implicitly invoke: receiver.call(...)
foreign call(receiver, method) // implicitly invoke: receiver.method(...) (supporting subscript and setter syntax)
}
- It can be used to discriminate an argument. There are situations, where some methods/grammar may need to differentiate between a
List
as a regular argument, orList
as a group of parameter. By delegating parameter passing to its own type, a simple test can be perform to behave accordingly.
Most of those behaviors exist in List
or could be emulated with it. Though, the last point is hard to hard to implement without duplicating the functionality or create a proxy. For me it is a valid reason, to provide a similar class to List
but with its own unique behaviors.
Your example about variable immutability with a 1-tuple is broken. Without read-only global variable, the variable can always be replaced with a fresh 1-tuple. Security wise, it might be interesting to bring immutability at top level, but it is a complete different topic.
Although for various reasons, it was not thought to be worthwhile supporting constants in Wren, one could nevertheless create a sort of 'backdoor' constant using a 1-tuple:
var a = (1) // can't be changed
~~Taking this example, I don't think this would function as a constant. The value of the tuple would be immutable but the variable of a
probably could be overwritten entirely.~~ I guess mhermier already explained this
var a = (1)
// Wouldn't work
a[1] = 4
// Though these may
a = (7)
a = "some other value type"
- count is known at compile time. Implementation wise, it means an indirect call can be avoided to access the data array (less cache misses...). It implies slightly better performance, when one need a container of size that rarely changes, or implementing some of the container to better fit users needs.
I am wondering whether this would be any different than an array literal or creating a filled array, then populating the elements.
// Are these optimized differently?
var a = [0, 1, 2]
var b = (0, 1, 2)
- I think it can be interesting to specialize call methods like:
class Tuple { ... foreign call(receiver) // implicitly invoke: receiver.call(...) foreign call(receiver, method) // implicitly invoke: receiver.method(...) (supporting subscript and setter syntax) }
I think this might be an interesting change but wonder if this means that tuples would need to be capped to the length of 16. Also curious whether you could just pass either the tuple, the tuple with some kind of "spread" operator, or use some other operator to a parameter lists when calling
var fn = Fn.call {|a, b, c|
System.print(a, b, c)
}
var tuple = (1 ,2, 3)
fn.call(tuple) // Could this spread?
fn.call(...tuple) // Or could we do something like this instead?
// Other options to pass values of a tuple to a function
tuple >> fn
tuple ~> fn
I tried making an example here to preview what it might feel like to use tuples in wren though this is an example. It probably should be implemented in C and have some new syntax for a literal.
// Just an example class. We'd probably want this to be a primitive
class Tuple {
// For this example, I'm just using lists for tuples. We'd probably want to have our own literal for tuples like: (1, 2, 3)
construct new(array) {
if (array.type != List) Fiber.abort("tuple must receive a list")
_t = array
}
// Passes tuple values as parameters to Fn or Fiber type variables and calls them
>>(fn) {
if (fn.type != Fn && fn.type != Fiber) Fiber.abort("Cannot call value of type %(fn.type)")
if (fn.arity == 0) return fn.call()
if (fn.arity == 1 && _t.count >= 1) return fn.call(_t[0])
if (fn.arity == 2 && _t.count >= 2) return fn.call(_t[0], _t[1])
if (fn.arity == 3 && _t.count >= 3) return fn.call(_t[0], _t[1], _t[2])
if (fn.arity == 4 && _t.count >= 4) return fn.call(_t[0], _t[1], _t[2], _t[3])
if (fn.arity == 5 && _t.count >= 5) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4])
if (fn.arity == 6 && _t.count >= 6) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5])
if (fn.arity == 7 && _t.count >= 7) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6])
if (fn.arity == 8 && _t.count >= 8) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7])
if (fn.arity == 9 && _t.count >= 9) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8])
if (fn.arity == 10 && _t.count >= 10) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9])
if (fn.arity == 11 && _t.count >= 11) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10])
if (fn.arity == 12 && _t.count >= 12) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11])
if (fn.arity == 13 && _t.count >= 13) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11], _t[12])
if (fn.arity == 14 && _t.count >= 14) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11], _t[12], _t[13])
if (fn.arity == 15 && _t.count >= 15) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11], _t[12], _t[13], _t[14])
if (fn.arity == 16 && _t.count >= 16) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11], _t[12], _t[13], _t[14], _t[15])
Fiber.abort("Insufficient values in tuple")
}
// Access values and tuple information
[index] { _t[index] }
count { _t.count }
// Test equality on values of a tuple and not the reference of the tuple itself
==(other) {
if (other.type != Tuple || count != other.count) return false
for (i in 0...count) {
if (this[i] != other[i]) return false
}
return true
}
!=(other) {!(this == other)}
// just what a stringified tuple might look like instead of an array
toString {
var str = "("
for (i in 0...count) {
if (i != 0) str = str + ", "
str = str + _t[i].toString
}
str = str + ")"
return str
}
}
var t = Tuple.new([1, 2, 3])
// Can access fields from tuple
System.print("len: %(t.count); [%(t[0]) %(t[1]) %(t[2])]") // len: 3; [1 2 3]
var fn = Fn.new { |x, y|
return "{ X: %(x), Y: %(y) }"
}
// Pass tuple as parameters to function types.
// Fn only accepts 2 values so the last value is dropped.
// This could be more strict instead, and abort if tuple lengths do not match.
System.print(t >> fn) // { X: 1, Y: 2 }
// ^
// | This operator is currently available in wren right now but
// | might make chaining problematic if also using Num Types.
// | Instead we could invent another operator like `=>`, `~>` or
// | `->` to be more clear as to what this is doing.
var a = Tuple.new([1, 2, 3])
var b = Tuple.new([1, 2, 3])
var c = Tuple.new([4, 5, 6])
// Can test whether values of Tuples match.
System.print("%(a) == %(b): %(a == b)") // (1, 2, 3) == (1, 2, 3): true
System.print("%(a) == %(c): %(a == c)") // (1, 2, 3) == (4, 5, 6): false
System.print("%(a) != %(b): %(a != b)") // (1, 2, 3) != (1, 2, 3): false
System.print("%(a) != %(c): %(a != c)") // (1, 2, 3) != (4, 5, 6): true
The above example would probably only work with Fn
s and Fiber
s since we can't really get the arity of methods.
This also makes me wonder about some other things we could experiment with like multi-value returns.
var fn = Fn.new{
return (1, 2, 3)
}
var x, y, z = fn.call()
// Or, if we wanted to have our own syntax for tuples or something.
var fn.call() >> x, y, z
var fn.call() ~> x, y, z
Some questions about this is whether the variables must match the length of the returned tuple or whether one variable could just contain the entire tuple (or whether extra values would be dropped). If we use some special operator for the tuple, then we're probably fine as the user has to explicitly deconstruct it.
// should this be allowed? Should it contain the full tuple or just the first element?
var a = fn.call()
// Would these drop the last element?
var a, b = fn.call()
var fn.call() ~> x, y
// Are these optimized differently? var a = [0, 1, 2] var b = (0, 1, 2)
Yes, this was already implemented for me locally from a while ago. I was thinking to use it for Array
but it makes more sense to use it for Tuple
. In the VM, the List
have basically the following data structure:
typedef struct
{
Obj obj;
// The elements in the list. (originally encapsulated in a ValueBuffer)
int capacity;
int count;
Value* data;
} ObjList;
My change set propose to add:
typedef struct
{
Obj obj;
size_t foreign_count;
size_t count;
Value data[FLEXIBLE_ARRAY];
// uint8_t foreign_data[FLEXIBLE_ARRAY];
} ObjMemorySegment;
The reason why it is a MemorySegement
is a little bit hairy, but basically it emulates what a computer memory segment is. It is meant to replace ObjForeign
and ObjInstance
in a single data structure, allowing foreign objects with fields in the future (but that is not the point of what I propose).
By reusing that structure for Tuple
, since size is fixed, every access to the data doesn't need an indirection through the data
pointer of the list implementation.
- I think it can be interesting to specialize call methods like:
class Tuple { ... foreign call(receiver) // implicitly invoke: receiver.call(...) foreign call(receiver, method) // implicitly invoke: receiver.method(...) (supporting subscript and setter syntax) }
I think this might be an interesting change but wonder if this means that tuples would need to be capped to the length of 16.
There is no need for such limitation, it can be checked when performing the call. Performing this check late is essential to allow to maintain the Array
behavior. It does also limit the number of place where the arbitrary MAX_PARAMETERS
is checked, allowing less assumptions for more easy maintenance of it (if we need to change the value later).
Also curious whether you could just pass either the tuple, the tuple with some kind of "spread" operator, or use some other operator to a parameter lists when calling
var fn = Fn.call {|a, b, c| System.print(a, b, c) } var tuple = (1 ,2, 3) fn.call(tuple) // Could this spread?
This is a consideration that I didn't thought deeply. And I don't have a definitive answer for it. While it may have been designed for this case, if one has to call it with a tuple, then the Tuples
have to be encapsulated in a tuple.
This is one of the reasons I prefer the Tuple.call
syntax which is not ambiguous and doesn't require such protection.
fn.call(...tuple) // Or could we do something like this instead?
While on paper it would be better (more explicit, and more in par with what other language do), it is not practical to introduce it yet. At compile type, since we don't know the count
of tuple, the generated code should generate the spread code and manually handle all the versions of the calls up to MAX_ARGUMENTS
.
While it is not impossible to do, it is really heavy, and really needs to be bench-marked. So I prefer to postpone the introduction of a spread operation for later.
// Other options to pass values of a tuple to a function tuple >> fn tuple ~> fn
I consider it bikeshedding for now, but it might need to be solved at some point.
// Just an example class. We'd probably want this to be a primitive class Tuple { // For this example, I'm just using lists for tuples. We'd probably want to have our own literal for tuples like: (1, 2, 3) construct new(array) { if (array.type != List) Fiber.abort("tuple must receive a list") _t = array } // Passes tuple values as parameters to Fn or Fiber type variables and calls them >>(fn) { if (fn.type != Fn && fn.type != Fiber) Fiber.abort("Cannot call value of type %(fn.type)") if (fn.arity == 0) return fn.call() if (fn.arity == 1 && _t.count >= 1) return fn.call(_t[0]) if (fn.arity == 2 && _t.count >= 2) return fn.call(_t[0], _t[1]) if (fn.arity == 3 && _t.count >= 3) return fn.call(_t[0], _t[1], _t[2]) if (fn.arity == 4 && _t.count >= 4) return fn.call(_t[0], _t[1], _t[2], _t[3]) if (fn.arity == 5 && _t.count >= 5) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4]) if (fn.arity == 6 && _t.count >= 6) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5]) if (fn.arity == 7 && _t.count >= 7) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6]) if (fn.arity == 8 && _t.count >= 8) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7]) if (fn.arity == 9 && _t.count >= 9) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8]) if (fn.arity == 10 && _t.count >= 10) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9]) if (fn.arity == 11 && _t.count >= 11) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10]) if (fn.arity == 12 && _t.count >= 12) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11]) if (fn.arity == 13 && _t.count >= 13) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11], _t[12]) if (fn.arity == 14 && _t.count >= 14) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11], _t[12], _t[13]) if (fn.arity == 15 && _t.count >= 15) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11], _t[12], _t[13], _t[14]) if (fn.arity == 16 && _t.count >= 16) return fn.call(_t[0], _t[1], _t[2], _t[3], _t[4], _t[5], _t[6], _t[7], _t[8], _t[9], _t[10], _t[11], _t[12], _t[13], _t[14], _t[15]) Fiber.abort("Insufficient values in tuple") } // Access values and tuple information [index] { _t[index] } count { _t.count } // Test equality on values of a tuple and not the reference of the tuple itself ==(other) { if (other.type != Tuple || count != other.count) return false for (i in 0...count) { if (this[i] != other[i]) return false } return true } !=(other) {!(this == other)} // just what a stringified tuple might look like instead of an array toString { var str = "(" for (i in 0...count) { if (i != 0) str = str + ", " str = str + _t[i].toString } str = str + ")" return str } }
It will most likely have a similar interface in the first implementation, but would be a subclass of Sequence
. Later we might need to have a ArrayedSequence
subclass, but it is not a priority.
// This could be more strict instead, and abort if tuple lengths do not match.
Well I'm not in favor of that. If we ignore the bug, nobody really complained about that issue, and there are interest in ignoring extra arguments. So unless a fatal/logic bug is raised, I prefer to have that functionality.
This also makes me wonder about some other things we could experiment with like multi-value returns.
var fn = Fn.new{ return (1, 2, 3) } var x, y, z = fn.call() // Or, if we wanted to have our own syntax for tuples or something. var fn.call() >> x, y, z var fn.call() ~> x, y, z
This is one thing I though. For performance reasons, I think we should allow multiple returns instead. The syntax would be nearly the same:
var fn_multiple_return = Fn.new{
return 1, 2, 3
}
{
var a = fn_multiple_return.call()
System.print(a) // expect: 1
}
{
var a, b, c, d = fn_multiple_return.call()
System.print(a) // expect: 1
System.print(b) // expect: 2
System.print(c) // expect: 3
System.print(d) // expect: null
}
var fn_tuple = Fn.new{
return (1, 2, 3) // Maybe we will want `()` doubling for uniformity
}
{
var a = fn_tuple.call()
System.print(a) // expect: (1, 2, 3)
}
If the elements themselves are to be mutable, I wonder whether it would be better to talk about (fixed-size) arrays rather than tuples? Off the top of my head, the only language I can think of which has mutable tuples is C# though they try to ride both horses by having a built-in immutable tuple type and a library-based mutable System.Tuple
type.
Having said that, you seem to be talking about the possibility of introducing an Array
type at some later stage. If so, how would that differ from tuples and lists?
Sorry, although irrelevant now, my point about 1-tuples being 'backdoor' constants was a specious one as there would, of course, be nothing to stop you assigning a different tuple to the variable itself.
With regard to the Tuple.filled
method, I'd have thought you'd be better to follow List.filled
and allow a second parameter rather than just set all elements automatically to null
.
Can I make a number of other points in no particular order:
-
It seems clear that there should be a performance advantage compared to lists though probably not a particularly significant one. Can I add that, from an allocation point of view, there would only ever need to be one allocation for a tuple, as 'count' would be known up-front.
-
I don't really understand why tuples would be any better than lists in distinguishing between passing them as regular arguments or as a group of parameters. In the latter case, you either want to allow for a variable number of parameters (which tuple couldn't handle anyway) or you have a fixed number of parameters exceeding 16 which is probably quite rare in practice.
-
An interesting aspect of @CrazyInfin8's example class was overloading the
==
and!=
operators to give the Tuple class value-type equality which could be a useful distinguishing characteristic between tuples and lists. However, having said that, there would be nothing to stop us giving the List class (or perhaps sequences in general) a named method to achieve the same goal. -
Although the ability to automatically destructure tuples (and perhaps other ordered sequences) is superficially attractive, I think the problem of deciding whether it's just returning one argument or several may be too difficult to solve in a simple language such as Wren.
-
Even if we had multiple return values, I think there are still problems with multiple assignment in general. For example what to do if some of the variables you want to use on the LHS have already been defined and some haven't. Should we still just use
var
for those cases or, ifvar
is used, should we insist that all LHS variables should be previously undefined?
Anyway I see you've now done a preview implementation so it will be interesting to play around with that :)
If the elements themselves are to be mutable, I wonder whether it would be better to talk about (fixed-size) arrays rather than tuples? Off the top of my head, the only language I can think of which has mutable tuples is C# though they try to ride both horses by having a built-in immutable tuple type and a library-based mutable
System.Tuple
type.
At least C++ have them mutable, and I find it superior in the sense that it makes a pendant to struct
where index replaces names.
Strictly speaking, in the case of wren it does not mater, if they are better named arrays or tuples, because of the lack of type safety/checking. But I still prefer the term tuples, since it implies that each elements are not required to be of the same type.
Having said that, you seem to be talking about the possibility of introducing an
Array
type at some later stage. If so, how would that differ from tuples and lists?
If type safety become a thing one day, list types can be homogeneous. but in essence Array
and Tuple
are the same thing. In C++ pseudo code:
template <size_t Size, typename T>
using Array = Tuple<T, /* repeat Size times */>
So it is more a convenience for the user.
List
on the other hand is an extension of Array
since it can grow.
Conceptually we would also need a resizable Tuple
but I think it is not possible without using dynamic meta class... (off topic)
Sorry, although irrelevant now, my point about 1-tuples being 'backdoor' constants was a specious one as there would, of course, be nothing to stop you assigning a different tuple to the variable itself.
With regard to the
Tuple.filled
method, I'd have thought you'd be better to followList.filled
and allow a second parameter rather than just set all elements automatically tonull
.
This is a minor detail, but added to my todo list.
Can I make a number of other points in no particular order:
- It seems clear that there should be a performance advantage compared to lists though probably not a particularly significant one. Can I add that, from an allocation point of view, there would only ever need to be one allocation for a tuple, as 'count' would be known up-front.
The benchmark I tried have mixed results. So I'd like to see if it has a real impact on some real code base.
But having a single allocation is a huge selling point for me.
- I don't really understand why tuples would be any better than lists in distinguishing between passing them as regular arguments or as a group of parameters. In the latter case, you either want to allow for a variable number of parameters (which tuple couldn't handle anyway) or you have a fixed number of parameters exceeding 16 which is probably quite rare in practice.
It is about emulating destructuring in API.
With List
, it is not trivial to distinguish between between a List argument or a List of arguments. Either one has to create a method or class to perform destructuring.
class Foo.new {
call(arg) { ... }
callAll(args) { ... }
}
Using Tuple
doesn't fully solve the issue, but the situation is handled with a single call. And it is up to the caller to create a Tuple
of Tuple
argument (which should be a rarity, unless doing extreme meta programing):
var Foo = Fn.new {|arg_or_args|
if (arg_or_args is Tuple) {
...
} else {
...
}
}
- An interesting aspect of @CrazyInfin8's example class was overloading the
==
and!=
operators to give the Tuple class value-type equality which could be a useful distinguishing characteristic between tuples and lists. However, having said that, there would be nothing to stop us giving the List class (or perhaps sequences in general) a named method to achieve the same goal.
This is true. I don't consider it a selling point, but functionally it is interesting to have it.
Off topic, I really which Object.!=
to be implement in assembly as !(this == rhs)
. It would remove a lot of dummy overloads...
- Although the ability to automatically destructure tuples (and perhaps other ordered sequences) is superficially attractive, I think the problem of deciding whether it's just returning one argument or several may be too difficult to solve in a simple language such as Wren.
While destructuring is an aspect of tuples, I don't want to be part of it for now. But that said, I think special care should be taken care, so we don't shoot ourself in the foot by preventing destructuring in some places.
About multiple returns, I think it is a topic to investigate more. I remember that there are situation could have been easier (while hacking wrenalyzer) if I was able to output more than one parameter (without having to rearchitect and output a class abstraction)...
- Even if we had multiple return values, I think there are still problems with multiple assignment in general. For example what to do if some of the variables you want to use on the LHS have already been defined and some haven't. Should we still just use
var
for those cases or, ifvar
is used, should we insist that all LHS variables should be previously undefined?
This is to consider, I didn't went that far since I'm not really familiar with destructuring. But currently I only want tuple to happen and avoid blocking us in the language if destructuring become a thing.
I'd forgotten about std::tuple
in C++ but you're right - the elements are mutable so happy to stick with Tuple
for wren.
Personally, I'd be surprised if Wren were ever to become statically typed as I think it would change the nature of the language and the compiler too much for most people's tastes. But who knows what may happen in the future!
Off topic, I really which Object.!= to be implement in assembly as !(this == rhs). It would remove a lot of dummy overloads...
I can only think it has been the done the way it has for implementation convenience as no other operators are automatically overloaded in pairs. It's difficult to think of any use case which would not require one to be the opposite of the other though there's some very weird stuff in maths and particle physics :)
Implementation got updated:
- Added
List.toTuple
- Added
Sequence.toTuple
(implementation is dumb, but required unless we rely on size) - Added
Tuple.filled
I have some prototype for Tuple.call
but I forgot one hairy implementation detail that forbid me to implement it as a builtin, So I had to do it by hand.
I made some progress to #1006 (not published yet), and I think I need a constant sequence.
While the API is secured and lazy optimized using private static hash tables, there are some resources (MethodMirror
lists and StackTrace
basically) that I don't want the user to be able to alter...
Would TupleConst
do? It would mean we would have
classDiagram
Sequence <|-- TupleConst
TupleConst <|-- Tuple
class Sequence {
-TupleConst toTupleConst
-Tuple toTuple
}
or
classDiagram
Sequence <|-- TupleConst
Sequence <|-- Tuple
class Sequence {
-TupleConst toTupleConst
-Tuple toTuple
}
I think, I prefer the second one, since if needed we can do:
class Tuple {
is(type) { super(type) || this is TupleConst }
}
until interface become a thing.
Back to our usual problems with naming things :)
If we need both, then my personal preference would be to use Array
for the mutable version and Tuple
for the immutable version as I think the chances of us wanting to use the former for anything else in the future are remote.
But, if you want to stick with Tuple
for the mutable version, then - whilst I can't say I like it - TupleConst
seems as good as anything. ImmutableTuple
is too much of a mouthful though FrozenTuple
might not be too bad.
An advantage of the first approach is that Tuple
could inherit a lot of stuff from TupleConst
thereby avoiding code duplication. OTOH, apart from Sequence
, we don't inherit from anything other than Object
in the core classes as this makes life easier for the VM.
The only reason I'm not keen on the second approach is because I don't really like overloading the is
operator. But, it's something which is allowed - and probably won't change - so I won't quarrel if that's the one you want to go with.
Proposing #1156 to solve this issue more broadly.