-
Notifications
You must be signed in to change notification settings - Fork 564
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
[RFC] Adding a Tuple
with language support
#1151
Comments
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 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: There are few advantages for tuples:
Most of those behaviors exist in 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. |
var a = (1)
// Wouldn't work
a[1] = 4
// Though these may
a = (7)
a = "some other value type"
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 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 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 |
Yes, this was already implemented for me locally from a while ago. I was thinking to use it for 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
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
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
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
I consider it bikeshedding for now, but it might need to be solved at some point.
It will most likely have a similar interface in the first implementation, but would be a subclass of
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 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 Having said that, you seem to be talking about the possibility of introducing an 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 Can I make a number of other points in no particular order:
Anyway I see you've now done a preview implementation so it will be interesting to play around with that :) |
At least C++ have them mutable, and I find it superior in the sense that it makes a pendant to
If type safety become a thing one day, list types can be homogeneous. but in essence template <size_t Size, typename T>
using Array = Tuple<T, /* repeat Size times */> So it is more a convenience for the user.
This is a minor detail, but added to my todo list.
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.
It is about emulating destructuring in API. With class Foo.new {
call(arg) { ... }
callAll(args) { ... }
} Using var Foo = Fn.new {|arg_or_args|
if (arg_or_args is Tuple) {
...
} else {
...
}
}
This is true. I don't consider it a selling point, but functionally it is interesting to have it. Off topic, I really which
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)...
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 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!
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:
I have some prototype for |
I made some progress to #1006 (not published yet), and I think I need a constant sequence. Would 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 But, if you want to stick with An advantage of the first approach is that The only reason I'm not keen on the second approach is because I don't really like overloading the |
Proposing #1156 to solve this issue more broadly. |
Hi,
The present RFC is about adding a
Tuple
class. It would follow the usual syntax:It would also act as an array container (non resizable):
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:
But, I'm not sure about all the implications. In particular with the variable declaration syntax, so it has to be tough more deeper.
The text was updated successfully, but these errors were encountered: