In this session, we are going to look at some other sequence collections beyond lists. You've seen that lists are linear. That means access to the first element is much faster than access to the middle or end of a list. The Scala library also defines an alternative sequence implementation, which is called vector. This one has a more evenly balanced access pattern than list. The idea with a vector is that it's essentially a tree with a very high branch out factor. You have essentially here, if the vector is small up to 32 elements, then it's just an array. If the vector grows beyond 32 elements, then it becomes an array of arrays, each of which has 32 elements. We have 32 times 32 equals 1,024 elements. If the vector grows beyond that, then each of these upper race will again spawn 32 children of 32 elements each, and so on. That one gives now 32 times 1,024 elements and so on. The vector could have a maximum of five levels which would give you 2^5 times 5 so that's 2^25th elements. That's the maximum size of a vector. Now, if you have a structure like that, then let's see what would happen if you have to change a single element functionally without actually modifying the vector, creating a new vector. Let's say you change an element in this sub-array here. What you would need to do is essentially create a new array of 32 elements which contains the changed element. Let's say this one is this one here, so that's the one you change, and then its parents need to change as well. That one would go here and here, and all the other ones, and then finally, the root has to change as well. It's not free to change your single element functionally, you have to create, in this case, with a depth three arrays. In general, you have to modify as many arrays as is the depths of your tree. But the depth of your tree is very shallow maximum is five. But it's still much better than having to copy a array of 1,000, or 32,000, or 2^25th elements scaling. It's essentially what we have here is a compromise to have reasonably fast access times and reasonably fast update times in all situations. Now, how can you use vectors? They are created analogously to lists on a you write vector instead of lists. Here you define two vectors, a vector of numbers and a vector of people. Vector support by enlarge the same operations as lists, with the exception of cons. So there's no cons, but there are two operations that add an element to the left of the vector or add an element to the right. Since vectors are symmetric, there's no bonus anymore, only add elements to the left. Adding them on the right is just as efficient as adding them on the left. Note here, as a reminder, the colon always points to the sequence, so x plus colon xs means add a single element to the sequence xs, and xs column plus x means the opposite. The sequence goes left and the element goes right. We've seen list and we've seen vector and those two are in fact instances of a larger class hierarchy of collections in Scala. The common super trait of list and vector is called seq for sequence. That is again not the most general one, so that is again an instance of an even more general trait called Iterable. Iterable has other sub-traits, namely set and map, that we are going to see in the sessions that will come. You might ask what about array? Is that not a collection? Yes, you can see that as a collection, but since it's defined in Java, it can't be a direct sub-trait of sequence because sequence is defined in Scala. What happens instead is that array is a separate class, but they are automatic conversions that upcast array to sequence and that also gives array, again, all the operations that you find in sequence. Arrays and also strings support the same operation as seq and can be implicitly converted to sequences when needed. For instance, you can write xs equals array 1, 2, 3 and then you can use a map on an array or you can write val ys equals a string and you can use a filter on the string. This filter here would pick all the characters which are in uppercase. Arrays and vectors are not the only kind of sequences in the Scala collection library. Another important one is the range. That's a very simple kind of sequence of integers only. It represents a sequence of evenly spaced integers. It has three operations to build ranges, to, until, and by. One until five would give you the range consisting of the numbers 1, 2, 3, 4, so the five is not included. Whereas 1-5 would give you the same numbers with the five, so to is inclusive and until is exclusive. Then you can also vary the step of the range using the by clause, so 1-10 by 3 would give you the sequence 1, 4, 7, 10, and 6-1 by minus 1 would give you 6, 4, 2, and the zero would no longer be included because it's already smaller than the end one. Ranges have a particular compact representation, a range is represented as a single object with just the three fields; the lower bound, the upper bound, and the step value. Here's some more operations that work on all sequences; on vectors, on lists, on ranges. You can ask whether there is an element in the sequence that satisfies a predicate p that's called xs.exists(p), and dually you can ask whether all elements in the sequence satisfy predicate p that's done with a for-all. You can have a zip. A zip takes two sequences, and it forms a sequence of pairs drawn from corresponding elements of sequences xs and ys. If one of them is longer than the other, then it's truncated to make them fit. For instance, if you had, let's say, the list 1, 2, 3, zip vector A, B, you can zip sequences of different type, and the result will always be the same type as the first sequence, so that would give the list of pairs, namely one and A, and two and B. Unzip is the opposite of zip, so it takes a list of pairs and gives you two lists; one list consisting of the first halves of the pair, and the second list consisting of the second halves of the pair. FlatMap is an important function. It's a little bit like map, but it takes a function that returns itself a collection, and what it will do is it will apply f to each element, and then concatenate all the results. It will concatenate all the collection results to give a single sequence. Sum and product form, the sum and product of a sequence of numbers. Maximum gives you the maximum of all elements of the sequence and the minimum. Let's do an example that uses some of the operations that we discussed. To list all combinations of numbers x and y, where x is drawn from 1-m, and y is drawn from 1-n, what would we do? Well, we start with the range 1-m, and then we do a flatMap of the following operation. We have the range 1-n, and for each element in that range, call that y, we give you the pair x, y. The x comes from the first range, the y comes from the second range, and with the pair we do all the combination. That by itself, the 1-n map operation gives you a sequence, and all these sequences are concatenated by this flatMap call that you see here. If you're curious, you might ask, well, this is a range, and I do a map on a range, what kind of collections do I get back? It can't be a range because the elements of that collections are pairs and not integers. The answer is what I get back is the default sequence type which in this case would be a vector. The run-time type of this thing is vector. Here's another example. To compute the scalar product of two vectors, we can write a function like this. Def scalarProduct of xs vector double ys vector double, it's xs.zip(ys). Map the function that takes corresponding elements x and y, forms the product of x and y, and afterwards, we sum it all up with.sum. Note that there's some automatic decomposition going on here. Xs.zip(ys) gives a list of pairs. Map takes a pair, but here the lambda with parts to the map is a function of two parameters. Behind the scenes, the compiler will automatically decompose the pair and put the first half in x and the second half in y. If we wanted to be more explicit, we could also write scalar product like that. Instead of decomposing the pair, and letting the compiler do it, we take the xy which is now the parameter that ranges over the pair, and we take the first half of xy and the second half of xy and multiply. On the other hand, if you wanted to be even more concise, we could also write it like this: xs.zip(ys) map of underscore times underscore.sum. As we've seen before, the function underscore is the function that takes a parameter x and a parameter y, and multiplies them. Most people would write scalar product like this. It's clearly the most elegant way to express it, and in terms of runtime performance, all three versions are basically equivalent. Here's an exercise for you. A number n is prime if the only divisors of n are one and n itself. What's a high level way to write a test for primality of numbers? For once, value conciseness over efficiency. So filling the triple question marks here for the test where they're a given integer n is prime. So let's see how would we write that test. If we go literally, we say for all numbers between one excluded and n excluded, that number is not a divisor of prime. So let's just write that literally. We say two until n for all these numbers, we have that n modulo the parameter not equal 0. You see one of the nice aspects of functional programming and powerful collection operations is that you can write very, very high level code that directly translates a mathematical specification into code.