In this session, we're going to take a closer look at Lists. Lists provide quite a rich catalog of operations, and we learn how to use some of these operations in our work. To recap, there's the type list which takes a type parameter of the list elements. We can create lists by writing List in front of the elements of a list we want to create, or alternatively, we can create list using a sequence of cons operations, like you see here for nums. To decompose lists, we can do that either with a three standard methods, head, tail, and is empty, or preferably with a pattern match, like what you see here for the example, where we pull out the first and second element from the list, nums. Here are some of the most commonly used list methods. First, there is length, that gives you the length of the list. That means the number of elements in the list xs. Last gives the list's last element; exception if xs is empty. Last is the analog of head. Init gives you the list consisting of all elements of xs except the last one; exception if xs is empty. So init is the analog of tail. xs dot take n gives you a sub-list that consists of the first n elements of the list xs, or of the list xs if the list is shorter than n. Let's say you have the list 1, 2, 3, and you take 10 elements, you still get the list 1, 2, 3. xs drop n gives the rest of the list after taking n elements, or nothing if the list is exhausted before getting to the nth element. xs applied to n, or written out simply xs dot apply n, is the element of excess at index n. If the index is out of range of the list, you'll get an exception. To create new lists there's a first concat return double plus that gives you the list consisting of all elements of xs followed by all elements of ys. There's also reverse that reverses the elements of xs, that means it gives you a new list where the elements of xs appear in reversed order. Finally, there's updated n, x that gives you a new list that has the same elements as xs, except at position n, which must be in range, it gives you x. Updated is still purely functional. It doesn't touch the list xs, but it gives you a new list with the properties I've described. The following two methods are useful for finding elements. First, there is indexOf, xs index of x gives you the index of the first element in xs, that's equal to x or minus 1 if x doesn't appear in xs. Xs contains x gives you a Boolean that tells you whether x is an element of xs. Xs contains as x is thus the same as excess dot index of x greater or equal zero. Let's take a look how some of these methods are implemented. We know that the complexity of head is small, constant time because head just pulls out one of the fields of a const node. What is the complexity of last? To find out, let's write a possible implementation of last as a stand-alone function. Here's what we could write, last of a list xs of type list of T as we match on xs. If it's the empty list, then that would be an error. If it's a list consisting of just one element x, then the last element of that list would be x itself. The third case would be, it's a list consisting of a head y followed by a list, ys. In this case, we have to search for the last element in the list ys. The result is last of ys. If you look at this implementation, we see that last takes steps proportional to the length of the list xs; it has to go through the length of the list to find the last element. That makes last a lot less efficient than head. So if possible, always define your operations in terms of head and tail, and not last and init. In fact, looking at init, we can see here a possible implementation scheme. We have the same triple pattern match. If the list is empty, then init is not defined, so we get an error. If the list consists of just a single element, then init is the list without that element, so that would be the empty list. If the list consists of an element y followed by a list ys, then init would definitely contain y and the initial elements of ys. That's a complete implementation of init, and like last, its complexity is proportional to the length of the argument list, xs. As a third method, let's look at concatenation. How could concatenation be implemented? Let's try for a change to write an extension method for plus plus. We have an extension method, plus plus, that follows a list, xs, and it has this additional argument, a list ys. As usual, we'll start with a pattern match, but a pattern match on what list? Should we match on xs or should we match on ys? Turns out that it's advantageous to match on xs, so let's do that. We write xs match. If access is nil, what do we return? Well, in that case, the concatenation of nil at ys is ys. If xs is not nil, let's say it's an element x followed by a list xs1, then the concatenation would start with x and be followed by the concatenation of x1 and ys. What's the complexity of plus plus of a concat? Well, we have to do a pattern match only on the left list, but we go to the end of that left list, so the complexity would be proportional to the length of the left list xs. As another example, let's look at reverse. How can we implement reverse? Let's try again by writing an extension method. In this case, there's only one list excess. Let's try to pattern match on that list. If the list is nil, so reverse off the empty list is the empty list, of course. Reverse off a non-empty list starting with y and being followed by ys is, or one way we could do it is to say, well, let's reverse the list ys, and append the y as the last element to that list. It's ys reverse concat, list of y. What's the complexity of reverse? Well, we go through the list xs that gives us a factor linear in the length of the list, and then before each of the sub-lists, we call concat. That concat is, as we've learned proportionally in the length of the left list. That gives us a factor of x.length times x.length. The complexity of reverse is quadratic in the length of the list xs. That's a bit disappointing because if we had an array, we would know, of course, how to reverse it in linear time, just swap pairs of corresponding elements. One question is, does functional programming in this case have an inherent performance overhead or can we do better? We'll solve that question later. But before that, let's do an exercise. Let's write a function to remove the nth element of a list xs. If n is out of bounds, it should return xs itself. We're after function with this signature. It takes an index n and a list xs. The implementation should satisfy a usage example like this one here. Let's say if we remove the element at Index 1 of the list a, b, c, d, we should get the list a, c, d because the element at Index 1 is b, so that element should be dropped. I have set up the worksheet with the signature of the removeAt method. The example list xs, for the moment it doesn't work. If we look at the removeAt call, then it says not implemented error. Yes, of course, we have still a triple question mark as the body. The task now is to fill in the body of removeAt. One proven strategy is we pattern match on what xs is. We write xs match. If it's nil, then we said, well, remove anything from the list where the index doesn't exist, should return the list itself. We return nil. If the list is not nil, say consists of y followed by ys, what do we do? Well, now we look at the index n. If n equals 0, then we should return the list consisting of all elements except the first, so that's why ys. If n is not zero, then we'll have to do a recursive call, so Y would be part of the list, and that will be followed by removeAt n minus 1 and ys. If we test that, then removeAt 2, xs indeed gives us a list a, b, d. We can do to some other elements, let's say four. That doesn't do anything because there is no fourth element, three would remove the last element, and so on. Second exercise is a bit harder, we want to flatten a list structure. We're after a method with a signature like this one. It should take a list of any, and it gives us a list of any, and what it should do is if we have a list that has nested lists as elements, then those nested lists should all be flattened so that we get a single list that contains just the elements themselves without any embedded sub-lists. If we take flatten of that input and we should just get the leaf elements 1, 1, 2, 3, 5, 8 in that order in a single list. Again, I've given the setup here. We have the signature of flatten. The example is that was given in the exercise. If we look at the call, then it says, again, not implemented. In fact, it turns out that it's advantages if we give flatten a somewhat more general type. It takes an any and gives us back a list of any. The understanding is if the argument that it takes is not a list, we would just return a list consisting of that single argument. Flatten takes any combinations of arguments and it will always return a single list. Let's do that. What we do is we match on xs. We see we can match on more things, and lists it can also be any. If the list xs is nil, then flatten the list of nil gives us nil. If it's another list consisting of a head y followed by ys, then what do we do? Well, y could have embedded sub-lists, so let's flatten those, and so could ys, so let's flatten ys. Then we concatenate the results because we are interested in a single list. The other case, it is something that we don't know what it is. In this case, we return the xs followed by nil. If it's not a list, then just turn it into a list by making it the only element of the list we return. Let's try that. If we do that, then indeed flatten of ys gives us 1, 1, 2, 3, 5, 8. What we've seen here in the example of flatten is that pattern matching can be done on very general types including any. The patterns will always essentially pick out the right sub-parts and match it with the right-hand side. This can be quite useful for code that is weakly typed. That works on many different types in a homogeneous fashion.