Giter VIP home page Giter VIP logo

Comments (11)

PureFox48 avatar PureFox48 commented on May 24, 2024

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 fromListmethod:

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.

from wren.

mhermier avatar mhermier commented on May 24, 2024

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:

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

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

  3. 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)
}
  1. 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, or List 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.

from wren.

CrazyInfin8 avatar CrazyInfin8 commented on May 24, 2024

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"

  1. 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)

  1. 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 Fns and Fibers 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

from wren.

mhermier avatar mhermier commented on May 24, 2024
// 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.

  1. 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)
}

from wren.

PureFox48 avatar PureFox48 commented on May 24, 2024

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:

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

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

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

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

  5. 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, if var 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 :)

from wren.

mhermier avatar mhermier commented on May 24, 2024

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 follow List.filled and allow a second parameter rather than just set all elements automatically to null.

This is a minor detail, but added to my todo list.

Can I make a number of other points in no particular order:

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

  1. 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 {
    ...
  }
}
  1. 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...

  1. 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)...

  1. 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, if var 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.

from wren.

PureFox48 avatar PureFox48 commented on May 24, 2024

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

from wren.

mhermier avatar mhermier commented on May 24, 2024

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.

from wren.

mhermier avatar mhermier commented on May 24, 2024

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.

from wren.

PureFox48 avatar PureFox48 commented on May 24, 2024

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.

from wren.

mhermier avatar mhermier commented on May 24, 2024

Proposing #1156 to solve this issue more broadly.

from wren.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.