Welcome to this first hands-on lecture on how to do functional programming with Scala. In this lecture, we're going to look at the most fundamental elements of programming. In fact, every non-trivial programming language provides primitive expressions, that represents the simplest elements of the language. That would be something like a number or a string, then it combines a way to combine expressions like add two numbers, concatenate two strings. Finally, it will provide ways to abstract expressions. Abstracting means we introduce a name for an expression, and then afterwards, we can refer to the expression by its name. A good way to approach functional programming is to think of it as a very powerful calculator. In fact, most programming languages, Scala included, have an interactive shell which is often called a REPL. REPL stands for Read Eval Print Loop. The REPL lets you write expressions and view responds with the value of those expressions. If you want to start the Scala REPL, you can simply type scala at the command prompt. Let's do that. We type scala and we get a REPL. It says you're starting dotty REPL because at the time I record this lecture, we are still having a pre-release of Scala 3 called dotty. We have the Scala prompt, and now we can type expressions like this one here. The REPL will respond with the value 232 and also its type, which is Int in this case. We'll learn more about types later. But functional programming languages, of course, are stronger than REPLs. For a starter, they also let you define values and functions and you can give them names. For instance, you can write def size equals 2, and now you have to find size and you can use it in another expression like this one. How are expressions in a functional language evaluated? In the end, it's, of course, the computer, but a good high-level way to think about it is, in very much the same way you would simplify an expression with a pen and paper. You would typically take the leftmost operator, evaluate the operands going left to right. Then apply the operator to the operands. If you have a name like size, then you evaluate that by replacing the name with the right-hand sides of its definition. Size would be two in the example that you saw previously. The evaluation process stops once the result is a value, and the value for now it's just a number. Later on, we'll see also other kinds of values. Let's just illustrate that with another arithmetic expressions. Two times Pi times radius where Pi and radius are defined like you see here. That would evaluate like this. You first take the name Pi and replace it by its definition. You perform the multiplication, you take the name radius and replace it by its definition. You perform the multiplication and you arrive at the result. Now let's do something a little bit more interesting. We can also define functions that take parameters. For instance, here's one. Now the REPL responds with a def, the name of the function, its parameter, and its result value. You can write then, square of two. You get four. You can write square of 5 plus 4. You get 81. Or you write square of 4, and you always get the result. Of course, we can push this further and now define a function that in turn caused square of [inaudible]. Let's call it the sum of squares. We have defined a function that takes two double parameters, and a double result. Let's look at the syntax of what we've done here. We can have function parameters. They are given by a name, and a colon, and a type. Functions can also have result types, in which case also we have a colon at the end, and its type. Types, for now, are basically just primitive types and those are as in Java, but they all start with a capital letter. We have Int for the usual integers, Long for 64-bit integers, Float double for floating point values, Char for characters, Short and Byte for shorter integers, and Boolean for the Boolean values true and false. Let's look at evaluation again. How would we evaluate an application of a parameterized function? In fact, it's done in a similar way to what we already know from operators. We evaluate all the function arguments from left to right. We replace the function application by the right-hand side, and at the same time, we replace the former parameters of the function by the actual arguments. Let's do this for the sum of squares example. Let's say we start with sum of squares 3, and 2 plus 2. We simplify the arguments, that simplifies to 2 plus 2 to 4. We replace it with the body of the function, and at the same time we replace the former parameters of sum of squares, which were X and Y, by the actual arguments which we're three for X, and four for Y. Now we're left with square of 3 plus square of 4. We simplify in turn, square of 3, that gives the body of square 3 times 3 plus square of 4. After simplification, we take the right operand call that expands to 4 times 4. Now we get the result 25. Now this will probably seem completely trivial to you. You might wonder why I insist so much on the details of evaluation. In fact, it turns out that this evaluation model, which we also call the substitution model, simple as it is, is universally powerful. That means we can express every algorithm with that model. The idea underlying the model is that all an evaluation does is reduce an expression to a value. It can be applied to all expressions as long as those expressions have no side effects, which means that those expressions are functional or purely functional. The substitution model has been formalized in the Lambda calculus, which gives a foundation for functional programming and for programming in general. In fact, the Lambda calculus is older than computers are. It was first invented by Alonzo Church, and it was intended as a foundation of mathematics. In fact, a couple of years later, another logician, called Godel, showed that a complete formalization of mathematics is impossible. So Lambda calculus was left as a model more for computable mathematics and for computing. Now that we know what evaluation is, we can ask interesting question. One interesting question is, does every expression reduce to a value in a finite number of steps? Does every evaluation terminate? Let's think a little bit. Can we find an expression whose evaluation would not terminate? In fact, yes, here is one. We can write an expression loop. It's of type Int [inaudible] , and its body is loop. If we reference loop in an expression, then, by our rule, the reference would be replaced by the body, which is, again, loop. So we get a beautiful and very tight loop of an expression that only evaluates to itself, and therefore evaluation of that expression will never terminate. Another interesting question to ask is whether the evaluation strategy we've seen is the only possible one or were there other possibilities? One particular detail is that our interpreter reduces function arguments to values before rewriting the function application. Can we change that? In fact, yes, we could alternatively apply the function to unreduced arguments. For instance, if we start with the same expression as before, sumOfSquares 3, 2 plus 2, then we would get square of 3 plus square 2 plus 2. So I haven't evaluated 2 plus 2, I just pass it like this. Then we would continue as before on the left, and on the right, we would now replace square of 2 plus 2 by 2 plus 2 times 2 plus 2. The parameter of square x gets replaced by the expression, not its value. Now, finally, I have to reduce the 2 plus 2 here and the 2 plus 2 here, and I will arrive at the same result. That alternative evaluation strategy is known as call-by-name, whereas the one we saw previously is called call-by-value because we evaluate things before we pass them. So we pass only values and not full expressions. In fact, it was no accident that both reduction sequences reduced to the same value 25; that's always the case. Both strategies reduce to the same final values as long as the reduced expression consists of pure functions, no side effects, and both evaluations terminate. Which one is better? Call-by-value or call-by-name? You might think that call-by-value is better because it evaluates every function argument only once. If you go back to the sequence that you saw here, you notice that the evaluation of 2 plus 2 is done twice because we passed it to square here and here. Now we have duplicate work to simplify 2 plus 2 to 4. Now, that's, of course, in this case, completely trivial. But instead of 2 plus 2, you could have an arbitrarily complicated expression, and then it starts to matter. Call-by-value definitely has the advantage that it evaluates every function argument only once. But call-by-name also has an advantage. Which is that? Well, if a parameter is completely unused in the evaluation of a function, then the function argument is not evaluated at all. Call-by-name can evaluate function arguments zero times, one times, or multiple times, whereas call-by-value always evaluates them exactly once. Sometimes, when it's zero, then call-by-name has an advantage. Let's make a little quiz about that. Let's say you have the following function definition: def test takes two parameters, x and y, and its body is x times x. So y is unused. For each of those functional applications that you see here, indicate which evaluation strategy is fastest. That means, has the fewest reduction steps. Call-by-value, call-by-name, or maybe sometimes they have the same number of steps. Let's see what the answers will be. If we write tests 2, 3, then, well, you have, in both cases, the evaluation 2 times 2, so they are the same. If we write test 3 plus 4, 8 then call-by-value is better because the expression in 3 plus 4 will be evaluated only once before it is passed into test. If you write test 7, 2 times 4, then call-by-name is faster because the evaluation of 2 times 4 is omitted altogether. Finally, if you write test 3 plus 4, 2 times 4, then essentially call-by-value has an advantage for the first parameter call-by-name, for the second parameter, and the two cancel each other out. So you will have, again, the same evaluation complexity.