Vector
Vector is a subclass of Collection that provides an ordered collection of elements, like List, but provides "reference semantics," which means that you can modify the elements of a Vector directly.
To use the Vector class, you should #include <systype.h> or #include <vector.h> in your source files.
Which should I use: List or Vector?
The List and Vector classes are very similar; both of these classes allow you to manage collections of values as a group. The differences between the classes are a little subtle, but they're important.
Lists offer two unique features. First, List is an intrinsic T3 VM datatype, which makes it the "universal" collection type; some functions and methods require list values, and will not accept other collection types. Second, Lists use "value semantics," so you never have to worry about the effects of changing a list value to which other parts of your program might be retaining references.
Vectors use "reference semantics," which are sometimes trickier to work with than a List's value semantics, but offer advantages in some situations. Reference semantics also make Vectors more efficient when you're performing an iterative process that involves repeated updates to a collection's elements: if you use a List for such a process, each update to an element would create a new list value, whereas changes to a Vector's elements simply change the existing Vector object, rather than creating a new Vector.
In general, you can decide which type of collection to use based on what you're going to do with it:
- If you're storing a value that will be used by many parts of your program, such as in an object property, and the value won't be changed frequently, List is a good choice. Because of a list's value semantics, the different parts of the code that refer to the same list won't have to coordinate their activities if they make local changes to the list.
- If you'll be updating the elements of a collection frequently, you should use a Vector. Using a Vector rather than a List avoids the overhead of creating a new copy of the collection every time you update one of its members.
- If you are dynamically building a collection through an iterative process that involves repeated changes to the collection (additions of new elements, removal of elements, or updates to existing element values), you should use a Vector.
Creating a Vector
To create a Vector, you use the new operator. You must pass an integer argument to the Vector constructor; this is an advisory value specifying the initial allocation size for the Vector.
// create a Vector with an initial allocation of 10 elements x = new Vector(10);
In addition, you can pass an optional second argument giving a List or Vector value to copy into the new Vector object. If this optional second argument is present, the new Vector is initialized with the elements from the argument:
x = new Vector(10, [1, 2, 3]);
Alternatively, you can pass an integer as the second argument, in which case the vector will be initialized with the given number of nil elements:
x = new Vector(10, 10);
The initial allocation size does not set an immutable upper bound for the number of elements in the Vector, nor does it specify the initial number of elements; this is purely an advisory figure that lets you make the Vector more efficient by providing a guess about how big the Vector might ultimately be. If you add elements to the Vector later that exceed the initial allocation, the system will automatically expand the Vector's memory allocation as needed to accommodate the new elements.
If no source list/vector is specified with the new operator, a newly created Vector has zero elements, regardless of the initial allocation size.
x = new Vector(100); say(x.length());
The code above displays 0, because a Vector never has any elements initially.
x = new Vector(1); x += 1; x += 2; x += 3; say(x.length());
The above code displays 3, because three elements have been added to the vector. This is perfectly legal; even though the vector's initial allocation size is only 1, you can still add any number of elements to the vector.
So, if the initial allocation size doesn't set the initial number of elements in the vector, and it doesn't set a maximum size for the vector, what good is it, and what does it matter what the value is? The answer is that the initial allocation size is purely advisory, and affects the memory efficiency of the vector. When you first create the vector, the system internally allocates the number of slots you specify in the initial allocation size; these slots are marked as "not yet in use," because the vector contains no elements at this point, but they're available for future use when you add elements. Later, if you add more elements than there are slots available, the vector automatically re-allocates its memory at a larger size.
If you make the initial allocation size too small, the system will have to re-allocate the vector's memory, possibly more than once, as you add new elements. If you make the initial allocation too large, the vector will take up more memory than it will ever actually need.
Don't worry about this too much, though. TADS 3 manages memory for you automatically, so it is not too dire a problem if your initial allocation is too high or too low. Any loss in efficiency resulting from an inaccurate initial allocation size will be fairly small. This parameter is provided only so that you can fine-tune your program's performance in cases where you have a pretty good idea in advance of how large a vector will be; in cases where you don't have any way of knowing, just pick a number that seems in the ballpark for a typical case.
Vector operators
The + operator adds new elements to the end of a Vector. If the operand on the right side of the + is a list or another Vector, its elements are individually added to the vector; otherwise, the value on the right side of the + is added as a single new element. Note that this operator always creates a new Vector to store the result; the original vector's value is unchanged.
The - operator removes elements from the Vector. If the operand on the right side of the - is a list or Vector, each element of the list of Vector is individually removed from the Vector on the left of the -. If the operand on the right side of the - is not a list or vector, each element of the vector whose value equals the right operand is deleted from the vector on the left. Note that the - operator always creates a new Vector to store the result.
The indexing operator [ ] can be used to get and set elements of the array using an integer index, just as with a List. If you assign an element of the vector past the current length of the vector, the vector is automatically extended to include the necessary number of elements; new elements between the last existing element and the element at the requested index are set to nil. If you try to retrieve a vector element with an index higher than any existing element, a run-time exception ("index out of range") is thrown.
A Vector can be used with the == or != operators to compare a Vector to another value. A Vector is equal to another Vector or List if the other Vector or List has the same number of elements, and each element of the Vector equals the corresponding element of the other Vector or List, using the same rules as the == operator to compare the elements.
Note: Because the == test is defined recursively, if a Vector contains a reference to itself, either directly or indirectly through another Vector, the == test can recurse infinitely. The Vector class avoids this infinite recursion by limiting the depth of recursion in an equality comparison to 256 levels. If this recursion depth is exceeded, the == test throws an exception ("maximum equality test/hash recursion depth exceeded"). This same exception will result, for the same reason, if a Vector with a self-reference is used as a key in a LookupTable. The recursion depth exception can occur even if a Vector contains no self-references, if it simply contains such a complex series of references that it exceeds the maximum depth. Note that this limit does not have anything to do with the number of elements in any Vector; rather, it pertains to the depth of the references from one Vector to another. So, if you create Vectors A, B, C, D, ..., and set A[1] = B, B[1] = C, C[1] = D, and so on for more than 256 vectors, then comparing A to another vector could exceed the maximum depth.
Vector methods
Vector is a subclass of Collection, so the Collection methods are available on a Vector object. In addition to the Collection methods, Vector provides many methods of its own, shown below.
append(val)
appendAll(val)
appendUnique(val)
applyAll(func)
This method is useful for transforming the elements of a vector by applying a modifier function. For example, if we have a vector of numbers, we could use this method to multiply each number in the vector by two:
x.applyAll({x: x*2});
This method is also handy for performing complex initializations on a new Vector. For example, here's a function that creates a new vector and initializes it with the first n Fibonacci numbers. Because we're simply initializing the new vector, note that the callback function doesn't make any reference to the original element value, but it must still declare a parameter for the argument value so that the arguments passed from applyAll() match the declaration.
createFibonacciVector(n) { local f0 = f1 = 1; return new Vector(n, n).applyAll(new function(x) { local ret = f0; f0 = f1; f1 += ret; return ret; }); }
Note that we specify the value n twice in the constructor to explicitly set the initial size of the vector to n nil elements. This is important because a newly-created vector normally doesn't contain any elements, regardless of the initial allocation setting; by explicitly using the initial length argument n, we ensure that applyAll() will visit n elements.
copyFrom(source, sourceStart, destStart, count)
Calling this method is equivalent to writing a code fragment like this:
for (local i = 0 ; i < count ; ++i) dest[destStart + i] = source[sourceStart + i];
If necessary, the method expands the self vector to make room for the added elements.
The copyFrom() method simply returns self; this is convenient for expressions like this:
x = new Vector(20).copyFrom(lst, 3, 2, 5);
countOf(val)
countWhich(cond)
fillValue(value, start?, count?)
If start isn't specified, the default starting index is 1.
If count isn't specified, the default count is self.length() - start + 1, or 0 if that yields a negative value. In other words, the default count is chosen to fill to the end of the existing elements of the Vector. Note that this is the actual populated length of the Vector, not the initial allocation size: new Vector(10).fillValue('x') yields a vector with zero elements filled in, not 10, because a Vector created this way is initially empty - the 10 is merely the initial allocation size hint, not the initial filled length.
This method is equivalent to writing a code fragment like this:
for (local i = 0 ; i < count ; ++i) dest[start + i] = value;
Calling fillValue() is easier than writing this code fragment, though, and considerably faster because it is implemented as native code.
This method returns self, which allows for expressions like this:
x = new Vector(20).fillValue('A', 1, 20);
forEach(func)
forEachAssoc(func)
getUnique()
indexOf(val)
indexWhich(cond)
insertAt(startingIndex, val, ...)
Returns the self object.
lastIndexOf(val)
lastIndexWhich(cond)
lastValWhich(cond)
length()
mapAll(func)
prepend(val)
removeElement(val)
removeElementAt(index)
removeRange(startingIndex, endingIndex)
Both startingIndex and endingIndex must refer to existing elements of the vector, and the ending index must be greater than or equal to the starting index; if these conditions don't hold, the method throws an error ("index out of range").
Returns the self object.
setLength(newLength)
sort(descending?, comparisonFunction?)
The optional comparisonFunction can be used to specify the ordering of the result. If this argument is not specified (or is nil), the method will sort the elements according to the standard system ordering of values; hence, the elements must be of comparable types (such as all integers or all strings). By specifying a comparison function, you can provide your own special ordering, and you can also sort values that have no system-defined order, such as object values.
The comparisonFunction works the same way as the for the List class's sort() method.
subset(func)
This method does not modify the original vector.
This example uses a short-form anonymous function to create to create a new vector that contains only the elements from an original vector whose values are greater than 10.
y = x.subset({x: x > 10});
toList(start?, count?)
This method is useful when you need to pass a value to a routine that requires a list value. Vectors cannot always be passed to routines requiring list values, so you can use this routine to create a list with the same values as the vector.
This method does not modify the vector.
valWhich(cond)
Reference Semantics
The most important distinction between lists and vectors, and the primary reason to use vectors rather than lists in certain situations, is that vectors use "reference" semantics, while lists use "value" semantics.
The difference is that a list's value can never change, but an vector's value can change.
When you do something that modifies a list, such as assigning a value to an element of the list, the operation does not change the list. Instead, it creates a new list that reflects the change, leaving the original list unmodified. TADS automatically updates the variable that contained the list being indexed so that it contains the newly-created list.
In contrast, when you assign a new value to an element of an vector, the vector's value is changed. No new vector object is created.
This might seem like a very obscure difference, but it has two important practical effects. The first is that operations that modify vectors are much cheaper to execute, because they don't result in creating new objects; this means that operations involving a large number of element changes will run faster with vectors than with lists.
The second practical difference is that, whenever you change a vector, the change is visible everywhere the vector is referenced. In contrast, when you change a list, the change is visible only to the code that made the change.
Consider this example:
local a = [1, 2, 3]; local b = a; a[2] = 100; tadsSay(b[2]);
What will this example display? At the beginning of the code, we set a to a list, and then we set b to the value in a, so b refers to the same list. So far we have only one object, and both a and b refer to this single object. We next assign a new value, 100, to the second element of a. As we've seen, this cannot change the list that a refers to, because lists can never change; so, what we're doing is creating a new list, copying each element from the original list to the new list, but changing the second element to reflect the assignment. This new list is then assigned to a, so a and b now refer to different lists. So, when we display the second element of b, we see the value "2" displayed, because b still refers to the original, unmodified list.
Now, consider the same example with an vector:
local a = new Vector(10, [1, 2, 3]); local b = a; a[2] = 100; tadsSay(b[2]);
This code looks almost identical, but it displays a different result than the list version. We start out by creating a new vector object and assigning it to a, and then we assign the same value to b. Next, we assign 100 to the second element of a. Unlike lists, vectors can be changed, so this assignment simply replaces the value in the vector object's second element. No new vector object is created, so a and b still refer to the same object. So, when we display b[2] in this example, we see the modified value.
Here's a more interesting example:
f1() { local a = new Vector(3); getInfo(a); "Thanks, <<a[1]>>! This information will allow us to send you specially targeted advertising based on your credit history! "; } getInfo(x) { "Please enter your name: "; x[1] = input(); "Please enter your age: "; x[2] = toInteger(input()); "Please enter your social security number: "; x[3] = input(); }
This is something we couldn't have done with lists: assigning elements of x in getInfo() wouldn't have affected the caller's copy of the list, so the routine wouldn't be able to pass back information this way using lists.
Note that, when you explicitly create a copy of a vector, the new copy is not affected by any changes to the original:
x = new Vector(10, [1, 2, 3, 4, 5]); y = new Vector(10, x); x[3] = 100; tadsSay(y[3]);
This example displays the value "3" (not "100"), because x and y refer to separate objects. Changing a value in the vector to which x refers has no effect on the vector to which y refers.