diff --git a/lectures/progfun1-1-1.md b/lectures/progfun1-1-1.md new file mode 100644 index 0000000000000000000000000000000000000000..bc05bfd966aaae39ddc89353357f8ac39faec015 --- /dev/null +++ b/lectures/progfun1-1-1.md @@ -0,0 +1,204 @@ +% Functional Programming Principles in Scala +% Martin Odersky +% \today + +Programming Paradigms +===================== + +Paradigm: In science, a \red{paradigm} describes distinct concepts or +thought patterns in some scientific discipline. + +Main programming paradigms: + + - imperative programming + - functional programming + - logic programming + +Orthogonal to it: + + - object-oriented programming + +Review: Imperative programming +============================== + +Imperative programming is about + + - modifying mutable variables, + - using assignments + - and control structures such as if-then-else, loops, break, continue, return. + +The most common informal way to understand imperative programs is as instruction sequences for a Von Neumann computer. + +Imperative Programs and Computers +================================= + +There's a strong correspondence between + +\begin{tabular}{lll} +Mutable variables & $\textcolor{navy}{\approx}$ & memory cells \\ +Variable dereferences & $\textcolor{navy}{\approx}$ & load instructions \\ +Variable assignments & $\textcolor{navy}{\approx}$ & store instructions \\ +Control structures & $\textcolor{navy}{\approx}$ & jumps +\end{tabular} +\bigskip + +\red{Problem}: Scaling up. How can we avoid conceptualizing programs word by word? + +\red{Reference}: John Backus, Can Programming Be Liberated from the von. Neumann Style?, Turing Award Lecture 1978. + +Scaling Up +========== + +In the end, pure imperative programming is limited by the "Von Neumann" +bottleneck: + +\begin{quote} +One tends to conceptualize data structures word-by-word. +\end{quote} + +We need other techniques for defining high-level abstractions such as collections, polynomials, geometric shapes, strings, documents. + +Ideally: Develop _theories_ of collections, shapes, strings, ... + +What is a Theory? +================= + +A theory consists of + + - one or more _data types_ + - _operations_ on these types + - _laws_ that describe the relationships between values and operations + +Normally, a theory does not describe mutations! + +Theories without Mutation +========================= + +For instance the theory of polynomials defines the sum of two polynomials by laws such as: + + (a*x + b) + (c*x + d) = (a + c)*x + (b + d) + +But it does not define an operator to change a coefficient while keeping the polynomial the same! +-> +Whereas in an imperative program one _can_ write: + +\begin{lstlisting} + class Polynomial { double[] coefficient; } + Polynomial p = ...; + p.coefficient[0] = 42; +\end{lstlisting} + +Theories without Mutation +========================= + +\red{Other example:} + +The theory of strings defines a concatenation operator ++ which is associative: + + (a ++ b) ++ c = a ++ (b ++ c) + +But it does not define an operator to change a sequence element while keeping the sequence the same! + +(This one, some languages _do_ get right; e.g. Java's strings are immutable) + +Consequences for Programming +============================ + +If we want to implement high-level concepts following their mathematical theories, +there's no place for mutation. + + - The theories do not admit it. + - Mutation can destroy useful laws in the theories. + +Therefore, let's + + - concentrate on defining theories for operators expressed as functions, + - avoid mutations, + - have powerful ways to abstract and compose functions. + +Functional Programming +====================== + + - In a _restricted_ sense, functional programming (FP) means programming without mutable variables, assignments, loops, and other imperative control structures. + - In a _wider_ sense, functional programming means focusing on the functions and immutable data. + - In particular, functions can be values that are produced, consumed, and composed. + - All this becomes easier in a functional language. + +Functional Programming Languages +================================ + + - In a _restricted_ sense, a functional programming language is one which does not have mutable variables, assignments, or imperative control structures. + - In a _wider_ sense, a functional programming language enables the + construction of elegant programs that focus on functions and immutable data structures. + - In particular, functions in a FP language are first-class citizens. This means + \begin{itemize} + \item they can be defined anywhere, including inside other functions + \item like any other value, they can be passed as parameters to functions and returned as results + \item as for other values, there exists a set of operators to compose functions + \end{itemize} + +Some functional programming languages +===================================== + +In the restricted sense: + + - Pure Lisp, XSLT, XPath, XQuery, FP + - Haskell (without I/O Monad or UnsafePerformIO) + +In the wider sense: + + - (Lisp, Scheme), Racket, Clojure + - SML, Ocaml, F\# + - Haskell (full language) + - Scala + - (Smalltalk, Ruby) + +(...): languages with first class functions but incomplete support for immutable data + +History of FP languages +======================= + +\begin{tabular}{llp{2cm}ll} + 1959 & (Lisp) && 2003 & Scala +\\ 1975-77 & ML, FP, Scheme && 2005 & F\# +\\ 1978 & (Smalltalk) && 2007 & Clojure +\\ 1986 & Standard ML && 2012 & Elixir +\\ 1990 & Haskell, Erlang && 2014 & Swift +\\ 2000 & OCaml && 2017 & Idris +\\ & && 2020 & Scala 3 +\end{tabular} + +Scala 3 is the language we will use in this course. + +Recommended Book (1) +==================== + +Structure and Interpretation of Computer Programs. Harold +Abelson and Gerald J. Sussman. 2nd edition. MIT Press 1996. + +\includegraphics[scale=0.4]{images/sicp.jpg} + +A classic. Many parts of the course and quizzes are based on it, +but we change the language from Scheme to Scala. + +The full text [\blue{can be downloaded here.}](http://mitpress.mit.edu/sicp/) + + +Recommended Book (2) +==================== + +Programming in Scala. Martin Odersky, Lex Spoon, and Bill Venners. 3rd edition. Artima 2016. + +\includegraphics[scale=0.2]{images/cover.pdf} + +The standard language introduction and reference. + +Other Recommended Books +======================= + +\ \hfill +\includegraphics[scale=0.25]{images/scala-impatient.png} \hfill +\includegraphics[scale=0.33]{images/programming-scala.jpeg} \hfill +\includegraphics[scale=0.45]{images/scala-in-depth.jpeg} \hfill\ + + diff --git a/lectures/progfun1-1-2.md b/lectures/progfun1-1-2.md new file mode 100644 index 0000000000000000000000000000000000000000..65ab2da4286ac07690bc21438a9f149ef440d4d7 --- /dev/null +++ b/lectures/progfun1-1-2.md @@ -0,0 +1,437 @@ +% Elements of Programming +% +% + +Elements of Programming +======================= + +Every non-trivial programming language provides: + + - primitive expressions representing the simplest elements + - ways to _combine_ expressions + - ways to _abstract_ expressions, which introduce a name for an expression by which it can then be referred to. + +The Read-Eval-Print Loop +======================== + +Functional programming is a bit like using a calculator + +An interactive shell (or REPL, for Read-Eval-Print-Loop) lets one +write expressions and responds with their value. + +The Scala REPL can be started by simply typing + +\begin{lstlisting} +> scala +\end{lstlisting} + +Expressions +=========== + +Here are some simple interactions with the REPL + + scala> 87 + 145 + res0: Int = 232 + +Functional programming languages are more than simple calcululators +because they let one define values and functions: + + scala> def size = 2 + size: Int + + scala> 5 * size + res1: Int = 10 + +Evaluation +========== + +A non-primitive expression is evaluated as follows. + + 1. Take the leftmost operator + 2. Evaluate its operands (left before right) + 3. Apply the operator to the operands + +A name is evaluated by replacing it with the right hand side of its definition + +The evaluation process stops once it results in a value + +A value is a number (for the moment) + +Later on we will consider also other kinds of values + +Example +======= + +Here is the evaluation of an arithmetic expression: + + def pi = 3.14159 + + def radius = 10 + + (2 * pi) * radius + +Example +======= + +Here is the evaluation of an arithmetic expression: + + (2 * pi) * radius + + (2 * 3.14159) * radius + +Example +======= + +Here is the evaluation of an arithmetic expression: + + (2 * pi) * radius + + (2 * 3.14159) * radius + + 6.28318 * radius + +Example +======= + +Here is the evaluation of an arithmetic expression: + + (2 * pi) * radius + + (2 * 3.14159) * radius + + 6.28318 * radius + + 6.28318 * 10 + +Example +======= + +Here is the evaluation of an arithmetic expression: + + (2 * pi) * radius + + (2 * 3.14159) * radius + + 6.28318 * radius + + 6.28318 * 10 + + 62.8318 + +Parameters +========== + +Definitions can have parameters. For instance: + + scala> def square(x: Double) = x * x + square: (x: Double)Double + + scala> square(2) + 4.0 + + scala> square(5 + 4) + 81.0 + + scala> square(square(4)) + 256.0 + + scala> def sumOfSquares(x: Double, y: Double) = square(x) + square(y) + sumOfSquares: (x: Double, y: Double)Double + + +Parameter and Return Types +========================== + +Function parameters come with their type, which is given after a colon + + def power(x: Double, y: Int): Double = ... + +If a return type is given, it follows the parameter list. + +Primitive types are as in Java, but are written capitalized, e.g: + +\begin{tabular}{ll} + \verb@Int@ & 32-bit integers +\\ \verb@Double@ & 64-bit floating point numbers +\\ \verb@Boolean@ & boolean values \verb@true@ and \verb@false@ +\end{tabular} + + + +Evaluation of Function Applications +=================================== + +Applications of parameterized functions are evaluated in a similar way as operators: + + 1. Evaluate all function arguments, from left to right + 2. Replace the function application by the function's right-hand side, and, at the same time + 3. Replace the formal parameters of the function by the actual arguments. + +Example +======= + + sumOfSquares(3, 2+2) + + +Example +======= + + sumOfSquares(3, 2+2) + sumOfSquares(3, 4) + + +Example +======= + + sumOfSquares(3, 2+2) + sumOfSquares(3, 4) + square(3) + square(4) + + +Example +======= + + sumOfSquares(3, 2+2) + sumOfSquares(3, 4) + square(3) + square(4) + 3 * 3 + square(4) + + + +Example +======= + + sumOfSquares(3, 2+2) + sumOfSquares(3, 4) + square(3) + square(4) + 3 * 3 + square(4) + 9 + square(4) + + +Example +======= + + sumOfSquares(3, 2+2) + sumOfSquares(3, 4) + square(3) + square(4) + 3 * 3 + square(4) + 9 + square(4) + 9 + 4 * 4 + +Example +======= + + sumOfSquares(3, 2+2) + sumOfSquares(3, 4) + square(3) + square(4) + 3 * 3 + square(4) + 9 + square(4) + 9 + 4 * 4 + 9 + 16 + + +Example +======= + + sumOfSquares(3, 2+2) + sumOfSquares(3, 4) + square(3) + square(4) + 3 * 3 + square(4) + 9 + square(4) + 9 + 4 * 4 + 9 + 16 + 25 + + + +The substitution model +====================== + +This scheme of expression evaluation is called the _substitution model_. + +The idea underlying this model is that all evaluation does is _reduce +an expression to a value_. + +It can be applied to all expressions, as long as they have no side effects. + +The substitution model is formalized in the \red{$\lambda$-calculus}, which gives a +foundation for functional programming. + +Termination +=========== + +> - Does every expression reduce to a value (in a finite number of steps)? + + +Termination +=========== + +> - Does every expression reduce to a value (in a finite number of steps)? +> - No. Here is a counter-example + + def loop: Int = loop + + loop + +Changing the evaluation strategy +================================ + +The interpreter reduces function arguments to values before rewriting the function application. + +One could alternatively apply the function to unreduced arguments. + +For instance: + + sumOfSquares(3, 2+2) + +Changing the evaluation strategy +================================ + +The interpreter reduces function arguments to values before rewriting the function application. + +One could alternatively apply the function to unreduced arguments. + +For instance: + + sumOfSquares(3, 2+2) + square(3) + square(2+2) + +Changing the evaluation strategy +================================ + +The interpreter reduces function arguments to values before rewriting the function application. + +One could alternatively apply the function to unreduced arguments. + +For instance: + + sumOfSquares(3, 2+2) + square(3) + square(2+2) + 3 * 3 + square(2+2) + +Changing the evaluation strategy +================================ + +The interpreter reduces function arguments to values before rewriting the function application. + +One could alternatively apply the function to unreduced arguments. + +For instance: + + sumOfSquares(3, 2+2) + square(3) + square(2+2) + 3 * 3 + square(2+2) + 9 + square(2+2) + +Changing the evaluation strategy +================================ + +The interpreter reduces function arguments to values before rewriting the function application. + +One could alternatively apply the function to unreduced arguments. + +For instance: + + sumOfSquares(3, 2+2) + square(3) + square(2+2) + 3 * 3 + square(2+2) + 9 + square(2+2) + 9 + (2+2) * (2+2) + +Changing the evaluation strategy +================================ + +The interpreter reduces function arguments to values before rewriting the function application. + +One could alternatively apply the function to unreduced arguments. + +For instance: + + sumOfSquares(3, 2+2) + square(3) + square(2+2) + 3 * 3 + square(2+2) + 9 + square(2+2) + 9 + (2+2) * (2+2) + 9 + 4 * (2+2) + +Changing the evaluation strategy +================================ + +The interpreter reduces function arguments to values before rewriting the function application. + +One could alternatively apply the function to unreduced arguments. + +For instance: + + sumOfSquares(3, 2+2) + square(3) + square(2+2) + 3 * 3 + square(2+2) + 9 + square(2+2) + 9 + (2+2) * (2+2) + 9 + 4 * (2+2) + 9 + 4 * 4 + +Changing the evaluation strategy +================================ + +The interpreter reduces function arguments to values before rewriting the function application. + +One could alternatively apply the function to unreduced arguments. + +For instance: + + sumOfSquares(3, 2+2) + square(3) + square(2+2) + 3 * 3 + square(2+2) + 9 + square(2+2) + 9 + (2+2) * (2+2) + 9 + 4 * (2+2) + 9 + 4 * 4 + 25 + + +Call-by-name and call-by-value +============================== + +The first evaluation strategy is known as _call-by-value_, +the second is is known as _call-by-name_. + +Both strategies reduce to the same final values +as long as + + - the reduced expression consists of pure functions, and + - both evaluations terminate. + +Call-by-value has the advantage that it evaluates every function argument only once. + +Call-by-name has the advantage that a function argument is not evaluated if the corresponding parameter is unused in the evaluation of the function body. + + +Call-by-name vs call-by-value +============================= + +\quiz + +Question: Say you are given the following function definition: + + def test(x: Int, y: Int) = x * x + +For each of the following function applications, indicate which evaluation strategy is fastest (has the fewest reduction steps) + + CBV CBN same + fastest fastest #steps + + O O O test(2, 3) + O O O test(3+4, 8) + O O O test(7, 2*4) + O O O test(3+4, 2*4) + +Call-by-name vs call-by-value +============================= + + def test(x: Int, y: Int) = x * x + + test(2, 3) + test(3+4, 8) + test(7, 2*4) + test(3+4, 2*4) diff --git a/lectures/progfun1-1-3.md b/lectures/progfun1-1-3.md new file mode 100644 index 0000000000000000000000000000000000000000..c3251354a6d10186042723a1757d2e712393f469 --- /dev/null +++ b/lectures/progfun1-1-3.md @@ -0,0 +1,90 @@ +% Evaluation Strategies and Termination +% +% + +Call-by-name, Call-by-value and termination +=========================================== + +You know from the last module that the call-by-name and call-by-value +evaluation strategies reduce an expression to the same value, +as long as both evaluations terminate. + +But what if termination is not guaranteed? + +We have: + + - If CBV evaluation of an expression $e$ terminates, then CBN evaluation of $e$ terminates, too. + - The other direction is not true + +Non-termination example +======================= + +Question: Find an expression that terminates under CBN but not under CBV. + +\quiz + +Non-termination example +======================= + +Let's define + + def first(x: Int, y: Int) = x + +and consider the expression `first(1, loop)`. + +\bigskip +\mbox{Under CBN: ~~~~~~~~~~~~~~~~~~~~~~~ Under CBV:} +\bigskip + + first(1, loop) first(1, loop) + +Scala's evaluation strategy +=========================== + +Scala normally uses call-by-value. + +But if the type of a function parameter starts with `=>` it uses call-by-name. + +Example: + + def constOne(x: Int, y: => Int) = 1 + +Let's trace the evaluations of + + constOne(1+2, loop) + +and + + constOne(loop, 1+2) + +Trace of `constOne(1 + 2, loop)` +================================ + + constOne(1 + 2, loop) + +Trace of `constOne(1 + 2, loop)` +================================ + + constOne(1 + 2, loop) + constOne(3, loop) + +Trace of `constOne(1 + 2, loop)` +================================ + + constOne(1 + 2, loop) + constOne(3, loop) + 1 + +Trace of `constOne(loop, 1 + 2)` +================================ + + constOne(loop, 1 + 2) + +Trace of `constOne(loop, 1 + 2)` +================================ + + constOne(loop, 1 + 2) + constOne(loop, 1 + 2) + constOne(loop, 1 + 2) + ... + diff --git a/lectures/progfun1-1-4.md b/lectures/progfun1-1-4.md new file mode 100644 index 0000000000000000000000000000000000000000..a7f399b4fc02f16d986835e4e220609cd632fa93 --- /dev/null +++ b/lectures/progfun1-1-4.md @@ -0,0 +1,111 @@ +% Conditionals and Value Definitions +% +% + +Conditional Expressions +======================= + +To express choosing between two alternatives, Scala +has a conditional expression `if-then-else`. + +It resembles an `if-else` in Java, but is used for expressions, not statements. + +Example: + + def abs(x: Int) = if x >= 0 then x else -x + +`x >= 0` is a _predicate_, of type `Boolean`. + +Boolean Expressions +=================== + +Boolean expressions `b` can be composed of + + true false // Constants + !b // Negation + b && b // Conjunction + b || b // Disjunction + +and of the usual comparison operations: + + e <= e, e >= e, e < e, e > e, e == e, e != e + +Rewrite rules for Booleans +========================== + +Here are reduction rules for Boolean expressions (`e` is an arbitrary expression): + + !true --> false + !false --> true + true && e --> e + false && e --> false + true || e --> true + false || e --> e + +Note that `&&` and `||` do not always need their right operand to be evaluated. + +We say, these expressions use "short-circuit evaluation". + +Exercise: Formulate rewrite rules for if-then-else +================================================= + +\quiz + + + + +Value Definitions +================= + +We have seen that function parameters can be passed by value or be passed by name. + +The same distinction applies to definitions. + +The `def` form is "by-name", its right hand side is evaluated on each use. + +There is also a `val` form, which is "by-value". Example: + + val x = 2 + val y = square(x) + +The right-hand side of a `val` definition is evaluated at the point of the definition itself. + +Afterwards, the name refers to the value. + +For instance, `y` above refers to `4`, not `square(2)`. + +Value Definitions and Termination +================================= + +The difference between `val` and `def` becomes apparent when the right +hand side does not terminate. Given + + def loop: Boolean = loop + +A definition + + def x = loop + +is OK, but a definition + + val x = loop + +will lead to an infinite loop. + + +Exercise +======== + +\quiz + +Write functions `and` and `or` such that for all argument expressions `x` and `y`: + + and(x, y) == x && y + or(x, y) == x || y + +(do not use `||` and `&&` in your implementation) + +What are good operands to test that the equalities hold? + + + diff --git a/lectures/progfun1-1-5.md b/lectures/progfun1-1-5.md new file mode 100644 index 0000000000000000000000000000000000000000..5c6e814b8cece95efcbbb8faab92151afe519201 --- /dev/null +++ b/lectures/progfun1-1-5.md @@ -0,0 +1,84 @@ +% Example: Square roots with Newton's method +% +% +Task +==== + +We will define in this session a function + + /** Calculates the square root of parameter x */ + def sqrt(x: Double): Double = ... + +The classical way to achieve this is by successive approximations using +Newton's method. + +Method +====== + +To compute `sqrt(x)`: + + - Start with an initial _estimate_ `y` (let's pick `y = 1`). + - Repeatedly improve the estimate by taking the mean of `y` and `x/y`. + +Example: + + Estimation Quotient Mean + + 1 2 / 1 = 2 1.5 + 1.5 2 / 1.5 = 1.333 1.4167 + 1.4167 2 / 1.4167 = 1.4118 1.4142 + 1.4142 ... ... + + +Implementation in Scala (1) +=========================== + +First, define a function which computes one iteration step + + def sqrtIter(guess: Double, x: Double): Double = + if isGoodEnough(guess, x) then guess + else sqrtIter(improve(guess, x), x) + +Note that `sqrtIter` is _recursive_, its right-hand side calls itself. + +Recursive functions need an explicit return type in Scala. + +For non-recursive functions, the return type is optional + +Implementation in Scala (2) +=========================== + +Second, define a function `improve` to improve an estimate and a test to check for terminatation: + + def improve(guess: Double, x: Double) = + (guess + x / guess) / 2 + + def isGoodEnough(guess: Double, x: Double) = + abs(guess * guess - x) < 0.001 + +Implementation in Scala (3) +=========================== + +Third, define the `sqrt` function: + + def sqrt(x: Double) = sqrtIter(1.0, x) + +Exercise +======== + +\quiz + +1. The `isGoodEnough` test is not very precise for small numbers and can +lead to non-termination for very large numbers. Explain why. + +2. Design a different version of `isGoodEnough` that does not have these problems. + +3. Test your version with some very very small and large numbers, e.g. \par + +\begin{verbatim} + 0.001 + 0.1e-20 + 1.0e20 + 1.0e50 +\end{verbatim} + diff --git a/lectures/progfun1-1-6.md b/lectures/progfun1-1-6.md new file mode 100644 index 0000000000000000000000000000000000000000..c21d0f2850c8946a99a15c70a739dbb02f2db179 --- /dev/null +++ b/lectures/progfun1-1-6.md @@ -0,0 +1,195 @@ +% Blocks and Lexical Scope +% +% +Nested functions +================ + +It's good functional programming style to split up a task into many small functions. + +But the names of functions like `sqrtIter`, `improve`, and `isGoodEnough` matter only for the _implementation_ of `sqrt`, not for its _usage_. + +Normally we would not like users to access these functions directly. + +We can achieve this and at the same time avoid "name-space pollution" by +putting the auxciliary functions inside `sqrt`. + +The `sqrt` Function, Take 2 +=========================== + + def sqrt(x: Double) = { + def sqrtIter(guess: Double, x: Double): Double = + if isGoodEnough(guess, x) then guess + else sqrtIter(improve(guess, x), x) + + def improve(guess: Double, x: Double) = + (guess + x / guess) / 2 + + def isGoodEnough(guess: Double, x: Double) = + abs(square(guess) - x) < 0.001 + + sqrtIter(1.0, x) + } + +The `sqrt` Function, Take 2 +=========================== + + def sqrt(x: Double) = + def sqrtIter(guess: Double, x: Double): Double = + if isGoodEnough(guess, x) then guess + else sqrtIter(improve(guess, x), x) + + def improve(guess: Double, x: Double) = + (guess + x / guess) / 2 + + def isGoodEnough(guess: Double, x: Double) = + abs(square(guess) - x) < 0.001 + + sqrtIter(1.0, x) + +In Scala 3, braces are optional for indented code. + +Blocks in Scala +=============== + +- A block is delimited by braces `{ ... }`. +\begin{verbatim} + { val x = f(3) + x * x + } +\end{verbatim} +- It contains a sequence of definitions or expressions. +- The last element of a block is an expression that defines its value. +- This return expression can be preceded by auxiliary definitions. +- Blocks are themselves expressions; a block may appear everywhere an expression can. +- In Scala 3, braces are optional (i.e. implied) around a correctly indented expression that appears after `=`, `then`, `else`, ... . + +Blocks and Visibility +===================== + + val x = 0 + def f(y: Int) = y + 1 + val result = + val x = f(3) + x * x + +- The definitions inside a block are only visible from within the block. +- The definitions inside a block _shadow_ definitions of the same names + outside the block. + +Exercise: Scope Rules +===================== + +\quiz + +Question: What is the value of `result` in the following program? + + val x = 0 + def f(y: Int) = y + 1 + val result = { + val x = f(3) + x * x + } + x + +Possible answers: + + O 0 + O 16 + O 32 + O reduction does not terminate + + + +Lexical Scoping +=============== + +Definitions of outer blocks are visible inside a block unless they are shadowed. + +Therefore, we can simplify `sqrt` by eliminating redundant occurrences of the `x` parameter, which means everywhere the same thing: + +The `sqrt` Function, Take 3 +=========================== + + def sqrt(x: Double) = + def sqrtIter(guess: Double): Double = + if isGoodEnough(guess) then guess + else sqrtIter(improve(guess)) + + def improve(guess: Double) = + (guess + x / guess) / 2 + + def isGoodEnough(guess: Double) = + abs(square(guess) - x) < 0.001 + + sqrtIter(1.0) + +End Markers +=========== + +With heavily indented code it is sometimes hard to see where a construct ends. + +End markers are a tool to make this explicit. + + def f() = + + ... + ... + ... + + end f + + - And end marker is followed by the name that's defined in the + definition that ends at this point. + + - It must align with the opening keyword (`def` in this case). + +The `sqrt` Function, Take 4 +=========================== + + def sqrt(x: Double) = + def sqrtIter(guess: Double): Double = + if isGoodEnough(guess) then guess + else sqrtIter(improve(guess)) + + def improve(guess: Double) = + (guess + x / guess) / 2 + + def isGoodEnough(guess: Double) = + abs(square(guess) - x) < 0.001 + + sqrtIter(1.0) + end sqrt + +Semicolons +========== + +In Scala, semicolons at the end of lines are in most cases optional + +You could write + + val x = 1; + +but most people would omit the semicolon. + +On the other hand, if there are more than one statements on a line, they need to be separated by semicolons: + + val y = x + 1; y * y + +Summary +======= + +You have seen simple elements of functional programing in Scala. + +- arithmetic and boolean expressions +- conditional expressions if-then-else +- functions with recursion +- nesting and lexical scope + +You have learned the difference between the call-by-name and +call-by-value evaluation strategies. + +You have learned a way to reason about program execution: reduce expressions using the substitution model. + +This model will be an important tool for the coming sessions. + + + diff --git a/lectures/progfun1-1-7.md b/lectures/progfun1-1-7.md new file mode 100644 index 0000000000000000000000000000000000000000..0476effd36c73077dd4f73b58c4fb8c026b43c6f --- /dev/null +++ b/lectures/progfun1-1-7.md @@ -0,0 +1,135 @@ +% Tail Recursion +% +% + +Review: Evaluating a Function Application +========================================= + +One simple rule : One evaluates a function application $\btt f(e_1, ..., e_n)$ + +- by evaluating the expressions $\btt e_1, \ldots, e_n$ resulting in the values +$\btt v_1, ..., v_n$, then +- by replacing the application with the body of the function `f`, +in which +- the actual parameters $\btt v_1, ..., v_n$ replace the +formal parameters of `f`. + + +Application Rewriting Rule +========================== + +This can be formalized as a \red{rewriting of the program itself}: + +$$ +\begin{array}{ll} + &\btt def\ f (x_1, ..., x_n) = B ;\ ...\ f (v_1, ..., v_n) \\ + \rightarrow & \\ + &\btt def\ f (x_1, ..., x_n) = B ;\ ...\ [v_1/x_1, ..., v_n/x_n]\,B +\end{array} +$$ + + +Here, $\btt [v_1/x_1, ..., v_n/x_n]\,B$ means: + +The expression $\btt B$ in which all occurrences of $\btt x_i$ have been replaced +by $\btt v_i$. + +$\btt [v_1/x_1, ..., v_n/x_n]$ is called a \red{substitution}. + +Rewriting example: +================== + +Consider `gcd`, the function that computes the greatest common divisor of two numbers. + +Here's an implementation of `gcd` using Euclid's algorithm. + + def gcd(a: Int, b: Int): Int = + if b == 0 then a else gcd(b, a % b) + +Rewriting example: +================== + +`gcd(14, 21)` is evaluated as follows: +\medskip + + + ` gcd(14, 21)` +-> + $\rightarrow$ ` if 21 == 0 then 14 else gcd(21, 14 % 21)` +-> + $\rightarrow$ ` if false then 14 else gcd(21, 14 % 21)` +-> + $\rightarrow$ ` gcd(21, 14 % 21)` +-> + $\rightarrow$ ` gcd(21, 14)` +-> + $\rightarrow$ ` if 14 == 0 then 21 else gcd(14, 21 % 14)` +-> + $\rightarrow\dhd$ ` gcd(14, 7)` +-> + $\rightarrow\dhd$ ` gcd(7, 0)` +-> + $\rightarrow$ ` if 0 == 0 then 7 else gcd(0, 7 % 0)` +-> + $\rightarrow$ ` 7` + +Another rewriting example: +========================== + +Consider `factorial`: + + def factorial(n: Int): Int = + if n == 0 then 1 else n * factorial(n - 1) +\medskip + + ` factorial(4)` +-> + $\rightarrow$ ` if 4 == 0 then 1 else 4 * factorial(4 - 1)` +3-> + $\rightarrow\dhd$ ` 4 * factorial(3)` +-> + $\rightarrow\dhd$ ` 4 * (3 * factorial(2))` +-> + $\rightarrow\dhd$ ` 4 * (3 * (2 * factorial(1)))` +-> + $\rightarrow\dhd$ ` 4 * (3 * (2 * (1 * factorial(0)))` +-> + $\rightarrow\dhd$ ` 4 * (3 * (2 * (1 * 1)))` +-> + $\rightarrow\dhd$ ` 24` + + +What are the differences between the two sequences? + +Tail Recursion +============== + +\red{Implementation Consideration:} +If a function calls itself as its last action, the function's stack frame can be reused. This is called _tail recursion_. + +$\Rightarrow$ Tail recursive functions are iterative processes. + +In general, if the last action of a function consists of calling a +function (which may be the same), one stack frame would be sufficient for +both functions. Such calls are called _tail-calls_. + +Tail Recursion in Scala +======================= + +In Scala, only directly recursive calls to the current function are optimized. + +One can require that a function is tail-recursive using a `@tailrec` annotation: + + @tailrec + def gcd(a: Int, b: Int): Int = ... + +If the annotation is given, and the implementation of `gcd` were not tail recursive, an error would be issued. + + +Exercise: Tail recursion +======================== + +\quiz + +Design a tail recursive version of \verb@factorial@. + diff --git a/lectures/progfun1-2-1.md b/lectures/progfun1-2-1.md new file mode 100644 index 0000000000000000000000000000000000000000..50fe7290f5ca6258ba2bb297eea6c8faf64918fc --- /dev/null +++ b/lectures/progfun1-2-1.md @@ -0,0 +1,153 @@ +% Higher-Order Functions +% +% +Higher-Order Functions +====================== + +Functional languages treat functions as _first-class values_. + +This means that, like any other value, a function +can be passed as a parameter and returned as a result. + +This provides a flexible way to compose programs. + +Functions that take other functions as parameters or that return functions +as results are called _higher order functions_. + +Example: +======== + +Take the sum of the integers between `a` and `b`: + + def sumInts(a: Int, b: Int): Int = + if a > b then 0 else a + sumInts(a + 1, b) + +Take the sum of the cubes of all the integers between `a` +and `b` : + + def cube(x: Int): Int = x * x * x + + def sumCubes(a: Int, b: Int): Int = + if a > b then 0 else cube(a) + sumCubes(a + 1, b) + +Example (ctd) +============= + +Take the sum of the factorials of all the integers between `a` +and `b` : + + def sumFactorials(a: Int, b: Int): Int = + if a > b then 0 else fact(a) + sumFactorials(a + 1, b) + + +These are special cases of +$$ + \sum^b_{n=a} f(n) +$$ +for different values of $f$. + +Can we factor out the common pattern? + +Summing with Higher-Order Functions +=================================== + +Let's define: + + def sum(f: Int => Int, a: Int, b: Int): Int = + if a > b then 0 + else f(a) + sum(f, a + 1, b) + +We can then write: + + def sumInts(a: Int, b: Int) = sum(id, a, b) + def sumCubes(a: Int, b: Int) = sum(cube, a, b) + def sumFactorials(a: Int, b: Int) = sum(fact, a, b) + +where + + def id(x: Int): Int = x + def cube(x: Int): Int = x * x * x + def fact(x: Int): Int = if x == 0 then 1 else x * fact(x - 1) + +Function Types +============== + +The type `A => B` is the type of a \red{function} that +takes an argument of type `A` and returns a result of +type `B`. + +So, `Int => Int` is the type of functions that map integers to integers. + +Anonymous Functions +=================== + +Passing functions as parameters leads to the creation of many small functions. + + - Sometimes it is tedious to have to define (and name) these functions using `def`. + +Compare to strings: We do not need to define a string using `def`. Instead of + + def str = "abc"; println(str) + +We can directly write + + println("abc") + +because strings exist as _literals_. Analogously we would like function literals, which let us write a function without giving it a name. + +These are called _anonymous functions_. + +Anonymous Function Syntax +========================= + +\example: A function that raises its argument to a cube: + + (x: Int) => x * x * x + +Here, `(x: Int)` is the \red{parameter} of the function, and +`x * x * x` is it's \red{body}. + +- The type of the parameter can be omitted if it can be inferred by the +compiler from the context. + +If there are several parameters, they are separated by commas: + + (x: Int, y: Int) => x + y + +Anonymous Functions are Syntactic Sugar +======================================= + +An anonymous function $(\btt x_1: T_1, ..., x_n: T_n) \Rightarrow E$ +can always be expressed using `def` as follows: + +$$ +\btt { def\ f(x_1: T_1, ..., x_n: T_n) = E ; f } +$$ + +where $\btt f$ is an arbitrary, fresh name (that's not yet used in the program). + + - One can therefore say that anonymous functions are _syntactic sugar_. + +Summation with Anonymous Functions +================================== + +Using anonymous functions, we can write sums in a shorter way: + + def sumInts(a: Int, b: Int) = sum(x => x, a, b) + def sumCubes(a: Int, b: Int) = sum(x => x * x * x, a, b) + +Exercise +======== + +\quiz + +The `sum` function uses linear recursion. Write a tail-recursive version by replacing the `???`s. + + def sum(f: Int => Int, a: Int, b: Int): Int = { + def loop(a: Int, acc: Int): Int = { + if ??? then ??? + else loop(???, ???) + } + loop(???, ???) + } + diff --git a/lectures/progfun1-2-2.md b/lectures/progfun1-2-2.md new file mode 100644 index 0000000000000000000000000000000000000000..dd1c7d2954a27322c8304445ce534f8d71a00744 --- /dev/null +++ b/lectures/progfun1-2-2.md @@ -0,0 +1,165 @@ +% Currying +% +% +Motivation +========== + +Look again at the summation functions: + + def sumInts(a: Int, b: Int) = sum(x => x, a, b) + def sumCubes(a: Int, b: Int) = sum(x => x * x * x, a, b) + def sumFactorials(a: Int, b: Int) = sum(fact, a, b) + +\question + +Note that `a` and `b` get passed unchanged from `sumInts` and `sumCubes` into `sum`. + +Can we be even shorter by getting rid of these parameters? + + + +Functions Returning Functions +============================= + +Let's rewrite `sum` as follows. + + def sum(f: Int => Int): (Int, Int) => Int = { + def sumF(a: Int, b: Int): Int = + if a > b then 0 + else f(a) + sumF(a + 1, b) + sumF + } + +`sum` is now a function that returns another function. + +The returned function `sumF` applies the given function parameter `f` and sums the results. + + +Stepwise Applications +===================== + +We can then define: + + def sumInts = sum(x => x) + def sumCubes = sum(x => x * x * x) + def sumFactorials = sum(fact) + +These functions can in turn be applied like any other function: + + sumCubes(1, 10) + sumFactorials(10, 20) + +Consecutive Stepwise Applications +================================= + +In the previous example, can we avoid the `sumInts`, `sumCubes`, ... middlemen? + +Of course: + + sum (cube) (1, 10) +-> +- `sum(cube)` applies `sum` to `cube` and returns +the _sum of cubes_ function. + +- `sum(cube)` is therefore equivalent to `sumCubes`. + +- This function is next applied to the arguments `(1, 10)`. +-> +Generally, function application associates to the left: + + sum(cube)(1, 10) == (sum (cube)) (1, 10) + +Multiple Parameter Lists +======================== + +The definition of functions that return functions is so useful in +functional programming that there is a special syntax for it in Scala. + +For example, the following definition of `sum` is equivalent to +the one with the nested `sumF` function, but shorter: + + def sum(f: Int => Int)(a: Int, b: Int): Int = + if a > b then 0 else f(a) + sum(f)(a + 1, b) + +Expansion of Multiple Parameter Lists +===================================== + +In general, a definition of a function with multiple parameter lists + +$$\btt + def\ f (args_1) ... (args_n) = E +$$ + +where $\btt n > 1$, is equivalent to +$$\btt + def\ f (args_1) ... (args_{n-1}) = \{ def\ g (args_n) = E ; g \} +$$ + +where `g` is a fresh identifier. +Or for short: + +$$\btt + def\ f (args_1) ... (args_{n-1}) = ( args_n \Rightarrow E ) +$$ + +Expansion of Multiple Parameter Lists (2) +========================================= + +By repeating the process $n$ times +$$\btt + def\ f (args_1) ... (args_{n-1}) (args_n) = E +$$ +is shown to be equivalent to +$$\btt + def\ f = (args_1 \Rightarrow ( args_2 \Rightarrow ... ( args_n \Rightarrow E ) ... )) +$$ + +This style of definition and function application is called _currying_, +named for its instigator, Haskell Brooks Curry (1900-1982), a twentieth century logician. + +In fact, the idea goes back even further to Schönfinkel and Frege, but the term ``currying" has stuck. + +More Function Types +=================== + +Question: Given, + + def sum(f: Int => Int)(a: Int, b: Int): Int = ... + +What is the type of `sum` ? + +More Function Types +=================== + +Question: Given, + + def sum(f: Int => Int)(a: Int, b: Int): Int = ... + +What is the type of `sum` ? + +\boldred{Answer:} + + (Int => Int) => (Int, Int) => Int + +Note that functional types associate to the right. That is to say that + + Int => Int => Int + +is equivalent to + + Int => (Int => Int) + +Exercise +======== + +\quiz + + 1. Write a `product` function that calculates the product of + the values of a function for the points on a given interval. + + 2. Write `factorial` in terms of `product`. + + 3. Can you write a more general function, which generalizes + both `sum` and `product`? + + + diff --git a/lectures/progfun1-2-3.md b/lectures/progfun1-2-3.md new file mode 100644 index 0000000000000000000000000000000000000000..79bd586ac9f33eea384a40da7b6b3e847e4f26f4 --- /dev/null +++ b/lectures/progfun1-2-3.md @@ -0,0 +1,154 @@ +% Example: Finding Fixed Points +% +% +Finding a fixed point of a function +=================================== + +A number `x` is called a \red{fixed point} of a function `f` if + + f(x) = x + +For some functions `f` we can locate the fixed points by +starting with an initial estimate and then by applying `f` in a repetitive way. + + x, f(x), f(f(x)), f(f(f(x))), ... + +until the value does not vary anymore (or the change is sufficiently +small). + +Programmatic Solution +===================== + +This leads to the following function for finding a fixed point: + + val tolerance = 0.0001 + def isCloseEnough(x: Double, y: Double) = + abs((x - y) / x) / x < tolerance + def fixedPoint(f: Double => Double)(firstGuess: Double) = { + def iterate(guess: Double): Double = { + val next = f(guess) + if isCloseEnough(guess, next) then next + else iterate(next) + } + iterate(firstGuess) + } + +Return to Square Roots +====================== + +Here is a _specification_ of the `sqrt` function: + +$\gap$ `sqrt(x)` $=$ the number `y` such that `y * y = x`. + +Or, by dividing both sides of the equation with `y`: + +$\gap$ `sqrt(x)` $=$ the number `y` such that `y = x / y`. + +Consequently, `sqrt(x)` is a fixed point of the function `(y => x / y)`. + +First Attempt +============= + +This suggests to calculate `sqrt(x)` by iteration towards a fixed point: + + def sqrt(x: Double) = + fixedPoint(y => x / y)(1.0) + +Unfortunately, this does not converge. + +Let's add a `println` instruction to the function `fixedPoint` so +we can follow the current value of `guess`: + +First Attempt (2) +================= + + def fixedPoint(f: Double => Double)(firstGuess: Double) = + + def iterate(guess: Double): Double = + val next = f(guess) + println(next) + if isCloseEnough(guess, next) then next + else iterate(next) + + iterate(firstGuess) + +`sqrt(2)` then produces: + + 2.0 + 1.0 + 2.0 + 1.0 + ... + +Average Damping +=============== + +One way to control such oscillations is to prevent the estimation from +varying too much. This is done by _averaging_ successive values of the +original sequence: + + def sqrt(x: Double) = fixedPoint(y => (y + x / y) / 2)(1.0) + +This produces + + 1.5 + 1.4166666666666665 + 1.4142156862745097 + 1.4142135623746899 + 1.4142135623746899 + +In fact, if we expand the fixed point function `fixedPoint` we find a similar +square root function to what we developed last week. + +Functions as Return Values +========================== + +The previous examples have shown that the expressive power of a +language is greatly increased if we can pass function arguments. + +The following example shows that functions that return functions +can also be very useful. + +Consider again iteration towards a fixed point. + +We begin by observing that $\sqrt x$ is a fixed point of the function +`y => x / y`. + +Then, the iteration converges by averaging successive values. + +This technique of _stabilizing by averaging_ is general enough to +merit being abstracted into its own function. + + def averageDamp(f: Double => Double)(x: Double) = (x + f(x)) / 2 + +Exercise: +========= + +Write a square root function using +`fixedPoint` and `averageDamp`. + +\quiz + +Final Formulation of Square Root +================================ + + def sqrt(x: Double) = fixedPoint (averageDamp (y => x/y)) (1.0) + +This expresses the elements of the algorithm as clearly as possible. + +Summary +======= + +We saw last week that the functions are essential abstractions because +they allow us to introduce general methods to perform computations as +explicit and named elements in our programming language. + +This week, we've seen that these abstractions can be combined with +higher-order functions to create new abstractions. + +As a programmer, one must look for opportunities to abstract and +reuse. + +The highest level of abstraction is not always the best, but it +is important to know the techniques of abstraction, so as to +use them when appropriate. diff --git a/lectures/progfun1-2-4.md b/lectures/progfun1-2-4.md new file mode 100644 index 0000000000000000000000000000000000000000..ed92d4d01c8017401908cacb68138eb42bf6abed --- /dev/null +++ b/lectures/progfun1-2-4.md @@ -0,0 +1,94 @@ +% Scala Syntax Summary +% +% + +Language Elements Seen So Far: +============================== + +We have seen language elements to express types, expressions +and definitions. + +Below, we give their context-free syntax in Extended Backus-Naur form (EBNF), +where + +$\gap$ `|` denotes an alternative, \par + +$\gap$ `[...]` an option (0 or 1), \par + +$\gap$ `{...}` a repetition (0 or more). + +Types +===== + +\begin{lstlisting} +Type = SimpleType | FunctionType +FunctionType = SimpleType `=>' Type + | `(' [Types] `)' `=>' Type +SimpleType = Ident +Types = Type {`,' Type} +\end{lstlisting} + +A \red{type} can be: + + - A \red{numeric type}: `Int`, `Double` (and `Byte`, `Short`, `Char`, `Long`, `Float`), + - The `Boolean` type with the values `true` and `false`, + - The `String` type, + - A \red{function type}, like `Int => Int`, `(Int, Int) => Int`. + +Later we will see more forms of types. + +Expressions +=========== + +\begin{lstlisting} +Expr = InfixExpr | FunctionExpr + | if Expr then Expr else Expr +InfixExpr = PrefixExpr | InfixExpr Operator InfixExpr +Operator = ident +PrefixExpr = [`+' | `-' | `!' | `~' ] SimpleExpr +SimpleExpr = ident | literal | SimpleExpr `.' ident + | Block +FunctionExpr = Bindings `=>` Expr +Bindings = ident + | `(' [Binding {`,' Binding}] `)' +Binding = ident [`:' Type] +Block = `{' {Def `;'} Expr `}' + | <indent> {Def `;'} Expr <outdent> +\end{lstlisting} + +Expressions (2) +=============== + +An \red{expression} can be: + +- An \red{identifier} such as `x`, `isGoodEnough`, +- A \red{literal}, like `0`, `1.0`, `"abc"`, +- A \red{function application}, like `sqrt(x)`, +- An \red{operator application}, like `-x`, `y + x`, +- A \red{selection}, like `math.abs`, +- A \red{conditional expression}, like `if x < 0 then -x else x`, +- A \red{block}, like `{ val x = math.abs(y) ; x * 2 }` +- An \red{anonymous function}, like `x => x + 1`. + +Definitions +=========== + +\begin{lstlisting} +Def = FunDef | ValDef +FunDef = def ident {`(' [Parameters] `)'} + [`:' Type] `=' Expr +ValDef = val ident [`:' Type] `=' Expr +Parameter = ident `:' [ `=>' ] Type +Parameters = Parameter {`,' Parameter} +\end{lstlisting} + +A \red{definition} can be: + +- A \red{function definition}, like `def square(x: Int) = x * x` +- A \red{value definition}, like `val y = square(2)` + +A \red{parameter} can be: + +- A \red{call-by-value parameter}, like `(x: Int)`, +- A \red{call-by-name parameter}, like `(y: => Double)`. + diff --git a/lectures/progfun1-2-5.md b/lectures/progfun1-2-5.md new file mode 100644 index 0000000000000000000000000000000000000000..837cf3c4dce0919ecfc112c973a51bb92452eeaa --- /dev/null +++ b/lectures/progfun1-2-5.md @@ -0,0 +1,175 @@ +% Functions and Data +% +% + +Functions and Data +================== + +In this section, we'll learn how functions create and encapsulate data +structures. + +\example + +Rational Numbers + +We want to design a package for doing rational arithmetic. + +A rational number $\blue{\frac{x}{y}}$ is represented by two integers: + + - its _numerator_ $\blue{x}$, and + - its _denominator_ $\blue{y}$. + +Rational Addition +================= + +Suppose we want to implement the addition of two rational numbers. + + def addRationalNumerator(n1: Int, d1: Int, n2: Int, d2: Int): Int + def addRationalDenominator(n1: Int, d1: Int, n2: Int, d2: Int): Int + +but it would be difficult to manage all these numerators and denominators. + +A better choice is to combine the numerator and +denominator of a rational number in a data structure. + +Classes +======= + +In Scala, we do this by defining a \red{class}: + + class Rational(x: Int, y: Int) + def numer = x + def denom = y + +This definition introduces two entities: + + - A new \red{type}, named `Rational`. + - A \red{constructor} `Rational` to create elements of this type. + +Scala keeps the names of types and values in \red{different namespaces}. +So there's no conflict between the two defintions of `Rational`. + +Objects +======= + +We call the elements of a class type \red{objects}. + +We create an object by calling the constructor of the class: + +\example + + Rational(1, 2) + +Members of an Object +==================== + +Objects of the class `Rational` have two \red{members}, +`numer` and `denom`. + +We select the members of an object with the infix operator `.' (like in Java). + +\example + +\begin{tabular}{ll} + \verb@val x = Rational(1, 2)@ \wsf x: Rational = Rational@2abe0e27 +\\ + x.numer \wsf 1 +\\ + x.denom \wsf 2 +\end{tabular} + +Rational Arithmetic +=================== + +We can now define the arithmetic functions that implement the standard rules. + +$$ +\ba{l} +\frac{n_1}{d_1} + \frac{n_2}{d_2} \ = \ \frac{n_1 d_2 + n_2 d_1}{d_1 d_2} +\\[1em] +\frac{n_1}{d_1} - \frac{n_2}{d_2} \ = \ \frac{n_1 d_2 - n_2 d_1}{d_1 +d_2} +\\[1em] +\frac{n_1}{d_1} \cdot \frac{n_2}{d_2} \ = \ \frac{n_1 n_2}{d_1 d_2} +\\[1em] +\frac{n_1}{d_1} / \frac{n_2}{d_2} \ = \ \frac{n_1 d_2}{d_1 n_2} +\\[1em] +\frac{n_1}{d_1} = \frac{n_2}{d_2} \quad \small \mbox{iff} \quad n_1 d_2 = d_1 n_2 +\ea +$$ + +Implementing Rational Arithmetic +================================ + + def addRational(r: Rational, s: Rational): Rational = + Rational( + r.numer * s.denom + s.numer * r.denom, + r.denom * s.denom) + + def makeString(r: Rational) = + r.numer + "/" + r.denom + +\begin{worksheet} + \verb@makeString(addRational(new Rational(1, 2), new Rational(2, 3)))@ \wsf 7/6 +\end{worksheet} + + +Methods +======= + +One can go further and also package functions operating on a data +abstraction in the data abstraction itself. + +Such functions are called \red{methods}. + +\example + +Rational numbers now would have, in addition to the functions `numer` +and `denom`, the functions `add`, `sub`, +`mul`, `div`, `equal`, `toString`. + +Methods for Rationals +===================== + +Here's a possible implementation: + + class Rational(x: Int, y: Int) { + def numer = x + def denom = y + def add(r: Rational) = + new Rational(numer * r.denom + r.numer * denom, + denom * r.denom) + def mul(r: Rational) = ... + ... + override def toString = numer + "/" + denom + } + +\red{Remark}: the modifier `override` declares that `toString` +redefines a method that already exists (in the class `java.lang.Object`). + +Calling Methods +=============== + + +Here is how one might use the new `Rational` abstraction: + + val x = new Rational(1, 3) + val y = new Rational(5, 7) + val z = new Rational(3, 2) + x.add(y).mul(z) + +Exercise +======== + +1. In your worksheet, add a method `neg` to class Rational that is used like this: \medskip + + x.neg // evaluates to -x + +2. Add a method `sub` to subtract two rational numbers. + +3. With the values of `x`, `y`, `z` as given in the previous slide, what is the result of +\medskip + + x - y - z + +\ \ \ ? diff --git a/lectures/progfun1-2-6.md b/lectures/progfun1-2-6.md new file mode 100644 index 0000000000000000000000000000000000000000..42c6cd08d6ea96126cc793328ca72d733ec621fd --- /dev/null +++ b/lectures/progfun1-2-6.md @@ -0,0 +1,189 @@ +% More Fun with Rationals +% \ +% \ + +Data Abstraction +================ + +The previous example has shown that rational numbers aren't always +represented in their simplest form. (Why?) + +One would expect the rational numbers to be _simplified_: + +- reduce them to their smallest numerator and denominator by dividing both with a divisor. + +We could implement this in each rational operation, but it would be easy +to forget this division in an operation. + +A better alternative consists of simplifying the representation in +the class when the objects are constructed: + +Rationals with Data Abstraction +=============================== + + class Rational(x: Int, y: Int) { + private def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b) + private val g = gcd(x, y) + def numer = x / g + def denom = y / g + ... + } + +`gcd` and `g` are \red{private} members; we can only access them +from inside the `Rational` class. + +In this example, we calculate `gcd` immediately, so that its value can be re-used +in the calculations of `numer` and `denom`. + +Rationals with Data Abstraction (2) +=================================== + +It is also possible to call `gcd` in the code of +`numer` and `denom`: + + class Rational(x: Int, y: Int) { + private def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b) + def numer = x / gcd(x, y) + def denom = y / gcd(x, y) + } + +This can be advantageous if it is expected that the functions `numer` +and `denom` are called infrequently. + +Rationals with Data Abstraction (3) +=================================== + +It is equally possible to turn `numer` and `denom` into `val`s, so that they are computed only once: + + class Rational(x: Int, y: Int) { + private def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b) + val numer = x / gcd(x, y) + val denom = y / gcd(x, y) + } + +This can be advantageous if the functions `numer` and `denom` are called often. + +The Client's View +================= + +Clients observe exactly the same behavior in each case. + +This ability to choose different implementations of the data without affecting +clients is called \red{data abstraction}. + +It is a cornerstone of software engineering. + +Self Reference +============== + +On the inside of a class, the name `this` represents the object +on which the current method is executed. + +\example + +Add the functions `less` and `max` to the class `Rational`. + + class Rational(x: Int, y: Int) { + ... + def less(that: Rational) = + numer * that.denom < that.numer * denom + + def max(that: Rational) = + if this.less(that) then that else this + } + +Self Reference (2) +================== + +Note that a simple name `x`, which refers to another member +of the class, is an abbreviation of `this.x`. Thus, an equivalent +way to formulate `less` is as follows. + + def less(that: Rational) = + this.numer * that.denom < that.numer * this.denom + +Preconditions +============= + +Let's say our `Rational` class requires that the denominator is positive. + +We can enforce this by calling the `require` function. + + class Rational(x: Int, y: Int) { + require(y > 0, "denominator must be positive") + ... + } + +`require` is a predefined function. + +It takes a condition and an optional message string. + +If the condition passed to `require` is `false`, an `IllegalArgumentException` is thrown with the given message string. + +Assertions +========== + +Besides `require`, there is also `assert`. + +Assert also takes a condition and an optional message string as parameters. E.g. + + val x = sqrt(y) + assert(x >= 0) + +Like `require`, a failing `assert` will also throw an exception, but it's a different one: `AssertionError` for `assert`, `IllegalArgumentException` for `require`. + +This reflects a difference in intent + + - `require` is used to enforce a precondition on the caller of a function. + - `assert` is used as to check the code of the function itself. + +Constructors +============ + +In Scala, a class implicitly introduces a constructor. This one +is called the \red{primary constructor} of the class. + +The primary constructor + + - takes the parameters of the class + - and executes all statements in the class body + (such as the `require` a couple of slides back). + +Auxiliary Constructors +====================== + +Scala also allows the declaration of \red{auxiliary constructors}. + +These are methods named `this` + +\example Adding an auxiliary constructor to the class `Rational`. + + class Rational(x: Int, y: Int) { + def this(x: Int) = this(x, 1) + ... + } + +\begin{worksheet} + new Rational(2) \wsf 2/1 +\end{worksheet} + + +Exercise +======== + +Modify the `Rational` class so that rational numbers are kept unsimplified internally, +but the simplification is applied when numbers are converted to strings. + +Do clients observe the same behavior when interacting with the rational class? \bigskip + +\begin{tabular}{lp{8cm}} + \verb@ O @ & yes +\\ \verb@ O @ & no +\\ \verb@ O @ & yes for small sizes of denominators and nominators and small numbers of operations. +\end{tabular} + + + + + + diff --git a/lectures/progfun1-2-7.md b/lectures/progfun1-2-7.md new file mode 100644 index 0000000000000000000000000000000000000000..60bffd35e14a40e75b99fc3dc42ce13b11df9a33 --- /dev/null +++ b/lectures/progfun1-2-7.md @@ -0,0 +1,185 @@ +% Evaluation and Operators +% +% + +Classes and Substitutions +========================= + +We previously defined the meaning of a function application using +a computation model based on substitution. Now we extend this +model to classes and objects. + +\red{Question:} How is an instantiation of the class +$\btt new\ C(e_1, ..., e_m)$ evaluted? + +\red{Answer:} The expression arguments $\btt e_1, ..., e_m$ +are evaluated like the arguments of a normal function. That's it. + +The resulting expression, say, $\btt new\ C(v_1, ..., v_m)$, is +already a value. + +Classes and Substitutions +========================= + +Now suppose that we have a class definition, + +$$\btt + class\ C(x_1, ..., x_m) \{\ ...\ def\ f(y_1, ..., y_n) = b\ ...\ \} +$$ +where + + - The formal parameters of the class are $\btt x_1, ..., x_m$. + - The class defines a method $\btt f$ with formal parameters + $\btt y_1, ..., y_n$. + +(The list of function parameters can be absent. For simplicity, we +have omitted the parameter types.) + +\red{Question:} How is the following expression evaluated? +$$\btt + new\ C(v_1, ..., v_m).f(w_1, ..., w_n) +$$ + +Classes and Substitutions (2) +============================= + +\red{Answer:} The expression $\btt new\ C(v_1, ..., v_m).f(w_1, ..., w_n)$ is rewritten to: +$$\btt +[w_1/y_1, ..., w_n/y_n] + [v_1/x_1, ..., v_m/x_m] + [new\ C(v_1, ..., v_m)/this]\,b +$$ +There are three substitutions at work here: + + - the substitution of the formal parameters $\btt y_1, ..., y_n$ of the function $\btt f$ by the +arguments $\btt w_1, ..., w_n$, + - the substitution of the formal parameters $\btt x_1, ..., x_m$ of the class $\btt C$ by the class arguments +$\btt v_1, ..., v_m$, + - the substitution of the self reference $this$ by the value of the object $\btt new\ C(v_1, ..., v_n)$. + +Object Rewriting Examples +========================= + + ` new Rational(1, 2).numer` +-> + $\rightarrow$ $\btt [1/x, 2/y]\ []\ [new\ Rational(1, 2)/this]$ `x` +-> + =$\,$ `1` +\medskip +-> + ` new Rational(1, 2).less(new Rational(2, 3))` +-> + $\rightarrow$ $\btt [1/x, 2/y]\ [new Rational(2, 3)/that]\ [new\ Rational(1, 2)/this]$ + +\nl\gap `this.numer * that.denom < that.numer * this.denom` +-> + =$\,$ `new Rational(1, 2).numer * new Rational(2, 3).denom <` + +\nl\gap `new Rational(2, 3).numer * new Rational(1, 2).denom` +-> + $\rightarrow\dhd$ `1 * 3 < 2 * 2` + + $\rightarrow\dhd$ `true` + + +Operators +========= + +In principle, the rational numbers defined by `Rational` are +as natural as integers. + +But for the user of these abstractions, there is a noticeable +difference: + + - We write `x + y`, if `x` and `y` are integers, but + - We write `r.add(s)` if `r` and `s` are rational numbers. + +In Scala, we can eliminate this difference. We proceed in two steps. + +Step 1: Infix Notation +====================== + +Any method with a parameter can be used like an infix operator. + +It is therefore possible to write + + r add s r.add(s) + r less s /* in place of */ r.less(s) + r max s r.max(s) + +Step 2: Relaxed Identifiers +=========================== + +Operators can be used as identifiers. + +Thus, an identifier can be: + + - \red{Alphanumeric}: starting with a letter, followed by a sequence of letters or numbers + - \red{Symbolic}: starting with an operator symbol, followed by other operator symbols. + - The underscore character `'_'` counts as a letter. + - Alphanumeric identifiers can also end in an underscore, followed by some operator symbols. + +Examples of identifiers: + + x1 * +?%& vector_++ counter_= + + + + + +Operators for Rationals +======================= + +A more natural definition of class `Rational`: + + class Rational(x: Int, y: Int) { + private def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b) + private val g = gcd(x, y) + def numer = x / g + def denom = y / g + def + (r: Rational) = + new Rational( + numer * r.denom + r.numer * denom, + denom * r.denom) + def - (r: Rational) = ... + def * (r: Rational) = ... + ... + } + +Operators for Rationals +======================= + +... and rational numbers can be used like `Int` or `Double`: + + val x = new Rational(1, 2) + val y = new Rational(1, 3) + x * x + y * y + +Precedence Rules +================ + +The \red{precedence} of an operator is determined by its first character. + +The following table lists the characters in increasing order of priority precedence: + + (all letters) + | + ^ + & + < > + = ! + : + + - + * / % + (all other special characters) + +Exercise +======== + +Provide a fully parenthized version of + + a + b ^? c ?^ d less a ==> b | c + +Every binary operation needs to be put into parentheses, +but the structure of the expression should not change. + diff --git a/lectures/progfun1-2-new/progfun1-2-1.md b/lectures/progfun1-2-new/progfun1-2-1.md new file mode 100644 index 0000000000000000000000000000000000000000..f1418b6e22640df2b7829903c84498ea3aa3b9c6 --- /dev/null +++ b/lectures/progfun1-2-new/progfun1-2-1.md @@ -0,0 +1,173 @@ +% Functions and Data +% +% + +Functions and Data +================== + +In this section, we'll learn how functions create and encapsulate data +structures. + +\example + +Rational Numbers + +We want to design a package for doing rational arithmetic. + +A rational number $\blue{\frac{x}{y}}$ is represented by two integers: + + - its _numerator_ $\blue{x}$, and + - its _denominator_ $\blue{y}$. + +Rational Addition +================= + +Suppose we want to implement the addition of two rational numbers. + + def addRationalNumerator(n1: Int, d1: Int, n2: Int, d2: Int): Int + def addRationalDenominator(n1: Int, d1: Int, n2: Int, d2: Int): Int + +but it would be difficult to manage all these numerators and denominators. + +A better choice is to combine the numerator and +denominator of a rational number in a data structure. + +Case Classes +============ + +In Scala, we do this by defining a \red{case class}: + +~~~ +case class Rational(numer: Int, denom: Int) +~~~ + +This definition introduces two entities: + + - A new \red{type}, named `Rational`. + - A \red{constructor} `Rational` to create elements of this type. + +Scala keeps the names of types and values in \red{different namespaces}. +So there's no conflict between the two definitions of `Rational`. + +Objects +======= + +We call the elements of a class type \red{objects}. + +We create an object by calling the constructor of the class. + +\example + + Rational(1, 2) + +Members of an Object +==================== + +Objects of the class `Rational` have two \red{members}, +`numer` and `denom`. + +We select the members of an object with the infix operator `.' (like in Java). + +\example + +\begin{tabular}{ll} + \verb@val x = Rational(1, 2)@ \wsf x: Rational = Rational@2abe0e27 +\\ + x.numer \wsf 1 +\\ + x.denom \wsf 2 +\end{tabular} + +Rational Arithmetic +=================== + +We can now define the arithmetic functions that implement the standard rules. + +$$ +\ba{l} +\frac{n_1}{d_1} + \frac{n_2}{d_2} \ = \ \frac{n_1 d_2 + n_2 d_1}{d_1 d_2} +\\[1em] +\frac{n_1}{d_1} - \frac{n_2}{d_2} \ = \ \frac{n_1 d_2 - n_2 d_1}{d_1 +d_2} +\\[1em] +\frac{n_1}{d_1} \cdot \frac{n_2}{d_2} \ = \ \frac{n_1 n_2}{d_1 d_2} +\\[1em] +\frac{n_1}{d_1} / \frac{n_2}{d_2} \ = \ \frac{n_1 d_2}{d_1 n_2} +\\[1em] +\frac{n_1}{d_1} = \frac{n_2}{d_2} \quad \small \mbox{iff} \quad n_1 d_2 = d_1 n_2 +\ea +$$ + +Implementing Rational Arithmetic +================================ + + def addRational(r: Rational, s: Rational): Rational = + Rational( + r.numer * s.denom + s.numer * r.denom, + r.denom * s.denom) + + def makeString(r: Rational) = + s"${r.numer}/${r.denom}" + +\begin{worksheet} + \verb@makeString(addRational(Rational(1, 2), Rational(2, 3)))@ \wsf 7/6 +\end{worksheet} + + +Methods +======= + +One can go further and also package functions operating on a data +abstraction in the data abstraction itself. + +Such functions are called \red{methods}. + +\example + +Rational numbers now would have, in addition to the functions `numer` +and `denom`, the functions `add`, `sub`, +`mul`, `div`, `equal`, `toString`. + +Methods for Rationals +===================== + +Here's a possible implementation: + + case class Rational(numer: Int, denom: Int) + def add(r: Rational) = + Rational(numer * r.denom + r.numer * denom, + denom * r.denom) + def mul(r: Rational) = ... + ... + override def toString = s"$numer/$denom" + end Rational + +\red{Remark}: the modifier `override` declares that `toString` +redefines a method that already exists (in the class `java.lang.Object`). + +Calling Methods +=============== + + +Here is how one might use the new `Rational` abstraction: + + val x = Rational(1, 3) + val y = Rational(5, 7) + val z = Rational(3, 2) + x.add(y).mul(z) + +Exercise +======== + +1. In your worksheet, add a method `neg` to class Rational that is used like this: \medskip + + x.neg // evaluates to -x + +2. Add a method `sub` to subtract two rational numbers. + +3. With the values of `x`, `y`, `z` as given in the previous slide, what is the result of +\medskip + + x - y - z + +\ \ \ ? diff --git a/lectures/progfun1-2-new/progfun1-2-2.md b/lectures/progfun1-2-new/progfun1-2-2.md new file mode 100644 index 0000000000000000000000000000000000000000..2a94488ec178839824beb317508b7bb62bf9428e --- /dev/null +++ b/lectures/progfun1-2-new/progfun1-2-2.md @@ -0,0 +1,328 @@ +% Enums +% +% + +Enums: Motivation +================= + +We have seen that case classes *aggregate several values* into a single abstraction. +For instance, the `Rational` case class aggregates a numerator and a denominator. + +Conversely, how could we define an abstraction *accepting alternative values*? + +\example + +Define a `Paradigm` type that can be either `Functional` or `Imperative`. + +Enums +===== + +We can define an abstraction accepting alternative values with an \red{enum}: + +~~~ +enum Paradigm + case Functional, Imperative +~~~ + +This definition introduces: + +- A new \red{type}, named `Paradigm`. +- Two possible \red{values} for this type, `Paradigm.Functional` and + `Paradigm.Imperative`. + +Enumerate the Values of an Enumeration +====================================== + +It is possible to enumerate all the values of an enum by calling the +`values` operation on the enum companion object: + +\begin{tabular}{ll} + \verb@Paradigm.values@ \wsf Array(Functional, Imperative) \\ + \verb@val p = Paradigm.Functional@ \wsf p: Paradigm = Functional \\ + \verb@p == Paradigm.values(0)@ \wsf true +\end{tabular} + +Discriminate the Values of an Enumeration +========================================= + +You can discriminate between the values of an enum by using a \red{match} +expression: + +~~~ +def isReferentiallyTransparent(paradigm: Paradigm): Boolean = + paradigm match + case Paradigm.Functional => true + case Paradigm.Imperative => false +~~~ + +-> + +And then: + +\begin{tabular}{ll} + \verb@val p = Paradigm.Functional@ \wsf p: Paradigm = Functional \\ + \verb@isReferentiallyTransparent(p)@ \wsf true +\end{tabular} + +Match Syntax +============ + +_Pattern matching_ is a generalization of `switch` from C/Java +to class hierarchies. + +It’s expressed in Scala using the `match` keyword. + +- `match` is followed by a sequence of \red{cases}, `case value => expr`. +- Each case associates an \red{expression} `expr` with a + \red{constant} `value`. + +We will see in the next slides that pattern matching can do more +than discriminating enums. + +Enumerations Can Have Methods +============================= + +Alternatively, we can define `isReferentiallyTransparent` as a method: + +~~~ +enum Paradigm + case Functional, Imperative + + def isReferentiallyTransparent: Boolean = this match + case Functional => true + case Imperative => false +~~~ + +-> + +And then: + +\begin{tabular}{ll} + \verb@val p = Paradigm.Functional@ \wsf p: Paradigm = Functional \\ + \verb@p.isReferentiallyTransparent@ \wsf true +\end{tabular} + +Enumerations Values Can Take Parameters +======================================= + +Consider the following definition of a data type modeling students +following a lecture: + +~~~ +enum Student + case Listening + case Asking(question: String) +~~~ + +This definition introduces a `Student` type consisting of two cases, +`Listening` or `Asking`. The `Asking` case is parameterized with a +value parameter `question`. Since `Listening` is not parameterized, +it is treated as a normal enum value. + +Matching on Constructors +======================== + +Since `Asking` is not a constant, we match on it using +a _constructor pattern_: + +~~~ +student match + case Student.Listening => "Student is listening" + case Student.Asking(q) => s"Student is asking: $q" +~~~ + +A constructor pattern allows us to _extract_ the value of +the `question` parameter, in case the `student` value is +indeed of type `Asking`. + +Here, the `q` identifier is bound to the `question` parameter +of the `student` object. + +Evaluating Match Expressions +============================ + +An expression of the form +$$\btt +e\ match\ \{\ case\ p_1 => e_1\ ...\ case\ p_n => e_n\ \} +$$ +matches the value of the selector $\btt e$ with the patterns +$\btt p_1, ..., p_n$ in the order in which they are written. + +The whole match expression is rewritten to the right-hand side of the first +case where the pattern matches the selector $e$. + +References to pattern variables are replaced by the corresponding +parts in the selector. + +Forms of Patterns +================= + +Patterns are constructed from: + + - _constructors_, e.g. `Rational`, `Student.Asking`, + - _variables_, e.g. `n`, `e1`, `e2`, + - _wildcard patterns_ `_`, + - _constants_, e.g. `1`, `true`, `Paradigm.Functional`. + +Variables always begin with a _lowercase letter_. + +The same variable name can only appear _once_ in a pattern. + +Names of constants begin with a capital letter, +with the exception of the reserved words `null`, `true`, +`false`. + +What Do Patterns Match? +======================= + +- A constructor pattern $\btt C(p_1 , ..., p_n)$ matches + all the values of type $\btt C$ (or a subtype) that have been + constructed with arguments matching the patterns $\btt p_1, ..., p_n$. +- A variable pattern $\btt x$ matches any value, and + \red{binds} the name of the variable to this value. +- A constant pattern $\btt c$ matches values that are equal to + $\btt c$ (in the sense of `==`) + +Matching on Case Classes +======================== + +Constructor patterns also works with case classes: + +~~~ +def invert(x: Rational): Rational = + x match + case Rational(0, _) => sys.error("Unable to invert zero") + case Rational(n, d) => Rational(d, n) +~~~ + +Relationship Between Case Classes and Parameterized Enum Cases +============================================================== + +Case classes and parameterized enum cases are very similar. + +When should you use one or the other? + +- Parameterized enum cases should be used to define a type + that is part of a set of alternatives, +- If there are no alternatives, just use a case class. + +Enumerations Can Be Recursive +============================= + +A list of integer values can be modeled as either an empty list, +or a node containing both a number and a reference to the remainder +of the list. + +~~~ +enum List + case Empty + case Node(value: Int, next: List) +~~~ + +A list with values `1`, `2`, and `3` can be constructed as follows: + +~~~ +List.Node(1, List.Node(2, List.Node(3, List.Empty))) +~~~ + +Enumerations Can Take Parameters +================================ + +~~~ +enum Vehicle(val numberOfWheels: Int) + case Unicycle extends Vehicle(1) + case Bicycle extends Vehicle(2) + case Car extends Vehicle(4) +~~~ + +- Enumeration cases have to use an explicit `extends` clause + +Exercise: Arithmetic Expressions +================================ + +Define a datatype modeling arithmetic expressions. + +An expression can be: + +- a number (e.g. `42`), +- a sum of two expressions, +- or, the product of two expressions. + +`Expr` Type Definition +====================== + +~~~ +enum Expr + case Number(n: Int) + case Sum(lhs: Expr, rhs: Expr) + case Prod(lhs: Expr, rhs: Expr) + +-> + +\begin{tabular}{ll} + \verb@val one = Expr.Number(1)@ \wsf one: Expr = Number(1) \\ + \verb@val two = Expr.Number(2)@ \wsf two: Expr = Number(2) \\ + \verb@Expr.Sum(one, two)@ \wsf Sum(Number(1), Number(2)) +\end{tabular} + + +Exercise +======== + +Is the following match expression valid? + +~~~ +expr match + case Expr.Sum(x, x) => Expr.Prod(2, x) + case e => e +~~~ + + O Yes + O No + +Exercise +======== + +Implement an `eval` operation that takes an `Expr` as parameter and +evaluates its value: + +~~~ +def eval(e: Expr): Int +~~~ + +Examples of use: + +\begin{tabular}{ll} + \verb@eval(Expr.Number(42))@ \wsf 42 \\ + \verb@eval(Expr.Prod(2, Expr.Sum(Expr.Number(8), Expr.Number(13))))@ \wsf 42 +\end{tabular} + +Exercise +======== + +Implement a `show` operation that takes an `Expr` as parameter and +returns a `String` representation of the operation. + +Be careful to introduce parenthesis when necessary! + +~~~ +def show(e: Expr): String +~~~ + +Examples of use: + +\begin{tabular}{ll} + \verb@show(Expr.Number(42))@ \wsf "42" \\ + \verb@show(Expr.Prod(2, Expr.Sum(Expr.Number(8), Expr.Number(13))))@ \wsf "2 * (8 + 13)" +\end{tabular} + +Summary +======= + +In this lecture, we have seen: + +- how to define data types accepting alternative values using + `enum` definitions, +- enumeration cases can be discriminated using pattern matching, +- enumeration cases can take parameters or be simple values, +- pattern matching allows us to extract information carried by an object. diff --git a/lectures/progfun1-2-new/progfun1-2-3.md b/lectures/progfun1-2-new/progfun1-2-3.md new file mode 100644 index 0000000000000000000000000000000000000000..348950bd3af6c4e92e0150db6d8ca56d950a71f3 --- /dev/null +++ b/lectures/progfun1-2-new/progfun1-2-3.md @@ -0,0 +1,115 @@ +% Business Domain Modeling +% +% + +Business Domain Modeling +======================== + +Enums and case classes together provide basic building blocks +for defining data types. + +They are typically used to **model the business domain** of a program. + +How to Model Things? +==================== + +Modeling consists of translating concepts from the real world +into data type definitions in the Scala world. + +Modeling requires you to think about what details of the real +world are relevant for your program. + +> The purpose of abstraction is not to be vague, but to create +> a new semantic level in which one can be absolutely precise. +> +> Edsger W. Dijkstra + + +Modeling Methodology (1) +======================== + +There is no systematic methodology: often, a same set of concepts +can be modeled in multiple ways. + +But here are some advice. + +-> + +- Identify the concepts (in general, nouns) that you are interested in +- Identify the relations between them + - Does a concept _belong to_ another one? + - e.g. “a rational number _has_ a numerator and a denominator†+ - e.g. “a sum _has_ a left summand and a right summand†+ - Does a concept _generalize_ another one? + - e.g. “‘functional programming’ _is_ a possible programming paradigm†+ - e.g. “an arithmetic operation can either _be_ a product or a sum†+ +Modeling Methodology (2) +======================== + +- Translate each concept into a type definition + - Concepts belonging to others become **fields** of case classes + or of enumeration cases + - Concepts generalizing others become **enumerations** +- Check that you can construct meaningful objects from your model + +Example: Painting Application +============================= + +An image is made of a shape, a stroke width and a color. + +Possible shapes are a square or a circle. A square has a side length. +A circle has a radius. + +A color has a blue component, a red component and a green component. +Each component of a color is an integer value (between 0 and 255). + +-> + +~~~ +case class Image(shape: Shape, strokeWidth: Int, color: Color) + +enum Shape { + case Square(side: Double) + case Circle(radius: Double) +} + +case class Color(red: Int, green: Int, blue: Int) +~~~ + +Example: Painting Application +============================= + +~~~ +case class Image(shape: Shape, strokeWidth: Int, color: Color) + +enum Shape + case Square(side: Double) + case Circle(radius: Double) + +case class Color(red: Int, green: Int, blue: Int) +~~~ + +A blue circle: + +~~~ +val blue = Color(0, 0, 255) +val blueCircle = Image(Circle(10), 1, blue) +~~~ + +Data Types Define a Space of Possible States +============================================ + +The domain model defines the rules for constructing +objects representing a possible state of a program. + +The more possible states, the more risks to have bugs +or to reach illegal states. + +Hint: restrict the space of possible states to the minimum. + +Summary +======= + +In this lecture we have seen how to model the business domain +of a program using enumerations and case classes. diff --git a/lectures/progfun1-3-1.md b/lectures/progfun1-3-1.md new file mode 100644 index 0000000000000000000000000000000000000000..09383cbf32a963dec7ee24debf1e1024f33947f8 --- /dev/null +++ b/lectures/progfun1-3-1.md @@ -0,0 +1,199 @@ +% More Fun with Rationals +% +% + +More Examples With Rationals +============================ + +\begin{tabular}{ll} + \verb@val half = Rational(1, 2)@ \wsf half: Rational = Rational(1, 2) \\ + \verb@half.add(half)@ \wsf Rational(3, 3) +\end{tabular} + +Data Abstraction +================ + +The previous example has shown that rational numbers aren't always +represented in their simplest form. (Why?) + +One would expect the rational numbers to be _simplified_: + +- reduce them to their smallest numerator and denominator by dividing both with a divisor. + +We could implement this in each rational operation, but it would be easy +to forget this division in an operation. + +A better alternative consists of simplifying the representation in +the class when the objects are constructed: + +Rationals with Data Abstraction +=============================== + + class Rational(x: Int, y: Int) + private def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b) + private val g = gcd(x, y) + def numer = x / g + def denom = y / g + ... + +We turned the `case class` definition into a simple `class` definition. + +A class definition also introduces a type (named `Rational`) and +a constructor, but there are some differences with case classes: + +- the `x` and `y` constructor parameters are not available as + object fields + +`gcd` and `g` are \red{private} members; we can only access them +from inside the `Rational` class. + +In this example, we calculate `gcd` immediately, so that its value can be re-used +in the calculations of `numer` and `denom`. + +Rationals with Data Abstraction (2) +=================================== + +It is also possible to call `gcd` in the code of +`numer` and `denom`: + + class Rational(x: Int, y: Int) + private def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b) + def numer = x / gcd(x, y) + def denom = y / gcd(x, y) + +This can be advantageous if it is expected that the functions `numer` +and `denom` are called infrequently. + +Rationals with Data Abstraction (3) +=================================== + +It is equally possible to turn `numer` and `denom` into `val`s, so that they are computed only once: + + class Rational(x: Int, y: Int) + private def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b) + val numer = x / gcd(x, y) + val denom = y / gcd(x, y) + +This can be advantageous if the functions `numer` and `denom` are called often. + +The Client's View +================= + +Clients observe exactly the same behavior in each case. + +This ability to choose different implementations of the data without affecting +clients is called \red{data abstraction}. + +It is a cornerstone of software engineering. + +Self Reference +============== + +On the inside of a class, the name `this` represents the object +on which the current method is executed. + +\example + +Add the functions `less` and `max` to the class `Rational`. + + class Rational(x: Int, y: Int) + ... + def less(that: Rational) = + numer * that.denom < that.numer * denom + + def max(that: Rational) = + if this.less(that) that else this + +Self Reference (2) +================== + +Note that a simple name `x`, which refers to another member +of the class, is an abbreviation of `this.x`. Thus, an equivalent +way to formulate `less` is as follows. + + def less(that: Rational) = + this.numer * that.denom < that.numer * this.denom + +Preconditions +============= + +Let's say our `Rational` class requires that the denominator is positive. + +We can enforce this by calling the `require` function. + + class Rational(x: Int, y: Int) + require(y > 0, "denominator must be positive") + ... + +`require` is a predefined function. + +It takes a condition and an optional message string. + +If the condition passed to `require` is `false`, an `IllegalArgumentException` is thrown with the given message string. + +Assertions +========== + +Besides `require`, there is also `assert`. + +Assert also takes a condition and an optional message string as parameters. E.g. + + val x = sqrt(y) + assert(x >= 0) + +Like `require`, a failing `assert` will also throw an exception, but it's a different one: `AssertionError` for `assert`, `IllegalArgumentException` for `require`. + +This reflects a difference in intent + + - `require` is used to enforce a precondition on the caller of a function. + - `assert` is used as to check the code of the function itself. + +Constructors +============ + +In Scala, a class implicitly introduces a constructor. This one +is called the \red{primary constructor} of the class. + +The primary constructor + + - takes the parameters of the class + - and executes all statements in the class body + (such as the `require` a couple of slides back). + +Auxiliary Constructors +====================== + +Scala also allows the declaration of \red{auxiliary constructors}. + +These are methods named `this` + +\example Adding an auxiliary constructor to the class `Rational`. + + class Rational(x: Int, y: Int) + def this(x: Int) = this(x, 1) + ... + +\begin{worksheet} + Rational(2) \wsf 2/1 +\end{worksheet} + + +Exercise +======== + +Modify the `Rational` class so that rational numbers are kept unsimplified internally, +but the simplification is applied when numbers are converted to strings. + +Do clients observe the same behavior when interacting with the rational class? \bigskip + +\begin{tabular}{lp{8cm}} + \verb@ O @ & yes +\\ \verb@ O @ & no +\\ \verb@ O @ & yes for small sizes of denominators and nominators and small numbers of operations. +\end{tabular} + + + + + + diff --git a/lectures/progfun1-3-2.md b/lectures/progfun1-3-2.md new file mode 100644 index 0000000000000000000000000000000000000000..052bad82a37359f3416a5418930642707b5145a1 --- /dev/null +++ b/lectures/progfun1-3-2.md @@ -0,0 +1,185 @@ +% Evaluation and Operators +% +% + +Classes and Substitutions +========================= + +We previously defined the meaning of a function application using +a computation model based on substitution. Now we extend this +model to classes and objects. + +\red{Question:} How is an instantiation of the class +$\btt new\ C(e_1, ..., e_m)$ evaluted? + +\red{Answer:} The expression arguments $\btt e_1, ..., e_m$ +are evaluated like the arguments of a normal function. That's it. + +The resulting expression, say, $\btt new\ C(v_1, ..., v_m)$, is +already a value. + +Classes and Substitutions +========================= + +Now suppose that we have a class definition, + +$$\btt + class\ C(x_1, ..., x_m) \{\ ...\ def\ f(y_1, ..., y_n) = b\ ...\ \} +$$ +where + + - The formal parameters of the class are $\btt x_1, ..., x_m$. + - The class defines a method $\btt f$ with formal parameters + $\btt y_1, ..., y_n$. + +(The list of function parameters can be absent. For simplicity, we +have omitted the parameter types.) + +\red{Question:} How is the following expression evaluated? +$$\btt + new\ C(v_1, ..., v_m).f(w_1, ..., w_n) +$$ + +Classes and Substitutions (2) +============================= + +\red{Answer:} The expression $\btt new\ C(v_1, ..., v_m).f(w_1, ..., w_n)$ is rewritten to: +$$\btt +[w_1/y_1, ..., w_n/y_n] + [v_1/x_1, ..., v_m/x_m] + [C(v_1, ..., v_m)/this]\,b +$$ +There are three substitutions at work here: + + - the substitution of the formal parameters $\btt y_1, ..., y_n$ of the function $\btt f$ by the +arguments $\btt w_1, ..., w_n$, + - the substitution of the formal parameters $\btt x_1, ..., x_m$ of the class $\btt C$ by the class arguments +$\btt v_1, ..., v_m$, + - the substitution of the self reference $this$ by the value of the object $\btt C(v_1, ..., v_n)$. + +Object Rewriting Examples +========================= + + ` Rational(1, 2).numer` +-> + $\rightarrow$ $\btt [1/x, 2/y]\ []\ [Rational(1, 2)/this]$ `x` +-> + =$\,$ `1` +\medskip +-> + ` Rational(1, 2).less(Rational(2, 3))` +-> + $\rightarrow$ $\btt [1/x, 2/y]\ [Rational(2, 3)/that]\ [new\ Rational(1, 2)/this]$ + +\nl\gap `this.numer * that.denom < that.numer * this.denom` +-> + =$\,$ `Rational(1, 2).numer * Rational(2, 3).denom <` + +\nl\gap `Rational(2, 3).numer * Rational(1, 2).denom` +-> + $\rightarrow\dhd$ `1 * 3 < 2 * 2` + + $\rightarrow\dhd$ `true` + + +Operators +========= + +In principle, the rational numbers defined by `Rational` are +as natural as integers. + +But for the user of these abstractions, there is a noticeable +difference: + + - We write `x + y`, if `x` and `y` are integers, but + - We write `r.add(s)` if `r` and `s` are rational numbers. + +In Scala, we can eliminate this difference. We proceed in two steps. + +Step 1: Infix Notation +====================== + +Any method with a parameter can be used like an infix operator. + +It is therefore possible to write + + r add s r.add(s) + r less s /* in place of */ r.less(s) + r max s r.max(s) + +Step 2: Relaxed Identifiers +=========================== + +Operators can be used as identifiers. + +Thus, an identifier can be: + + - \red{Alphanumeric}: starting with a letter, followed by a sequence of letters or numbers + - \red{Symbolic}: starting with an operator symbol, followed by other operator symbols. + - The underscore character `'_'` counts as a letter. + - Alphanumeric identifiers can also end in an underscore, followed by some operator symbols. + +Examples of identifiers: + + x1 * +?%& vector_++ counter_= + + + + + +Operators for Rationals +======================= + +A more natural definition of class `Rational`: + + class Rational(x: Int, y: Int) { + private def gcd(a: Int, b: Int): Int = if b == 0 then a else gcd(b, a % b) + private val g = gcd(x, y) + def numer = x / g + def denom = y / g + def + (r: Rational) = + Rational( + numer * r.denom + r.numer * denom, + denom * r.denom) + def - (r: Rational) = ... + def * (r: Rational) = ... + ... + } + +Operators for Rationals +======================= + +... and rational numbers can be used like `Int` or `Double`: + + val x = Rational(1, 2) + val y = Rational(1, 3) + x * x + y * y + +Precedence Rules +================ + +The \red{precedence} of an operator is determined by its first character. + +The following table lists the characters in increasing order of priority precedence: + + (all letters) + | + ^ + & + < > + = ! + : + + - + * / % + (all other special characters) + +Exercise +======== + +Provide a fully parenthized version of + + a + b ^? c ?^ d less a ==> b | c + +Every binary operation needs to be put into parentheses, +but the structure of the expression should not change. + diff --git a/lectures/progfun1-3-3.md b/lectures/progfun1-3-3.md new file mode 100644 index 0000000000000000000000000000000000000000..6ff97d64f298cc21b5666bae9e01c9586dfdbd21 --- /dev/null +++ b/lectures/progfun1-3-3.md @@ -0,0 +1,192 @@ +% Class Hierarchies +% +% + + +Abstract Classes +================ + +Consider the task of writing a class for sets of integers with +the following operations. + + abstract class IntSet + def incl(x: Int): IntSet + def contains(x: Int): Boolean + +`IntSet` is an \red{abstract class}. + +Abstract classes can contain members which are +missing an implementation (in our case, `incl` and `contains`). + +Consequently, no instances of an abstract class can be created with +the operator \verb$new$. + +Class Extensions +================ + +Let's consider implementing sets as binary trees. + +There are two types of possible trees: a tree for the empty set, and +a tree consisting of an integer and two sub-trees. + +Here are their implementations: + + class Empty() extends IntSet + def contains(x: Int): Boolean = false + def incl(x: Int): IntSet = NonEmpty(x, Empty(), Empty()) + +Class Extensions (2) +==================== + + class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet + + def contains(x: Int): Boolean = + if x < elem then left.contains(x) + else if x > elem then right.contains(x) + else true + + def incl(x: Int): IntSet = + if x < elem then NonEmpty(elem, left.incl(x), right) + else if x > elem then NonEmpty(elem, left, right.incl(x)) + else this + +Terminology +=========== + +`Empty` and `NonEmpty` both \red{extend} the class `IntSet`. + +This implies that the types `Empty` and `NonEmpty` \red{conform} to the type `IntSet` + +- an object of type `Empty` or `NonEmpty` can be used wherever an object of type `IntSet` is required. + +Base Classes and Subclasses +=========================== + +`IntSet` is called the \red{superclass} of `Empty` +and `NonEmpty`. + +`Empty` and `NonEmpty` are \red{subclasses} of +`IntSet`. + +In Scala, any user-defined class extends another class. + +If no superclass is given, the standard class `Object` in the Java package `java.lang` is assumed. + +The direct or indirect superclasses of a class `C` are called \red{base classes} of `C`. + +So, the base classes of `NonEmpty` are `IntSet` and `Object`. + + +Implementation and Overriding +============================= + +The definitions of `contains` and `incl` in the classes +`Empty` and `NonEmpty` \red{implement} the abstract +functions in the base trait `IntSet`. + +It is also possible to \red{redefine} an existing, non-abstract +definition in a subclass by using `override`. + +\example + + abstract class Base class Sub extends Base + def foo = 1 override def foo = 2 + def bar: Int def bar = 3 + +Object Definitions +================== + +In the `IntSet` example, one could argue that there is really only a single empty `IntSet`. + +So it seems overkill to have the user create many instances of it. + +We can express this case better with an _object definition_: + + object Empty extends IntSet + def contains(x: Int): Boolean = false + def incl(x: Int): IntSet = NonEmpty(x, Empty, Empty) + +This defines a \red{singleton object} named `Empty`. + +No other `Empty` instances can be (or need to be) created. + +Singleton objects are values, so `Empty` evaluates to itself. + +Programs +======== + +So far we have executed all Scala code from the REPL or the worksheet. + +But it is also possible to create standalone applications in Scala. + +Each such application contains an object with a `main` method. + +For instance, here is the "Hello World!" program in Scala. + + object Hello + def main(args: Array[String]) = println("hello world!") + +Once this program is compiled, you can start it from the command line with + + > scala Hello + +Exercise +======== + +Write a method `union` for forming the union of two sets. You should +implement the following abstract class. + + abstract class IntSet + def incl(x: Int): IntSet + def contains(x: Int): Boolean + def union(other: IntSet): IntSet + +Dynamic Binding +=============== + +Object-oriented languages (including Scala) implement \red{dynamic method dispatch}. + +This means that the code invoked by a method call depends on the +runtime type of the object that contains the method. + +\example + + `Empty.contains(1)` +-> + $\rightarrow$ $\btt [1/x]\ [Empty/this]$ `false` +-> + =$\,$ `false` + +Dynamic Binding (2) +=================== + +Another evaluation using `NonEmpty`: + + `(NonEmpty(7, Empty, Empty)).contains(7)` +-> + $\rightarrow$ $\btt [7/elem]\ [7/x]\ [new\ NonEmpty(7, Empty, Empty)/this]$ + +\nl \quad$\ $ `if x < elem then this.left.contains(x)` + +\nl\gap$\ $ `else if x > elem then this.right.contains(x) else true` +-> + =$\,$ `if 7 < 7 then NonEmpty(7, Empty, Empty).left.contains(7)` + +\nl\gap `else if 7 > 7 then NonEmpty(7, Empty, Empty).right` + +\nl\gap\gap `.contains(7) else true` +-> + $\rightarrow$ `true` + +Something to Ponder +=================== + +Dynamic dispatch of methods is analogous to calls to +higher-order functions. + +\red{Question:} + +Can we implement one concept in terms of the other? + + - Objects in terms of higher-order functions? + - Higher-order functions in terms of objects? \ No newline at end of file diff --git a/lectures/progfun1-3-4.md b/lectures/progfun1-3-4.md new file mode 100644 index 0000000000000000000000000000000000000000..d90c46acc81c9800bdc5c77c4a6deadf81b40ac8 --- /dev/null +++ b/lectures/progfun1-3-4.md @@ -0,0 +1,193 @@ +% How Classes are Organized +% +% + +Packages +======== + +Classes and objects are organized in packages. + +To place a class or object inside a package, use a package clause +at the top of your source file. + + package progfun.examples + + object Hello { ... } + +This would place `Hello` in the package `progfun.examples`. + +You can then refer to `Hello` by its _fully qualified name_ +`progfun.examples.Hello`. For instance, to run the `Hello` program: + + > scala progfun.examples.Hello + +Imports +======= + +Say we have a class `Rational` in package `week3`. + +You can use the class using its fully qualified name: + + val r = new week3.Rational(1, 2) + +Alternatively, you can use an import: + + import week3.Rational + val r = new Rational(1, 2) + +Forms of Imports +================ + +Imports come in several forms: + + import week3.Rational // imports just Rational + import week3.{Rational, Hello} // imports both Rational and Hello + import week3._ // imports everything in package week3 + +The first two forms are called _named imports_. + +The last form is called a _wildcard import_. + +You can import from either a package or an object. + +Automatic Imports +================= + +Some entities are automatically imported in any Scala program. + +These are: + + - All members of package `scala` + - All members of package `java.lang` + - All members of the singleton object `scala.Predef`. + +Here are the fully qualified names of some types and functions which you have seen so far: + + Int scala.Int + Boolean scala.Boolean + Object java.lang.Object + require scala.Predef.require + assert scala.Predef.assert + +Scaladoc +======== + +You can explore the standard Scala library using the scaladoc web pages. + +You can start at + + [\blue{www.scala-lang.org/api/current}](http://www.scala-lang.org/api/current) + +Traits +====== + +In Java, as well as in Scala, a class can only have one superclass. + +But what if a class has several natural supertypes to which it conforms +or from which it wants to inherit code? + +Here, you could use `trait`s. + +A trait is declared like an abstract class, just with `trait` instead of +`abstract class`. + + trait Planar { + def height: Int + def width: Int + def surface = height * width + } + +Traits (2) +========== + +Classes, objects and traits can inherit from at most one class but +arbitrary many traits. + +Example: + + class Square extends Shape with Planar with Movable ... + +Traits resemble interfaces in Java, but are more powerful because they can contains fields and concrete methods. + +On the other hand, traits cannot have (value) parameters, only classes can. + + +Scala's Class Hierarchy +======================= + +\includegraphics[scale=0.33]{images/classhierarchy.pdf} + +Top Types +========= + +At the top of the type hierarchy we find: + +\begin{tabular}{ll} +\verb@Any @ & the base type of all types +\\[1em] + & Methods: `==`, `!=`, `equals`, `hashCode, `toString` +\\[2em] +\verb@AnyRef@ & The base type of all reference types; +\\ & Alias of `java.lang.Object` +\\[2em] +\verb@AnyVal@ & The base type of all primitive types. +\end{tabular} + +The Nothing Type +================ + +`Nothing` is at the bottom of Scala's type hierarchy. It is a subtype of every other type. + +There is no value of type `Nothing`. + +Why is that useful? + + - To signal abnormal termination + - As an element type of empty collections (see next session) + +Exceptions +========== + +Scala's exception handling is similar to Java's. + +The expression + + throw Exc + +aborts evaluation with the exception `Exc`. + +The type of this expression is `Nothing`. + +The Null Type +============= + +Every reference class type also has `null` as a value. + +The type of `null` is `Null`. + +`Null` is a subtype of every class that inherits from `Object`; it is +incompatible with subtypes of `AnyVal`. + + val x = null // x: Null + val y: String = null // y: String + val z: Int = null // error: type mismatch + + + +Exercise +======== + +What is the type of + + if true then 1 else false + + O Int + O Boolean + O AnyVal + O Object + O Any + + + + + diff --git a/lectures/progfun1-3-5.md b/lectures/progfun1-3-5.md new file mode 100644 index 0000000000000000000000000000000000000000..88c6ccc12d311b59b4c8e3dfc1e2e4042b438526 --- /dev/null +++ b/lectures/progfun1-3-5.md @@ -0,0 +1,128 @@ +% Objects Everywhere +% +% + +Pure Object Orientation +======================= + +A pure object-oriented language is one in which every value is an +object. + +If the language is based on classes, this means that the type of +each value is a class. + +Is Scala a pure object-oriented language? + +At first glance, there seem to be some exceptions: primitive types, +functions. + +But, let's look closer: + +Standard Classes +================ + +Conceptually, types such as `Int` or `Boolean` do not +receive special treatment in Scala. They are like the other classes, +defined in the package `scala`. + +For reasons of efficiency, the Scala compiler represents +the values of type `scala.Int` by 32-bit integers, and +the values of type `scala.Boolean` by Java's Booleans, etc. + +Pure Booleans +============= + +The `Boolean` type maps to the JVM's primitive type `boolean`. + +But one _could_ define it as a class from first principles: + + package idealized.scala + abstract class Boolean extends AnyVal + def ifThenElse[T](t: => T, e: => T): T + + def && (x: => Boolean): Boolean = ifThenElse(x, false) + def || (x: => Boolean): Boolean = ifThenElse(true, x) + def unary_!: Boolean = ifThenElse(false, true) + + def == (x: Boolean): Boolean = ifThenElse(x, x.unary_!) + def != (x: Boolean): Boolean = ifThenElse(x.unary_!, x) + ... + +Boolean Constants +================= + +Here are constants `true` and `false` that go with `Boolean` in `idealized.scala`: + + package idealized.scala + + object true extends Boolean + def ifThenElse[T](t: => T, e: => T) = t + + object false extends Boolean + def ifThenElse[T](t: => T, e: => T) = e + +Exercise +======== + +Provide an implementation of the comparison operator `<` +in class `idealized.scala.Boolean`. + +Assume for this that `false` < `true`. +-> + + +The class Int +============= + +Here is a partial specification of the class `scala.Int`. + + class Int + def + (that: Double): Double + def + (that: Float): Float + def + (that: Long): Long + def + (that: Int): Int // same for -, *, /, % + + def << (cnt: Int): Int // same for >>, >>> */ + + def & (that: Long): Long + def & (that: Int): Int // same for |, ^ */ + +The class Int (2) +================= + + + def == (that: Double): Boolean + def == (that: Float): Boolean + def == (that: Long): Boolean // same for !=, <, >, <=, >= + ... + +Can it be represented as a class from first principles (i.e. not using primitive `int`s? + +Exercise +======== + +Provide an implementation of the abstract class `Nat` that represents non-negative integers. + + + abstract class Nat + def isZero: Boolean + def predecessor: Nat + def successor: Nat + def + (that: Nat): Nat + def - (that: Nat): Nat + +Exercise (2) +============ + +Do not use standard numerical classes in this implementation. + +Rather, implement a sub-object and a sub-class: + + object Zero extends Nat + class Succ(n: Nat) extends Nat + +One for the number zero, the other for strictly positive numbers. + +(this one is a bit more involved than previous quizzes). + +\quiz \ No newline at end of file diff --git a/lectures/progfun1-4-1.md b/lectures/progfun1-4-1.md new file mode 100644 index 0000000000000000000000000000000000000000..7ab595d440263d27b6260a75394b588bb9276549 --- /dev/null +++ b/lectures/progfun1-4-1.md @@ -0,0 +1,165 @@ +% Polymorphism +% +% + +Cons-Lists +========== + +A fundamental data structure in many functional languages is +the immutable linked list. + +It is constructed from two building blocks: + +\begin{tabular}{ll} + \verb@Nil@ & the empty list \\ + \verb@Cons@ & a cell containing an element and the remainder of the list. +\end{tabular} + +Examples for Cons-Lists +======================= + + + + List(1, 2, 3) + + + + + + + + List(List(true, false), List(3)) + +Cons-Lists in Scala +=================== + +Here's an outline of a class hierarchy that represents lists of integers in this fashion: + + package week4 + + trait IntList ... + class Cons(val head: Int, val tail: IntList) extends IntList ... + class Nil() extends IntList ... + +A list is either + + - an empty list `Nil()`, or + - a list `Cons(x, xs)` consisting of a `head` element `x` + and a `tail` list `xs`. + + +Value Parameters +================ + +Note the abbreviation `(val head: Int, val tail: IntList)` in the definition of `Cons`. + +This defines at the same time parameters and fields of a class. + +It is equivalent to: + + class Cons(_head: Int, _tail: IntList) extends IntList + val head = _head + val tail = _tail + +where `_head` and `_tail` are otherwise unused names. + +Type Parameters +=============== + +It seems too narrow to define only lists with `Int` elements. + +We'd need another class hierarchy for `Double` lists, and so on, one for each possible element type. + +We can generalize the definition using a type parameter: + + package week4 + + trait List[T] + class Cons[T](val head: T, val tail: List[T]) extends List[T] + class Nil[T]() extends List[T] + +Type parameters are written in square brackets, e.g. `[T]`. + + +Complete Definition of List +=========================== + + trait List[T] + def isEmpty: Boolean + def head: T + def tail: List[T] + + class Cons[T](val head: T, val tail: List[T]) extends List[T] + def isEmpty = false + + class Nil[T] extends List[T] + def isEmpty = true + def head = throw NoSuchElementException("Nil.head") + def tail = throw NoSuchElementException("Nil.tail") + +Generic Functions +================= + +Like classes, functions can have type parameters. + +For instance, here is a function that creates a list consisting of a single element. + + def singleton[T](elem: T) = Cons[T](elem, Nil[T]()) + +We can then write: + + singleton[Int](1) + singleton[Boolean](true) + +Type Inference +============== + +In fact, the Scala compiler can usually deduce the correct type +parameters from the value arguments of a function call. + +So, in most cases, type parameters can be left out. You could also write: + + singleton(1) + singleton(true) + +Types and Evaluation +==================== + +Type parameters do not affect evaluation in Scala. + +We can assume that all type parameters and type arguments are removed +before evaluating the program. + +This is also called \red{type erasure}. + +Languages that use type erasure include Java, Scala, Haskell, ML, OCaml. + +Some other languages keep the type parameters around at run time, these include C++, C#, F#. + +Polymorphism +============ + +Polymorphism means that a function type comes "in many forms". + +In programming it means that + + - the function can be applied to arguments of many types, or + - the type can have instances of many types. +-> + +We have seen two principal forms of polymorphism: + + - subtyping: instances of a subclass can be passed to a base class + - generics: instances of a function or class are created by type parameterization. + +Exercise +======== + +Write a function `nth` that takes an integer `n` and a list and +selects the `n`'th element of the list. + +Elements are numbered from 0. + +If index is outside the range from `0` up the the length of the list minus one, a `IndexOutOfBoundsException` should be thrown. + +\quiz \ No newline at end of file diff --git a/lectures/progfun1-4-2.md b/lectures/progfun1-4-2.md new file mode 100644 index 0000000000000000000000000000000000000000..8cbcbb004aa41d68aa767ef23ced74fb5d5a970f --- /dev/null +++ b/lectures/progfun1-4-2.md @@ -0,0 +1,174 @@ +% Subtyping and Generics +% +% + +Polymorphism +============ + +Two principal forms of polymorphism: + + - subtyping + - generics + +In this session we will look at their interactions. + +Two main areas: + + - bounds + - variance + +Type Bounds +=========== + +Consider the method `assertAllPos` which + + - takes an `IntSet` + - returns the `IntSet` itself if all its elements are positive + - throws an exception otherwise + +What would be the best type you can give to `assertAllPos`? Maybe: +-> + def assertAllPos(s: IntSet): IntSet + +In most situations this is fine, but can one be more precise? + +Type Bounds +=========== + +One might want to express that `assertAllPos` +takes `Empty` sets to `Empty` sets and `NonEmpty` sets to `NonEmpty` sets. + +A way to express this is: + + def assertAllPos[S <: IntSet](r: S): S = ... + +Here, "`<: IntSet`" is an \red{upper bound} of the type parameter `S`: + +It means that `S` can be instantiated only to types that conform to `IntSet`. + +Generally, the notation + + - `S <: T` means: _`S` is a subtype of `T`_, and + - `S >: T` means: _`S` is a supertype of `T`_, or _`T` is a subtype of `S`_. + +Lower Bounds +============ + +You can also use a lower bound for a type variable. + +\example + + [S >: NonEmpty] + +introduces a type parameter `S` that can range only over \red{supertypes} +of `NonEmpty`. + +So `S` could be one of `NonEmpty`, `IntSet`, `AnyRef`, or `Any`. + +We will see later on in this session where lower bounds are useful. + +Mixed Bounds +============ + +Finally, it is also possible to mix a lower bound with an upper bound. + +For instance, + + [S >: NonEmpty <: IntSet] + +would restrict `S` any type on the interval between `NonEmpty` and `IntSet`. + +Covariance +========== + +There's another interaction between subtyping and type parameters we +need to consider. Given: + + NonEmpty <: IntSet + +is + + List[NonEmpty] <: List[IntSet] ? +-> +Intuitively, this makes sense: A list of non-empty sets is a special case of a list of arbitrary sets. + +We call types for which this relationship holds \red{covariant} +because their subtyping relationship varies with the type parameter. + +Does covariance make sense for all types, not just for `List`? + +Arrays +====== + +For perspective, let's look at arrays in Java (and C#). + +Reminder: + + - An array of `T` elements is written `T[]` in Java. + - In Scala we use parameterized type syntax `Array[T]` to refer to the same type. + +Arrays in Java are covariant, so one would have: + + NonEmpty[] <: IntSet[] + +Array Typing Problem +==================== + +But covariant array typing causes problems. + +To see why, consider the Java code below. + +\begin{lstlisting} + NonEmpty[] a = new NonEmpty[]{new NonEmpty(1, new Empty(), new Empty())} + IntSet[] b = a + b[0] = new Empty() + NonEmpty s = a[0] +\end{lstlisting} + +It looks like we assigned in the last line an `Empty` set to a +variable of type `NonEmpty`! + +What went wrong? + +The Liskov Substitution Principle +================================ + +The following principle, stated by Barbara Liskov, tells us when a +type can be a subtype of another. + +\begin{quote} +If \verb`A <: B`, +then everything one can to do with a value of type \verb`B` one should also +be able to do with a value of type \verb`A`. +\end{quote} + +[The actual definition Liskov used is a bit more formal. It says: + +\begin{quote} +Let \verb`q(x)` be a property provable about objects \verb`x` of type \verb`B`. +Then \verb`q(y)` should be provable for objects \verb`y` of type \verb`A` where \verb`A <: B`. +\end{quote} +] + +Exercise +======== + +The problematic array example would be written as follows in Scala: + + val a: Array[NonEmpty] = Array(NonEmpty(1, Empty(), Empty())) + val b: Array[IntSet] = a + b(0) = Empty() + val s: NonEmpty = a(0) + +When you try out this example, what do you observe? + +\begin{tabular}{ll} + \verb` O ` & A type error in line 1 +\\ \verb` O ` & A type error in line 2 +\\ \verb` O ` & A type error in line 3 +\\ \verb` O ` & A type error in line 4 +\\ \verb` O ` & A program that compiles and throws an exception at run-time +\\ \verb` O ` & A program that compiles and runs without exception +\end{tabular} +-> +\quiz \ No newline at end of file diff --git a/lectures/progfun1-4-3.md b/lectures/progfun1-4-3.md new file mode 100644 index 0000000000000000000000000000000000000000..ba76748076844ecc0b47332b77685397dfb22360 --- /dev/null +++ b/lectures/progfun1-4-3.md @@ -0,0 +1,236 @@ +% Variance +% +% + +Variance +======== + +You have seen the the previous session that some types should be covariant whereas +others should not. + +Roughly speaking, a type that accepts mutations of its elements should +not be covariant. + +But immutable types can be covariant, if some conditions +on methods are met. + +Definition of Variance +====================== + +Say `C[T]` is a parameterized type and `A`, `B` are types such that `A <: B`. + +In general, there are _three_ possible relationships between `C[A]` and `C[B]`: + +\begin{tabular}{p{8cm}l} + \verb@C[A] <: C[B]@ & \verb@C@ is \red{covariant} +\\ \verb@C[A] >: C[B]@ & \verb@C@ is \red{contravariant} +\\ neither \verb@C[A]@ nor \verb@C[B]@ is a subtype of the other & \verb@C@ is \red{nonvariant} +\end{tabular}\medskip + +-> + +Scala lets you declare the variance of a type by annotating the type parameter: + +\begin{tabular}{p{8cm}l} + \verb@class C[+A] { ... }@ & \verb@C@ is \red{covariant} +\\ \verb@class C[-A] { ... }@ & \verb@C@ is \red{contravariant} +\\ \verb@class C[A] { ... } @ & \verb@C@ is \red{nonvariant} +\end{tabular} + +Exercise +======== + +Say you have two function types: + + type A = IntSet => NonEmpty + type B = NonEmpty => IntSet + +According to the Liskov Substitution Principle, which of the +following should be true? + + O A <: B + O B <: A + O A and B are unrelated. +-> +\quiz + +Typing Rules for Functions +========================== + +Generally, we have the following rule for subtyping between function types: + +If `A2 <: A1` and `B1 <: B2`, then + + A1 => B1 <: A2 => B2 + +Function Trait Declaration +========================== + +So functions are _contravariant_ in their argument type(s) and +_covariant_ in their result type. + +This leads to the following revised definition of the `Function1` trait: + + package scala + trait Function1[-T, +U] + def apply(x: T): U + +Variance Checks +=============== + +We have seen in the array example that the combination of covariance with +certain operations is unsound. + +In this case the problematic operation was the update operation on an array. + +If we turn `Array` into a class, and `update` into a method, it would look like this: + + class Array[+T] + def update(x: T) = ... + +The problematic combination is + + - the covariant type parameter `T` + - which appears in parameter position of the method `update`. + +Variance Checks (2) +=================== + +The Scala compiler will check that there are no problematic combinations when compiling a class with variance annotations. + +Roughly, + + - _covariant_ type parameters can only appear in method results. + - _contravariant_ type parameters can only appear in method parameters. + - _invariant_ type parameters can appear anywhere. + +The precise rules are a bit more involved, fortunately the Scala compiler performs them for us. + +Variance-Checking the Function Trait +==================================== + +Let's have a look again at Function1: + + trait Function1[-T, +U] + def apply(x: T): U + +Here, + + - `T` is contravariant and appears only as a method parameter type + - `U` is covariant and appears only as a method result type + +So the method is checks out OK. + +Variance and Lists +================== + +Let's get back to the previous implementation of lists. + +One shortcoming was that `Nil` had to be a class, whereas we would +prefer it to be an object (after all, there is only one empty list). + +Can we change that? + +Yes, because we can make `List` covariant. +-> +Here are the essential modifications: + + trait List[+T] + ... + object Empty extends List[Nothing] + ... + +Making Classes Covariant +======================== + +Sometimes, we have to put in a bit of work to make a class covariant. + +Consider adding a `prepend` method to `List` which prepends a given +element, yielding a new list. + +A first implementation of `prepend` could look like this: + + trait List[+T] + def prepend(elem: T): List[T] = Cons(elem, this) + +But that does not work! + +Exercise +======== + +Why does the following code not type-check? + + trait List[+T] + def prepend(elem: T): List[T] = Cons(elem, this) + +Possible answers: + + `O` \tab `prepend` turns `List` into a mutable class. + + `O` \tab `prepend` fails variance checking. + + `O` \tab `prepend`'s right-hand side contains a type error. +-> + +\quiz + +Prepend Violates LSP +==================== + +Indeed, the compiler is right to throw out `List` with `prepend`, +because it violates the Liskov Substitution Principle: + +Here's something one can do with a list `xs` of type `List[IntSet]`: + + xs.prepend(Empty) + +But the same operation on a list `ys` of type `List[NonEmpty]` would lead to a type error: + + ys.prepend(Empty) + ^ type mismatch + required: NonEmpty + found: Empty + +So, `List[NonEmpty]` cannot be a subtype of `List[IntSet]`. + +Lower Bounds +============ + +But `prepend` is a natural method to have on immutable lists! + +\question: How can we make it variance-correct? +\medskip + +-> +We can use a _lower bound_: + + def prepend [U >: T] (elem: U): List[U] = Cons(elem, this) + +This passes variance checks, because: + + - _covariant_ type parameters may appear in _lower bounds_ of method type parameters + - _contravariant_ type parameters may appear in _upper bounds_. + +Exercise +======== + +Assume `prepend` in trait `List` is implemented like this: + + def prepend [U >: T] (elem: U): List[U] = Cons(elem, this) + +What is the result type of this function: + + def f(xs: List[NonEmpty], x: Empty) = xs.prepend(x) ? +\medskip + +Possible answers: + +\begin{tabular}{ll} + \verb` O ` & does not type check +\\ \verb` O ` & \verb`List[NonEmpty]` +\\ \verb` O ` & \verb`List[Empty]` +\\ \verb` O ` & \verb`List[IntSet]` +\\ \verb` O ` & \verb`List[Any]` +\end{tabular} +-> +\quiz \ No newline at end of file diff --git a/lectures/progfun1-4-4.md b/lectures/progfun1-4-4.md new file mode 100644 index 0000000000000000000000000000000000000000..945394cfd5df47f1e015f5174423a0b88cdb7490 --- /dev/null +++ b/lectures/progfun1-4-4.md @@ -0,0 +1,103 @@ +% Functions as Objects +% +% + +Functions as Objects +==================== + +We have seen that Scala's numeric types and the `Boolean` +type can be implemented like normal classes. + +But what about functions? +-> +In fact function values _are_ treated as objects in Scala. + +The function type `A => B` is just an abbreviation for the class +`scala.Function1[A, B]`, which is defined as follows. + + package scala + trait Function1[A, B] + def apply(x: A): B + +So functions are objects with `apply` methods. + +There are also traits `Function2`, `Function3`, ... for functions which take more parameters. + + +Expansion of Function Values +============================ + +An anonymous function such as + + (x: Int) => x * x + +is expanded to: +-> + { class AnonFun() extends Function1[Int, Int] + def apply(x: Int) = x * x + AnonFun() + } +-> +or, shorter, using _anonymous class syntax_: + + new Function1[Int, Int] { + def apply(x: Int) = x * x + } + + +Expansion of Function Calls +=========================== + +A function call, such as `f(a, b)`, where `f` is a value of some class +type, is expanded to + + f.apply(a, b) + +So the OO-translation of + + val f = (x: Int) => x * x + f(7) + +would be + + val f = new Function1[Int, Int] { + def apply(x: Int) = x * x + } + f.apply(7) + +Functions and Methods +===================== + +Note that a method such as + + def f(x: Int): Boolean = ... + +is not itself a function value. + +But if `f` is used in a place where a Function type is expected, it is +converted automatically to the function value + + (x: Int) => f(x) + +or, expanded: + + new Function1[Int, Boolean] { + def apply(x: Int) = f(x) + } + +Exercise +======== + +In package `week4`, define an + + object List { + ... + } + +with 3 functions in it so that users can create lists of lengths 0-2 using syntax + + List() // the empty list + List(1) // the list with single element 1 + List(2, 3) // the list with elements 2 and 3. + +\quiz \ No newline at end of file diff --git a/lectures/progfun1-4-5-decomposition.md b/lectures/progfun1-4-5-decomposition.md new file mode 100644 index 0000000000000000000000000000000000000000..5586855d429416753b23de05b405e8525fd3cdd0 --- /dev/null +++ b/lectures/progfun1-4-5-decomposition.md @@ -0,0 +1,166 @@ +% Decomposition +% +% + +Decomposition +============= + +Suppose you want to write a small interpreter for arithmetic +expressions. + +To keep it simple, let's restrict ourselves to numbers and +additions. + +Expressions can be represented as a class hierarchy, with a base trait +`Expr` and two subclasses, `Number` and `Sum`. + +To treat an expression, it's necessary to know the expression's shape +and its components. + +This brings us to the following implementation. + +Expressions +=========== + + trait Expr + def isNumber: Boolean + def isSum: Boolean + def numValue: Int + def leftOp: Expr + def rightOp: Expr + + class Number(n: Int) extends Expr + def isNumber: Boolean = true + def isSum: Boolean = false + def numValue: Int = n + def leftOp: Expr = throw Error("Number.leftOp") + def rightOp: Expr = throw Error("Number.rightOp") + +Expressions (2) +=============== + + class Sum(e1: Expr, e2: Expr) extends Expr + def isNumber: Boolean = false + def isSum: Boolean = true + def numValue: Int = throw Error("Sum.numValue") + def leftOp: Expr = e1 + def rightOp: Expr = e2 + +Evaluation of Expressions +========================= + +You can now write an evaluation function as follows. + + def eval(e: Expr): Int = + if e.isNumber then e.numValue + else if e.isSum then eval(e.leftOp) + eval(e.rightOp) + else throw Error("Unknown expression " + e) + +\red{Problem}: Writing all these classification and accessor functions +quickly becomes tedious! + +Adding New Forms of Expressions +=============================== + +So, what happens if you want to add new expression forms, say + + class Prod(e1: Expr, e2: Expr) extends Expr // e1 * e2 + class Var(x: String) extends Expr // Variable `x' + +You need to add methods for classification and access to all classes +defined above. + +Question +======== + +To integrate `Prod` and `Var` into the hierarchy, how many new +method definitions do you need? + +(including method definitions in `Prod` and `Var` themselves, but not counting methods that were already given on the slides) + +Possible Answers + + O 9 + O 10 + O 19 + O 25 + O 35 + O 40 + +-> +\quiz + +Non-Solution: Type Tests and Type Casts +======================================= + +A "hacky" solution could use type tests and type casts. + +Scala let's you do these using methods defined in class `Any`: + + def isInstanceOf[T]: Boolean // checks whether this object's type conforms to `T` + def asInstanceOf[T]: T // treats this object as an instance of type `T` + // throws `ClassCastException` if it isn't. + +These correspond to Java's type tests and casts + + Scala Java + + x.isInstanceOf[T] x instanceof T + x.asInstanceOf[T] (T) x + +But their use in Scala is discouraged, because there are better alternatives. + +Eval with Type Tests and Type Casts +=================================== + +Here's a formulation of the `eval` method using type tests and casts: + + def eval(e: Expr): Int = + if e.isInstanceOf[Number] then + e.asInstanceOf[Number].numValue + else if e.isInstanceOf[Sum] then + eval(e.asInstanceOf[Sum].leftOp) + + eval(e.asInstanceOf[Sum].rightOp) + else throw new Error("Unknown expression " + e) + +Assessment of this solution: +-> +\begin{tabular}{lp{10cm}} + + & no need for classification methods, access methods only for classes where the value is defined. +\\ + -- & low-level and potentially unsafe. +\end{tabular} + +Solution 1: Object-Oriented Decomposition +========================================= + +For example, suppose that all you want to do is _evaluate_ expressions. + +You could then define: + + trait Expr + def eval: Int + + class Number(n: Int) extends Expr + def eval: Int = n + + class Sum(e1: Expr, e2: Expr) extends Expr + def eval: Int = e1.eval + e2.eval + +But what happens if you'd like to display expressions now? + +You have to define new methods in all the subclasses. + +Limitations of OO Decomposition +=============================== + +And what if you want to simplify the expressions, say using the rule: + + a * b + a * c -> a * (b + c) + +\red{Problem}: This is a non-local simplification. It cannot be encapsulated in the +method of a single object. + +You are back to square one; you need test and access methods for all the different +subclasses. + diff --git a/lectures/progfun1-4-5.md b/lectures/progfun1-4-5.md new file mode 100644 index 0000000000000000000000000000000000000000..b0d8930fb1259b15f17f69b08224d47de2929e2c --- /dev/null +++ b/lectures/progfun1-4-5.md @@ -0,0 +1,151 @@ +% Higher-Order Functions +% +% +Higher-Order Functions +====================== + +Functional languages treat functions as _first-class values_. + +This means that, like any other value, a function +can be passed as a parameter and returned as a result. + +This provides a flexible way to compose programs. + +Functions that take other functions as parameters or that return functions +as results are called _higher order functions_. + +Example: +======== + +Take the sum of the integers between `a` and `b`: + + def sumInts(a: Int, b: Int): Int = + if a > b then 0 else a + sumInts(a + 1, b) + +Take the sum of the cubes of all the integers between `a` +and `b` : + + def cube(x: Int): Int = x * x * x + + def sumCubes(a: Int, b: Int): Int = + if a > b then 0 else cube(a) + sumCubes(a + 1, b) + +Example (ctd) +============= + +Take the sum of the factorials of all the integers between `a` +and `b` : + + def sumFactorials(a: Int, b: Int): Int = + if a > b then 0 else fact(a) + sumFactorials(a + 1, b) + + +These are special cases of +$$ + \sum^b_{n=a} f(n) +$$ +for different values of $f$. + +Can we factor out the common pattern? + +Summing with Higher-Order Functions +=================================== + +Let's define: + + def sum(f: Int => Int, a: Int, b: Int): Int = + if a > b then 0 + else f(a) + sum(f, a + 1, b) + +We can then write: + + def sumInts(a: Int, b: Int) = sum(id, a, b) + def sumCubes(a: Int, b: Int) = sum(cube, a, b) + def sumFactorials(a: Int, b: Int) = sum(fact, a, b) + +where + + def id(x: Int): Int = x + def cube(x: Int): Int = x * x * x + def fact(x: Int): Int = if x == 0 then 1 else x * fact(x - 1) + +Function Types +============== + +The type `A => B` is the type of a \red{function} that +takes an argument of type `A` and returns a result of +type `B`. + +So, `Int => Int` is the type of functions that map integers to integers. + +Anonymous Functions +=================== + +Passing functions as parameters leads to the creation of many small functions. + + - Sometimes it is tedious to have to define (and name) these functions using `def`. + +Compare to strings: We do not need to define a string using `def`. Instead of + + def str = "abc"; println(str) + +We can directly write + + println("abc") + +because strings exist as _literals_. Analogously we would like function literals, which let us write a function without giving it a name. + +These are called _anonymous functions_. + +Anonymous Function Syntax +========================= + +\example: A function that raises its argument to a cube: + + (x: Int) => x * x * x + +Here, `(x: Int)` is the \red{parameter} of the function, and +`x * x * x` is it's \red{body}. + +- The type of the parameter can be omitted if it can be inferred by the +compiler from the context. + +If there are several parameters, they are separated by commas: + + (x: Int, y: Int) => x + y + +Anonymous Functions are Syntactic Sugar +======================================= + +An anonymous function $(\btt x_1: T_1, ..., x_n: T_n) \Rightarrow E$ +can always be expressed using `def` as follows: + +$$ +\btt { def\ f(x_1: T_1, ..., x_n: T_n) = E ; f } +$$ + +where $\btt f$ is an arbitrary, fresh name (that's not yet used in the program). + + - One can therefore say that anonymous functions are _syntactic sugar_. + +Summation with Anonymous Functions +================================== + +Using anonymous functions, we can write sums in a shorter way: + + def sumInts(a: Int, b: Int) = sum(x => x, a, b) + def sumCubes(a: Int, b: Int) = sum(x => x * x * x, a, b) + +Exercise +======== + +\quiz + +The `sum` function uses linear recursion. Write a tail-recursive version by replacing the `???`s. + + def sum(f: Int => Int, a: Int, b: Int): Int = + def loop(a: Int, acc: Int): Int = + if ??? then ??? + else loop(???, ???) + loop(???, ???) + diff --git a/lectures/progfun1-4-6-pattern-matching.md b/lectures/progfun1-4-6-pattern-matching.md new file mode 100644 index 0000000000000000000000000000000000000000..791dc7335f7a645316aa4fb29b455f2885c0bcf4 --- /dev/null +++ b/lectures/progfun1-4-6-pattern-matching.md @@ -0,0 +1,211 @@ +% Pattern Matching +% +% +Reminder: Decomposition +======================= + +The task we are trying to solve is find a general and convenient way to access +objects in a extensible class hierarchy. + +\vspace{3cm} + + + +\red{Attempts seen previously}: + +- _Classification and access methods_: quadratic explosion + +- _Type tests and casts_: unsafe, low-level + +- _Object-oriented decomposition_: does not always work, need to touch all classes to add a new method. + + +Solution 2: Functional Decomposition with Pattern Matching +========================================================== + +Observation: the sole purpose of test and accessor functions is to +\red{reverse} the construction process: + + - Which subclass was used? + - What were the arguments of the constructor? + +This situation is so common that many functional languages, Scala included, automate it. + +Case Classes +============ + +A _case class_ definition is similar to a normal class definition, except +that it is preceded by the modifier `case`. For example: + + trait Expr + case class Number(n: Int) extends Expr + case class Sum(e1: Expr, e2: Expr) extends Expr + +Like before, this defines a trait `Expr`, and two +concrete subclasses `Number` and `Sum`. + +Case Classes (2) +================ + +It also implicitly defines companion objects with `apply` methods. + + object Number + def apply(n: Int) = new Number(n) + + object Sum + def apply(e1: Expr, e2: Expr) = new Sum(e1, e2) + +so you can write `Number(1)` instead of `new Number(1)`. + +However, these classes are now empty. So how can we access the members? + +Pattern Matching +================ + +_Pattern matching_ is a generalization of `switch` from +C/Java to class hierarchies. + +It's expressed in Scala using the keyword `match`. + +\example + + def eval(e: Expr): Int = e match + case Number(n) => n + case Sum(e1, e2) => eval(e1) + eval(e2) + +Match Syntax +============ + +Rules: + + - `match` is followed by a sequence of \red{cases}, `pat => expr`. + - Each case associates an \red{expression} `expr` with a \red{pattern} `pat`. + - A `MatchError` exception is thrown if no pattern matches the +value of the selector. + +Forms of Patterns +================= + +Patterns are constructed from: + + - _constructors_, e.g. `Number`, `Sum`, + - _variables_, e.g. `n`, `e1`, `e2`, + - _wildcard patterns_ `_`, + - _constants_, e.g. `1`, `true`. + +Variables always begin with a lowercase letter. + +The same variable name can only appear once in a pattern. So, +`Sum(x, x)` is not a legal pattern. + +Names of constants begin with a capital letter, +with the exception of the reserved words `null`, `true`, +`false`. + +Evaluating Match Expressions +============================ + +An expression of the form +$$\btt +e\ match\ \{\ case\ p_1 => e_1\ ...\ case\ p_n => e_n\ \} +$$ +matches the value of the selector $\btt e$ with the patterns +$\btt p_1, ..., p_n$ in the order in which they are written. + +The whole match expression is rewritten to the right-hand side of the first +case where the pattern matches the selector $e$. + +References to pattern variables are replaced by the corresponding +parts in the selector. + +What Do Patterns Match? +======================= + + - A constructor pattern $\btt C(p_1 , ..., p_n)$ matches + all the values of type $\btt C$ (or a subtype) that have been + constructed with arguments matching the patterns $\btt p_1, ..., p_n$. + - A variable pattern $\btt x$ matches any value, and + \red{binds} the name of the variable to this value. + - A constant pattern $\btt c$ matches values that are equal to + $\btt c$ (in the sense of `==`) + + +Example +======= +\example + + + eval(Sum(Number(1), Number(2))) + +$\rightarrow$ + + Sum(Number(1), Number(2)) match + case Number(n) => n + case Sum(e1, e2) => eval(e1) + eval(e2) + +$\rightarrow$ + + eval(Number(1)) + eval(Number(2)) + +Example (2) +=========== + +$\rightarrow$ + + Number(1) match + case Number(n) => n + case Sum(e1, e2) => eval(e1) + eval(e2) + + eval(Number(2)) + +$\rightarrow$ + + 1 + eval(Number(2)) + +$\rightarrow\dhd$ + + 3 + +Pattern Matching and Methods +============================ + +Of course, it's also possible to define the evaluation function as a +method of the base trait. + +\example + + trait Expr + def eval: Int = this match + case Number(n) => n + case Sum(e1, e2) => e1.eval + e2.eval + +Exercise +======== + +Write a function `show` that uses pattern +matching to return the representation of a given +expressions as a string. + + def show(e: Expr): String = ??? + +Exercise (Optional, Harder) +=========================== + +Add case classes `Var` for variables `x` and +`Prod` for products `x * y` as discussed previously. + +Change your `show` function so that it also deals with products. + +Pay attention you get operator precedence right but to use as few +parentheses as possible. + +\example + + Sum(Prod(2, Var("x")), Var("y")) + +should print as "`2 * x + y`". But + + Prod(Sum(2, Var("x")), Var("y")) + +should print as "`(2 + x) * y`". + + diff --git a/lectures/progfun1-4-6.md b/lectures/progfun1-4-6.md new file mode 100644 index 0000000000000000000000000000000000000000..62b771dc1def0a48aef306f6e0f069b9d721fd75 --- /dev/null +++ b/lectures/progfun1-4-6.md @@ -0,0 +1,164 @@ +% Currying +% +% +Motivation +========== + +Look again at the summation functions: + + def sumInts(a: Int, b: Int) = sum(x => x, a, b) + def sumCubes(a: Int, b: Int) = sum(x => x * x * x, a, b) + def sumFactorials(a: Int, b: Int) = sum(fact, a, b) + +\question + +Note that `a` and `b` get passed unchanged from `sumInts` and `sumCubes` into `sum`. + +Can we be even shorter by getting rid of these parameters? + + + +Functions Returning Functions +============================= + +Let's rewrite `sum` as follows. + + def sum(f: Int => Int): (Int, Int) => Int = + def sumF(a: Int, b: Int): Int = + if a > b then 0 + else f(a) + sumF(a + 1, b) + sumF + +`sum` is now a function that returns another function. + +The returned function `sumF` applies the given function parameter `f` and sums the results. + + +Stepwise Applications +===================== + +We can then define: + + def sumInts = sum(x => x) + def sumCubes = sum(x => x * x * x) + def sumFactorials = sum(fact) + +These functions can in turn be applied like any other function: + + sumCubes(1, 10) + sumFactorials(10, 20) + +Consecutive Stepwise Applications +================================= + +In the previous example, can we avoid the `sumInts`, `sumCubes`, ... middlemen? + +Of course: + + sum (cube) (1, 10) +-> +- `sum(cube)` applies `sum` to `cube` and returns +the _sum of cubes_ function. + +- `sum(cube)` is therefore equivalent to `sumCubes`. + +- This function is next applied to the arguments `(1, 10)`. +-> +Generally, function application associates to the left: + + sum(cube)(1, 10) == (sum (cube)) (1, 10) + +Multiple Parameter Lists +======================== + +The definition of functions that return functions is so useful in +functional programming that there is a special syntax for it in Scala. + +For example, the following definition of `sum` is equivalent to +the one with the nested `sumF` function, but shorter: + + def sum(f: Int => Int)(a: Int, b: Int): Int = + if a > b then 0 else f(a) + sum(f)(a + 1, b) + +Expansion of Multiple Parameter Lists +===================================== + +In general, a definition of a function with multiple parameter lists + +$$\btt + def\ f (args_1) ... (args_n) = E +$$ + +where $\btt n > 1$, is equivalent to +$$\btt + def\ f (args_1) ... (args_{n-1}) = \{ def\ g (args_n) = E ; g \} +$$ + +where `g` is a fresh identifier. +Or for short: + +$$\btt + def\ f (args_1) ... (args_{n-1}) = ( args_n \Rightarrow E ) +$$ + +Expansion of Multiple Parameter Lists (2) +========================================= + +By repeating the process $n$ times +$$\btt + def\ f (args_1) ... (args_{n-1}) (args_n) = E +$$ +is shown to be equivalent to +$$\btt + def\ f = (args_1 \Rightarrow ( args_2 \Rightarrow ... ( args_n \Rightarrow E ) ... )) +$$ + +This style of definition and function application is called _currying_, +named for its instigator, Haskell Brooks Curry (1900-1982), a twentieth century logician. + +In fact, the idea goes back even further to Schönfinkel and Frege, but the term ``currying" has stuck. + +More Function Types +=================== + +Question: Given, + + def sum(f: Int => Int)(a: Int, b: Int): Int = ... + +What is the type of `sum` ? + +More Function Types +=================== + +Question: Given, + + def sum(f: Int => Int)(a: Int, b: Int): Int = ... + +What is the type of `sum` ? + +\boldred{Answer:} + + (Int => Int) => (Int, Int) => Int + +Note that functional types associate to the right. That is to say that + + Int => Int => Int + +is equivalent to + + Int => (Int => Int) + +Exercise +======== + +\quiz + + 1. Write a `product` function that calculates the product of + the values of a function for the points on a given interval. + + 2. Write `factorial` in terms of `product`. + + 3. Can you write a more general function, which generalizes + both `sum` and `product`? + + + diff --git a/lectures/progfun1-4-7-syntax.md b/lectures/progfun1-4-7-syntax.md new file mode 100644 index 0000000000000000000000000000000000000000..ad1e9c7acef61e240830e779bc4c00198ef3da2f --- /dev/null +++ b/lectures/progfun1-4-7-syntax.md @@ -0,0 +1,93 @@ +% Scala Syntax Summary +% +% + +Language Elements Seen So Far: +============================== + +We have seen language elements to express types, expressions +and definitions. + +Below, we give their context-free syntax in Extended Backus-Naur form (EBNF), +where + +$\gap$ `|` denotes an alternative, \par + +$\gap$ `[...]` an option (0 or 1), \par + +$\gap$ `{...}` a repetition (0 or more). + +Types +===== + +\begin{lstlisting} +Type = SimpleType | FunctionType +FunctionType = SimpleType `=>' Type + | `(' [Types] `)' `=>' Type +SimpleType = Ident +Types = Type {`,' Type} +\end{lstlisting} + +A \red{type} can be: + + - A \red{numeric type}: `Int`, `Double` (and `Byte`, `Short`, `Char`, `Long`, `Float`), + - The `Boolean` type with the values `true` and `false`, + - The `String` type, + - A \red{function type}, like `Int => Int`, `(Int, Int) => Int`. + +Later we will see more forms of types. + +Expressions +=========== + +\begin{lstlisting} +Expr = InfixExpr | FunctionExpr + | if `(' Expr `)' Expr else Expr +InfixExpr = PrefixExpr | InfixExpr Operator InfixExpr +Operator = ident +PrefixExpr = [`+' | `-' | `!' | `~' ] SimpleExpr +SimpleExpr = ident | literal | SimpleExpr `.' ident + | Block +FunctionExpr = Bindings `=>` Expr +Bindings = ident [`:' SimpleType] + | `(' [Binding {`,' Binding}] `)' +Binding = ident [`:' Type] +Block = `{' {Def `;'} Expr `}' +\end{lstlisting} + +Expressions (2) +=============== + +An \red{expression} can be: + +- An \red{identifier} such as `x`, `isGoodEnough`, +- A \red{literal}, like `0`, `1.0`, `"abc"`, +- A \red{function application}, like `sqrt(x)`, +- An \red{operator application}, like `-x`, `y + x`, +- A \red{selection}, like `math.abs`, +- A \red{conditional expression}, like `if x < 0 then -x else x`, +- A \red{block}, like `{ val x = math.abs(y) ; x * 2 }` +- An \red{anonymous function}, like `x => x + 1`. + +Definitions +=========== + +\begin{lstlisting} +Def = FunDef | ValDef +FunDef = def ident {`(' [Parameters] `)'} + [`:' Type] `=' Expr +ValDef = val ident [`:' Type] `=' Expr +Parameter = ident `:' [ `=>' ] Type +Parameters = Parameter {`,' Parameter} +\end{lstlisting} + +A \red{definition} can be: + +- A \red{function definition}, like `def square(x: Int) = x * x` +- A \red{value definition}, like `val y = square(2)` + +A \red{parameter} can be: + +- A \red{call-by-value parameter}, like `(x: Int)`, +- A \red{call-by-name parameter}, like `(y: => Double)`. + diff --git a/lectures/progfun1-4-7.md b/lectures/progfun1-4-7.md new file mode 100644 index 0000000000000000000000000000000000000000..0c191802cbb2ba2c9a46449085cf4d4988673d8e --- /dev/null +++ b/lectures/progfun1-4-7.md @@ -0,0 +1,152 @@ +% Example: Finding Fixed Points +% +% +Finding a fixed point of a function +=================================== + +A number `x` is called a \red{fixed point} of a function `f` if + + f(x) = x + +For some functions `f` we can locate the fixed points by +starting with an initial estimate and then by applying `f` in a repetitive way. + + x, f(x), f(f(x)), f(f(f(x))), ... + +until the value does not vary anymore (or the change is sufficiently +small). + +Programmatic Solution +===================== + +This leads to the following function for finding a fixed point: + + val tolerance = 0.0001 + def isCloseEnough(x: Double, y: Double) = + abs((x - y) / x) / x < tolerance + def fixedPoint(f: Double => Double)(firstGuess: Double) = + def iterate(guess: Double): Double = + val next = f(guess) + if isCloseEnough(guess, next) then next + else iterate(next) + iterate(firstGuess) + +Return to Square Roots +====================== + +Here is a _specification_ of the `sqrt` function: + +$\gap$ `sqrt(x)` $=$ the number `y` such that `y * y = x`. + +Or, by dividing both sides of the equation with `y`: + +$\gap$ `sqrt(x)` $=$ the number `y` such that `y = x / y`. + +Consequently, `sqrt(x)` is a fixed point of the function `(y => x / y)`. + +First Attempt +============= + +This suggests to calculate `sqrt(x)` by iteration towards a fixed point: + + def sqrt(x: Double) = + fixedPoint(y => x / y)(1.0) + +Unfortunately, this does not converge. + +Let's add a `println` instruction to the function `fixedPoint` so +we can follow the current value of `guess`: + +First Attempt (2) +================= + + def fixedPoint(f: Double => Double)(firstGuess: Double) = + + def iterate(guess: Double): Double = + val next = f(guess) + println(next) + if isCloseEnough(guess, next) then next + else iterate(next) + + iterate(firstGuess) + +`sqrt(2)` then produces: + + 2.0 + 1.0 + 2.0 + 1.0 + ... + +Average Damping +=============== + +One way to control such oscillations is to prevent the estimation from +varying too much. This is done by _averaging_ successive values of the +original sequence: + + def sqrt(x: Double) = fixedPoint(y => (y + x / y) / 2)(1.0) + +This produces + + 1.5 + 1.4166666666666665 + 1.4142156862745097 + 1.4142135623746899 + 1.4142135623746899 + +In fact, if we expand the fixed point function `fixedPoint` we find a similar +square root function to what we developed last week. + +Functions as Return Values +========================== + +The previous examples have shown that the expressive power of a +language is greatly increased if we can pass function arguments. + +The following example shows that functions that return functions +can also be very useful. + +Consider again iteration towards a fixed point. + +We begin by observing that $\sqrt x$ is a fixed point of the function +`y => x / y`. + +Then, the iteration converges by averaging successive values. + +This technique of _stabilizing by averaging_ is general enough to +merit being abstracted into its own function. + + def averageDamp(f: Double => Double)(x: Double) = (x + f(x)) / 2 + +Exercise: +========= + +Write a square root function using +`fixedPoint` and `averageDamp`. + +\quiz + +Final Formulation of Square Root +================================ + + def sqrt(x: Double) = fixedPoint(averageDamp(y => x/y))(1.0) + +This expresses the elements of the algorithm as clearly as possible. + +Summary +======= + +We saw last week that the functions are essential abstractions because +they allow us to introduce general methods to perform computations as +explicit and named elements in our programming language. + +This week, we've seen that these abstractions can be combined with +higher-order functions to create new abstractions. + +As a programmer, one must look for opportunities to abstract and +reuse. + +The highest level of abstraction is not always the best, but it +is important to know the techniques of abstraction, so as to +use them when appropriate. diff --git a/lectures/progfun1-5-0.md b/lectures/progfun1-5-0.md new file mode 100644 index 0000000000000000000000000000000000000000..52ef6328539db2e6a9f2ebe8ab541d5056c7e928 --- /dev/null +++ b/lectures/progfun1-5-0.md @@ -0,0 +1,192 @@ +% Lists +% +% +Lists +===== + +The list is a fundamental data structure in functional programming. + +A list having $\btt x_1, ..., x_n$ +as elements is written `List(`$\btt x_1, ..., x_n$`)` + +\example + + val fruit = List("apples", "oranges", "pears") + val nums = List(1, 2, 3, 4) + val diag3 = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1)) + val empty = List() + +There are two important differences between lists and arrays. + + - Lists are immutable --- the elements of a list cannot be changed. + - Lists are recursive, while arrays are flat. + +Lists +===== + + val fruit = List("apples", "oranges", "pears") + val diag3 = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1)) + +The List Type +============= + +Like arrays, lists are \red{\em homogeneous}: the elements of a +list must all have the same type. + +The type of a list with elements of type `T` is written `scala.List[T]` or shorter just `List[T]` + +\example + + val fruit: List[String] = List("apples", "oranges", "pears") + val nums : List[Int] = List(1, 2, 3, 4) + val diag3: List[List[Int]] = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1)) + val empty: List[Nothing] = List() + +Constructors of Lists +===================== + +All lists are constructed from: + + - the empty list `Nil`, and + - the construction operation `::` (pronounced \emph{cons}): \newline +`x :: xs` gives a new list with the first element `x`, followed by the elements of `xs`. + +For example: + + fruit = "apples" :: ("oranges" :: ("pears" :: Nil)) + nums = 1 :: (2 :: (3 :: (4 :: Nil))) + empty = Nil + + +Right Associativity +=================== + +Convention: Operators ending in "`:`" associate to the right. + +\gap `A :: B :: C` is interpreted as `A :: (B :: C)`. + +We can thus omit the parentheses in the definition above. + +\example + + val nums = 1 :: 2 :: 3 :: 4 :: Nil + +Operators ending in "`:`" are also different in the they are seen as method calls of the _right-hand_ operand. + +So the expression above is equivalent to + + Nil.::(4).::(3).::(2).::(1) + +Operations on Lists +=================== + +All operations on lists can be expressed in terms of the following +three operations: + +\begin{tabular}{ll} +\verb`head` & the first element of the list \\ +\verb`tail` & the list composed of all the elements except the first. \\ +\verb`isEmpty` & `true` if the list is empty, `false` otherwise. +\end{tabular} + +These operations are defined as methods of objects of type list. +For example: + + fruit.head == "apples" + fruit.tail.head == "oranges" + diag3.head == List(1, 0, 0) + empty.head == throw new NoSuchElementException("head of empty list") + +List Patterns +============= + +It is also possible to decompose lists with pattern matching. + +\begin{tabular}{lp{9cm}} + \verb`Nil` & The \verb`Nil` constant \\ + \verb`p :: ps` & A pattern that matches a list with a \verb`head` matching \verb`p` and a + \verb`tail` matching \verb`ps`. \\ + \verb`List(p1, ..., pn)` & same as \verb`p1 :: ... :: pn :: Nil` +\end{tabular} + +\example + +\begin{tabular}{lp{9cm}} + \verb`1 :: 2 :: xs` & Lists of that start with \verb`1` and then \verb`2` +\\ \verb`x :: Nil` & Lists of length 1 +\\ \verb`List(x)` & Same as \verb`x :: Nil` +\\ \verb`List()` & The empty list, same as \verb`Nil` +\\ \verb`List(2 :: xs)` & A list that contains as only element another list + that starts with \verb`2`. +\end{tabular} + +Exercise +======== + +Consider the pattern `x :: y :: List(xs, ys) :: zs`. + +What is the condition that describes most accurately the length `L` +of the lists it matches? + + O L == 3 + O L == 4 + O L == 5 + O L >= 3 + O L >= 4 + O L >= 5 +-> +\quiz + +Sorting Lists +============= + +Suppose we want to sort a list of numbers in ascending order: + + - One way to sort the list `List(7, 3, 9, 2)` is to sort the + tail `List(3, 9, 2)` to obtain `List(2, 3, 9)`. + - The next step is to insert the head `7` in the right place + to obtain the result `List(2, 3, 7, 9)`. + +This idea describes \red{Insertion Sort} : + + def isort(xs: List[Int]): List[Int] = xs match { + case List() => List() + case y :: ys => insert(y, isort(ys)) + } + +Exercise +======== + +Complete the definition insertion sort by filling in the `???`s in the definition below: + + def insert(x: Int, xs: List[Int]): List[Int] = xs match { + case List() => ??? + case y :: ys => ??? + } + +What is the worst-case complexity of insertion sort relative to the length of the input list `N`? + + O the sort takes constant time + O proportional to N + O proportional to N log(N) + O proportional to N * N + +\quiz + +Exercise +======== + +Complete the definition insertion sort by filling in the `???`s in the definition below: + + def insert(x: Int, xs: List[Int]): List[Int] = xs match { + case List() => + case y :: ys => + } + +What is the worst-case complexity of insertion sort relative to the length of the input list `N`? + + O the sort takes constant time + O proportional to N + O proportional to N * log(N) + O proportional to N * N + diff --git a/lectures/progfun1-5-1.md b/lectures/progfun1-5-1.md new file mode 100644 index 0000000000000000000000000000000000000000..d62ba5a934313f91fa8c1e1c7a0a8d082d152199 --- /dev/null +++ b/lectures/progfun1-5-1.md @@ -0,0 +1,231 @@ +% More Functions on Lists +% +% + +List Methods (1) +================ + +\red{Sublists and element access:} + +\begin{tabular}{lp{8cm}} + \verb` xs.length ` & The number of elements of \verb`xs`. +\\ \verb` xs.last` & The list's last element, exception if \verb`xs` is empty. +\\ \verb` xs.init` & A list consisting of all elements of \verb`xs` except the last one, + exception if \verb`xs` is empty. +\\ \verb` xs take n` & A list consisting of the first \verb`n` elements of \verb`xs`, or \verb`xs` itself + if it is shorter than \verb`n`. +\\ \verb` xs drop n` & The rest of the collection after taking \verb`n` elements. +\\ \verb` xs(n)` & (or, written out, \verb`xs apply n`). The element of \verb`xs` at index \verb`n`. + +\end{tabular} + +List Methods (2) +================ + +\red{Creating new lists:} + +\begin{tabular}{lp{8cm}} + \verb` xs ++ ys` & The list consisting of all elements of \verb`xs` + followed by all elements of \verb`ys`. +\\ \verb` xs.reverse` & The list containing the elements of \verb`xs` in reversed order. +\\ \verb` xs updated (n, x)` & The list containing the same elements as \verb`xs`, except at index + \verb`n` where it contains \verb`x`. +\end{tabular} + +\red{Finding elements:} + +\begin{tabular}{lp{8cm}} + \verb` xs indexOf x `& The index of the first element in \verb`xs` equal to \verb`x`, or \verb`-1` if \verb`x` + does not appear in \verb`xs`. +\\ \verb` xs contains x ` & same as \verb`xs indexOf x >= 0` +\end{tabular} + +Implementation of `last` +======================== + +The complexity of `head` is (small) constant time. + +What is the complexity of `last`? + +To find out, let's write a possible implementation of `last` as a stand-alone function. + + def last[T](xs: List[T]): T = xs match { + case List() => throw new Error("last of empty list") + case List(x) => + case y :: ys => + } + +Implementation of `last` +======================== + +The complexity of `head` is (small) constant time. + +What is the complexity of `last`? + +To find out, let's write a possible implementation of `last` as a stand-alone function. + + def last[T](xs: List[T]): T = xs match { + case List() => throw new Error("last of empty list") + case List(x) => x + case y :: ys => + } + +Implementation of `last` +======================== + +The complexity of `head` is (small) constant time. + +What is the complexity of `last`? + +To find out, let's write a possible implementation of `last` as a stand-alone function. + + def last[T](xs: List[T]): T = xs match { + case List() => throw new Error("last of empty list") + case List(x) => x + case y :: ys => last(ys) + } +-> +So, `last` takes steps proportional to the length of the list `xs`. + +Exercise +======== + +Implement `init` as an external function, analogous to `last`. + + def init[T](xs: List[T]): List[T] = xs match { + case List() => throw new Error("init of empty list") + case List(x) => ??? + case y :: ys => ??? + } + +\quiz + +Exercise +======== + +Implement `init` as an external function, analogous to `last`. + + def init[T](xs: List[T]): List[T] = xs match { + case List() => throw new Error("init of empty list") + case List(x) => + case y :: ys => + } + +\quiz + +Implementation of Concatenation +=============================== + +How can concatenation be implemented? + +Let's try by writing a stand-alone function: + + def concat[T](xs: List[T], ys: List[T]) = + +Implementation of Concatenation +=============================== + +How can concatenation be implemented? + +Let's try by writing a stand-alone function: + + def concat[T](xs: List[T], ys: List[T]) = xs match { + case List() => + case z :: zs => + } + +Implementation of Concatenation +=============================== + +How can concatenation be implemented? + +Let's try by writing a stand-alone function: + + def concat[T](xs: List[T], ys: List[T]) = xs match { + case List() => ys + case z :: zs => + } + +Implementation of Concatenation +=============================== + +How can concatenation be implemented? + +Let's try by writing a stand-alone function: + + def concat[T](xs: List[T], ys: List[T]) = xs match { + case List() => ys + case z :: zs => z :: concat(zs, ys) + } + +-> +What is the complexity of `concat`? + + +Implementation of `reverse` +=========================== + +How can reverse be implemented? + +Let's try by writing a stand-alone function: + + def reverse[T](xs: List[T]): List[T] = xs match { + case List() => + case y :: ys => + } + +Implementation of `reverse` +=========================== + +How can reverse be implemented? + +Let's try by writing a stand-alone function: + + def reverse[T](xs: List[T]): List[T] = xs match { + case List() => List() + case y :: ys => + } + + + +Implementation of `reverse` +=========================== + +How can reverse be implemented? + +Let's try by writing a stand-alone function: + + def reverse[T](xs: List[T]): List[T] = xs match { + case List() => List() + case y :: ys => reverse(ys) ++ List(y) + } +-> +What is the complexity of `reverse`? + +\red{Can we do better?} (to be solved later). + +Exercise +======== + +Remove the `n`'th element of a list `xs`. If `n` is out of bounds, return `xs` itself. + + def removeAt[T](n: Int, xs: List[T]) = ??? + +Usage example: + +\begin{worksheet} + \verb`removeAt(1, List('a', 'b', 'c', 'd'))` \wsf List(a, c, d) +\end{worksheet} + +Exercise (Harder, Optional) +=========================== + +Flatten a list structure: + + def flatten(xs: List[Any]): List[Any] = ??? + + flatten(List(List(1, 1), 2, List(3, List(5, 8)))) + > res0: List[Any] = List(1, 1, 2, 3, 5, 8) + + + diff --git a/lectures/progfun1-5-2.md b/lectures/progfun1-5-2.md new file mode 100644 index 0000000000000000000000000000000000000000..508f56b31257bbd74f4abe5606b8a5d1aa4b2e35 --- /dev/null +++ b/lectures/progfun1-5-2.md @@ -0,0 +1,147 @@ +% Pairs and Tuples +% +% +Sorting Lists Faster +==================== + +As a non-trivial example, let's design a function to sort lists +that is more efficient than insertion sort. + +A good algorithm for this is \red{merge sort}. The idea is as +follows: + +If the list consists of zero or one elements, it is already sorted. + +Otherwise, + + - Separate the list into two sub-lists, each containing around half of +the elements of the original list. + - Sort the two sub-lists. + - Merge the two sorted sub-lists into a single sorted list. + +First MergeSort Implementation +============================== + +Here is the implementation of that algorithm in Scala: + + def msort(xs: List[Int]): List[Int] = { + val n = xs.length/2 + if n == 0 then xs + else { + def merge(xs: List[Int], ys: List[Int]) = ??? + val (fst, snd) = xs.splitAt(n) + merge(msort(fst), msort(snd)) + } + } + +Definition of Merge +=================== + +Here is a definition of the `merge` function: + + def merge(xs: List[Int], ys: List[Int]) = + xs match { + case Nil => + ys + case x :: xs1 => + ys match { + case Nil => + xs + case y :: ys1 => + if x < y then x :: merge(xs1, ys) + else y :: merge(xs, ys1) + } + } + +The SplitAt Function +==================== + +The `splitAt` function on lists returns two sublists + + - the elements up the the given index + - the elements from that index + +The lists are returned in a \red{pair}. + + +Detour: Pair and Tuples +======================= + +The pair consisting of `x` and `y` is written `(x, y)` in Scala. + +\example + +\begin{worksheet} +\verb` val pair = ("answer", 42)` \wsf pair : (String, Int) = (answer,42) +\end{worksheet} + +The type of `pair` above is `(String, Int)`. + +Pairs can also be used as patterns: + +\begin{worksheet} +\verb` val (label, value) = pair` \wsf label : String = answer \\ + \wsn value : Int = 42 +\end{worksheet} + +This works analogously for tuples with more than two elements. + +Translation of Tuples +===================== + +A tuple type $\btt (T_1, ..., T_n)$ is an abbreviation of the +parameterized type + +$$\btt + scala.Tuple{\it n}[T_1, ..., T_n] +$$ + +A tuple expression $\btt (e_1, ..., e_n)$ +is equivalent to the function application + +$$\btt + scala.Tuple{\it n}(e_1, ..., e_n) +$$ + +A tuple pattern $\btt (p_1, ..., p_n)$ +is equivalent to the constructor pattern + +$$\btt + scala.Tuple{\it n}(p_1, ..., p_n) +$$ + +The Tuple class +=============== + +Here, all `Tuple`\mbox{\it n} classes are modeled after the following pattern: + + case class Tuple2[T1, T2](_1: +T1, _2: +T2) { + override def toString = "(" + _1 + "," + _2 +")" + } + +The fields of a tuple can be accessed with names `_1`, `_2`, ... + +So instead of the pattern binding + + val (label, value) = pair + +one could also have written: + + val label = pair._1 + val value = pair._2 + +But the pattern matching form is generally preferred. + +Exercise +======== + +The `merge` function as given uses a nested pattern match. + +This does not reflect the inherent symmetry of the merge algorithm. + +Rewrite `merge` using a pattern matching over pairs. + + def merge(xs: List[Int], ys: List[Int]): List[Int] = + (xs, ys) match { + ??? + } diff --git a/lectures/progfun1-5-3.md b/lectures/progfun1-5-3.md new file mode 100644 index 0000000000000000000000000000000000000000..e0179c514a89b04a4adb6f1bf09a288194b9259e --- /dev/null +++ b/lectures/progfun1-5-3.md @@ -0,0 +1,152 @@ +% Implicit Parameters +% +% + +Making Sort more General +======================== + +Problem: How to parameterize `msort` so that it can also be used for +lists with elements other than `Int`? + + def msort[T](xs: List[T]): List[T] = ... + +does not work, because the comparison `<` in `merge` is not defined +for arbitrary types `T`. + +\red{Idea:} Parameterize `merge` with the necessary comparison function. + +Parameterization of Sort +======================== + +The most flexible design is to make the function \verb@sort@ +polymorphic and to pass the comparison operation as an additional +parameter: + + def msort[T](xs: List[T])(lt: (T, T) => Boolean) = { + ... + merge(msort(fst)(lt), msort(snd)(lt)) + } + +Merge then needs to be adapted as follows: + + def merge(xs: List[T], ys: List[T]) = (xs, ys) match { + ... + case (x :: xs1, y :: ys1) => + if lt(x, y) then ... + else ... + } + +Calling Parameterized Sort +========================== + +We can now call `msort` as follows: + + val xs = List(-5, 6, 3, 2, 7) + val fruits = List("apple", "pear", "orange", "pineapple") + + msort(xs)((x: Int, y: Int) => x < y) + msort(fruits)((x: String, y: String) => x.compareTo(y) < 0) + +Or, since parameter types can be inferred from the call `msort(xs)`: + + msort(xs)((x, y) => x < y) + + +Parametrization with Ordering +============================= + +There is already a class in the standard library that represents orderings. + + scala.math.Ordering[T] + +provides ways to compare elements of type `T`. So instead of +parameterizing with the `lt` operation directly, we could parameterize +with `Ordering` instead: + + def msort[T](xs: List[T])(ord: Ordering[T]) = + + def merge(xs: List[T], ys: List[T]) = + ... if ord.lt(x, y) then ... + + ... merge(msort(fst)(ord), msort(snd)(ord)) ... + +Ordering Instances: +=================== + +Calling the new `msort` can be done like this: + + import math.Ordering + + msort(nums)(Ordering.Int) + msort(fruits)(Ordering.String) + +This makes use of the values `Int` and `String` defined in the +`scala.math.Ordering` object, which produce the right orderings on +integers and strings. + +Aside: Implicit Parameters +========================== + +\red{Problem:} Passing around `lt` or `ord` values is cumbersome. + +We can avoid this by making `ord` an implicit parameter. + + def msort[T](xs: List[T])(implicit ord: Ordering[T]) = + + def merge(xs: List[T], ys: List[T]) = + ... if ord.lt(x, y) then ... + + ... merge(msort(fst), msort(snd)) ... + +Then calls to `msort` can avoid the ordering parameters: + + msort(nums) + msort(fruits) + +The compiler will figure out the right implicit to pass based on the +demanded type. + +Rules for Implicit Parameters +============================= + +Say, a function takes an implicit parameter of type `T`. + +The compiler will search an implicit definition that + + - is marked `implicit` + - has a type compatible with `T` + - is visible at the point of the function call, or is defined + in a companion object associated with `T`. + +If there is a single (most specific) definition, it will be taken as +actual argument for the implicit parameter. + +Otherwise it's an error. + +Exercise: Implicit Parameters +============================= + +Consider the following line of the definition of `msort`: + + ... merge(msort(fst), msort(snd)) ... + +Which implicit argument is inserted? + + O Ordering.Int + O Ordering.String + O the "ord" parameter of "msort" + + + + + + + + + + + + + + + diff --git a/lectures/progfun1-5-4.md b/lectures/progfun1-5-4.md new file mode 100644 index 0000000000000000000000000000000000000000..6cdb8f9c818e4441f503d609fbe0f329c3627571 --- /dev/null +++ b/lectures/progfun1-5-4.md @@ -0,0 +1,162 @@ +% Higher-order List Functions +% +% + +Recurring Patterns for Computations on Lists +============================================ + + + The examples have shown that functions on lists often have + similar structures. + + We can identify several recurring patterns, like, + + - transforming each element in a list in a certain way, + - retrieving a list of all elements satisfying a criterion, + - combining the elements of a list using an operator. + +Functional languages allow programmers to write generic functions +that implement patterns such as these using \red{\em higher-order functions}. + +Applying a Function to Elements of a List +========================================= + +A common operation is to transform each element of a list and then +return the list of results. + +For example, to multiply each element of a list by the same factor, you could write: + + def scaleList(xs: List[Double], factor: Double): List[Double] = xs match { + case Nil => xs + case y :: ys => y * factor :: scaleList(ys, factor) + } + +Map +=== + + +This scheme can be generalized to the method `map` of the +`List` class. A simple way to define `map` is as follows: + + abstract class List[T] { ... + def map[U](f: T => U): List[U] = this match { + case Nil => this + case x :: xs => f(x) :: xs.map(f) + } + +(in fact, the actual definition of `map` is a bit more complicated, +because it is tail-recursive, and also because it works for arbitrary +collections, not just lists). + +Using `map`, `scaleList` can be written more concisely. + + def scaleList(xs: List[Double], factor: Double) = + xs map (x => x * factor) + +Exercise +======== + +Consider a function to square each element of a list, and +return the result. Complete the two following equivalent definitions of +`squareList`. + + + def squareList(xs: List[Int]): List[Int] = xs match { + case Nil => ??? + case y :: ys => ??? + } + + def squareList(xs: List[Int]): List[Int] = + xs map ??? + +Exercise +======== + +Consider a function to square each element of a list, and +return the result. Complete the two following equivalent definitions of +`squareList`. + + + def squareList(xs: List[Int]): List[Int] = xs match { + case Nil => + case y :: ys => + } + + def squareList(xs: List[Int]): List[Int] = + xs map + +Filtering +========= + +Another common operation on lists is the selection of all elements +satisfying a given condition. For example: + + def posElems(xs: List[Int]): List[Int] = xs match { + case Nil => xs + case y :: ys => if y > 0 then y :: posElems(ys) else posElems(ys) + } + +Filter +====== + +This pattern is generalized by the method `filter` of the `List` class: + + abstract class List[T] { + ... + def filter(p: T => Boolean): List[T] = this match { + case Nil => this + case x :: xs => if p(x) then x :: xs.filter(p) else xs.filter(p) + } + } + +Using `filter`, `posElems` can be written more concisely. + + def posElems(xs: List[Int]): List[Int] = + xs filter (x => x > 0) + +Variations of Filter +==================== + +Besides filter, there are also the following methods that extract +sublists based on a predicate: + +\begin{tabular}{lp{8cm}} + \verb` xs.filterNot(p)` & Same as \verb`xs filter (x => !p(x))`; The list consisting of those elements of \verb`xs` that do not satisfy the predicate \verb`p`. +\\ \verb` xs.partition(p)` & Same as \verb`(xs filter p, xs filterNot p)`, but computed in a single traversal of the list \verb`xs`. +\\ \verb` xs.takeWhile(p)` & The longest prefix of list \verb`xs` consisting of elements that all satisfy the predicate \verb`p`. +\\ \verb` xs.dropWhile(p)` & The remainder of the list \verb`xs` after any leading elements satisfying \verb`p` have been removed. +\\ \verb` xs.span(p)` & Same as \verb`(xs takeWhile p, xs dropWhile p)` but computed in a single traversal of the list \verb`xs`. +\end{tabular} + +Exercise +======== + +Write a function `pack` that packs consecutive duplicates of list elements into sublists. For instance, + + pack(List("a", "a", "a", "b", "c", "c", "a")) + +should give + + List(List("a", "a", "a"), List("b"), List("c", "c"), List("a")). + +You can use the following template: + + def pack[T](xs: List[T]): List[List[T]] = xs match { + case Nil => Nil + case x :: xs1 => ??? + } + +Exercise +======== + +Using `pack`, write a function `encode` that produces the run-length +encoding of a list. + +The idea is to encode `n` consecutive duplicates of an element `x` as a pair `(x, n)`. For instance, + + encode(List("a", "a", "a", "b", "c", "c", "a")) + +should give + + List(("a", 3), ("b", 1), ("c", 2), ("a", 1)). + diff --git a/lectures/progfun1-5-5.md b/lectures/progfun1-5-5.md new file mode 100644 index 0000000000000000000000000000000000000000..2e1aa8f71eea86a470b1f0b96d36ffaa7668ad1e --- /dev/null +++ b/lectures/progfun1-5-5.md @@ -0,0 +1,203 @@ +% Reduction of Lists +% +% +Reduction of Lists +================== + +Another common operation on lists is to combine the elements of a list using a given operator. + +For example: + + sum(List(x1, ..., xn)) = 0 + x1 + ... + xn + product(List(x1, ..., xn)) = 1 * x1 * ... * xn + +We can implement this with the usual recursive schema: + + def sum(xs: List[Int]): Int = xs match { + case Nil => 0 + case y :: ys => y + sum(ys) + } + +ReduceLeft +========== + +This pattern can be abstracted out using the generic method `reduceLeft`: + +`reduceLeft` inserts a given binary operator between adjacent elements +of a list: + + List(x1, ..., xn) reduceLeft op = (...(x1 op x2) op ... ) op xn + +\vspace{2cm} +Using `reduceLeft`, we can simplify: + + def sum(xs: List[Int]) = (0 :: xs) reduceLeft ((x, y) => x + y) + def product(xs: List[Int]) = (1 :: xs) reduceLeft ((x, y) => x * y) + +A Shorter Way to Write Functions +================================ + +Instead of `((x, y) => x * y))`, one can also write shorter: + + (_ * _) + +Every `_` represents a new parameter, going from left to right. + +The parameters are defined at the next outer pair of parentheses (or the whole expression if there are no enclosing parentheses). + +So, `sum` and `product` can also be expressed like this: + + def sum(xs: List[Int]) = (0 :: xs) reduceLeft (_ + _) + def product(xs: List[Int]) = (1 :: xs) reduceLeft (_ * _) + +FoldLeft +======== + +The function `reduceLeft` is defined in terms of a more general function, `foldLeft`. + +`foldLeft` is like `reduceLeft` but takes an \red{accumulator}, `z`, as an additional parameter, which is returned when `foldLeft` is called on an empty list. + + (List(x1, ..., xn) foldLeft z)(op) = (...(z op x1) op ... ) op xn + +\vspace{1.7cm} + +So, `sum` and `product` can also be defined as follows: + + def sum(xs: List[Int]) = (xs foldLeft 0) (_ + _) + def product(xs: List[Int]) = (xs foldLeft 1) (_ * _) + +Implementations of ReduceLeft and FoldLeft +========================================== + +`foldLeft` and `reduceLeft` can be implemented in class `List` as follows. + + abstract class List[T] { ... + def reduceLeft(op: (T, T) => T): T = this match { + case Nil => throw new Error("Nil.reduceLeft") + case x :: xs => (xs foldLeft x)(op) + } + def foldLeft[U](z: U)(op: (U, T) => U): U = this match { + case Nil => z + case x :: xs => (xs foldLeft op(z, x))(op) + } + } + +FoldRight and ReduceRight +========================= + +Applications of `foldLeft` and `reduceLeft` unfold on trees that lean to the left. + +They have two dual functions, `foldRight` and `reduceRight`, which +produce trees which lean to the right, i.e., + + List(x1, ..., x{n-1}, xn) reduceRight op = x1 op ( ... (x{n-1} op xn) ... ) + (List(x1, ..., xn) foldRight acc)(op) = x1 op ( ... (xn op acc) ... ) + +Implementation of FoldRight and ReduceRight +=========================================== + +They are defined as follows + + def reduceRight(op: (T, T) => T): T = this match { + case Nil => throw new Error("Nil.reduceRight") + case x :: Nil => x + case x :: xs => op(x, xs.reduceRight(op)) + } + def foldRight[](z: U)(op: (T, U) => U): U = this match { + case Nil => z + case x :: xs => op(x, (xs foldRight z)(op)) + } + +Difference between FoldLeft and FoldRight +========================================= + +For operators that are associative and commutative, `foldLeft` and `foldRight` are equivalent (even though there may be a difference in efficiency). + +But sometimes, only one of the two operators is appropriate. + +Exercise +======== + +Here is another formulation of `concat`: + + def concat[T](xs: List[T], ys: List[T]): List[T] = + (xs foldRight ys) (_ :: _) + +Here, it isn't possible to replace `foldRight` by `foldLeft`. Why? + + O The types would not work out + O The resulting function would not terminate + O The result would be reversed + +Back to Reversing Lists +======================= + +We now develop a function for reversing lists which has a linear cost. + +The idea is to use the operation `foldLeft`: + + def reverse[T](xs: List[T]): List[T] = (xs foldLeft z?)(op?) + +All that remains is to replace the parts `z?` and `op?`. + +Let's try to \red{compute} them from examples. + +Deduction of Reverse (1) +======================== + +To start computing `z?`, let's consider `reverse(Nil)`. + +We know `reverse(Nil) == Nil`, so we can compute as follows: + + Nil +-> + = reverse(Nil) +-> + = (Nil foldLeft z?)(op) +-> + = z? + +Consequently, `z? = List()` + +Deduction of Reverse (2) +======================== + +We still need to compute `op?`. To do that let's plug in the next simplest +list after `Nil` into our equation for `reverse`: + + List(x) +-> + = reverse(List(x)) +-> + = (List(x) foldLeft Nil)(op?) +-> + = op?(Nil, x) + +Consequently, `op?(Nil, x) = List(x) = x :: List()`. + +This suggests to take for `op?` the operator `::` but with its operands swapped. + +Deduction of Reverse(3) +======================= + +We thus arrive at the following implementation of `reverse`. + + def reverse[a](xs: List[T]): List[T] = + (xs foldLeft List[T]())((xs, x) => x :: xs) + +Remark: the type parameter in `List[T]()` is necessary for type inference. + +\question: What is the complexity of this implementation of `reverse` ? + + +Exercise +======== + +Complete the following definitions of the basic functions `map` and `length` on lists, such that their implementation uses `foldRight`: + + def mapFun[T, U](xs: List[T], f: T => U): List[U] = + (xs foldRight List[U]())( ??? ) + + def lengthFun[T](xs: List[T]): Int = + (xs foldRight 0)( ??? ) + diff --git a/lectures/progfun1-5-6.md b/lectures/progfun1-5-6.md new file mode 100644 index 0000000000000000000000000000000000000000..65161b0bf817168aa569e405069e23b536440db4 --- /dev/null +++ b/lectures/progfun1-5-6.md @@ -0,0 +1,172 @@ +% Reasoning About Lists +% +% + +Laws of Concat +================== + +Recall the concatenation operation `++` on lists. + +We would like to verify that concatenation is associative, and that it admits the empty list \verb@Nil@ as neutral element to the left and to the right: + + (xs ++ ys) ++ zs = xs ++ (ys ++ zs) + xs ++ Nil = xs + Nil ++ xs = xs + +\red{Q}: How can we prove properties like these? +-> +\red{A}: By \red{structural induction} on lists. + + +Reminder: Natural Induction +=========================== + +Recall the principle of proof by \red{natural induction}: + +To show a property $\btt P(n)$ for all the integers $\btt n \geq b$, + + - Show that we have $\btt P(b)$ (\red{base case}), + - for all integers $\btt n \geq b$ show the \red{induction step}: +\begin{quote} + if one has $\btt P(n)$, then one also has $\btt P(n+1)$. +\end{quote} + +Example +======= + +Given: + + def factorial(n: Int): Int = + if n == 0 then 1 // 1st clause + else n * factorial(n-1) // 2nd clause + +Show that, for all `n >= 4` + + factorial(n) >= power(2, n) + +Base Case +========= + +\BaseCase{{\tt 4}} + +This case is established by simple calculations: + + factorial(4) = 24 >= 16 = power(2, 4) + +Induction Step +============== + +\InductionStep{\tt n+1} + +We have for `n >= 4`: + + factorial(n + 1) +-> + >= (n + 1) * factorial(n) // by 2nd clause in factorial +-> + > 2 * factorial(n) // by calculating +-> + >= 2 * power(2, n) // by induction hypothesis +-> + = power(2, n + 1) // by definition of power + +Referential Transparency +======================== + +Note that a proof can freely apply reduction steps as equalities to some +part of a term. + +That works because pure functional programs don't have side effects; +so that a term is equivalent to the term to which it reduces. + +This principle is called \red{referential transparency}. + + +Structural Induction +==================== + +The principle of structural induction is analogous to natural induction: + +To prove a property $\btt P(xs)$ for all lists $\btt xs$, + + - show that $\btt P(Nil)$ holds (\red{base case}), + - for a list $\btt xs$ and some element $\btt x$, show the \red{induction step}: +\begin{quote} + if $\btt P(xs)$ holds, then $\btt P(x :: xs)$ also holds. +\end{quote} + +Example +======= + +Let's show that, for lists `xs`, `ys`, `zs`: + + (xs ++ ys) ++ zs = xs ++ (ys ++ zs) + +To do this, use structural induction on `xs`. From the previous implementation of `concat`, + + def concat[T](xs: List[T], ys: List[T]) = xs match { + case List() => ys + case x :: xs1 => x :: concat(xs1, ys) + } + +distill two _defining clauses_ of `++`: + + Nil ++ ys = ys // 1st clause + (x :: xs1) ++ ys = x :: (xs1 ++ ys) // 2nd clause + + +Base Case +========= + +\BaseCase{\tt Nil} + +For the left-hand side we have: + + (Nil ++ ys) ++ zs +-> + = ys ++ zs // by 1st clause of ++ +-> +For the right-hand side, we have: + + Nil ++ (ys ++ zs) +-> + = ys ++ zs // by 1st clause of ++ + +This case is therefore established. + +Induction Step: LHS +=================== + +\InductionStep{\tt x :: xs} + +For the left-hand side, we have: + + ((x :: xs) ++ ys) ++ zs +-> + = (x :: (xs ++ ys)) ++ zs // by 2nd clause of ++ +-> + = x :: ((xs ++ ys) ++ zs) // by 2nd clause of ++ +-> + = x :: (xs ++ (ys ++ zs)) // by induction hypothesis + +Induction Step: RHS +=================== + +For the right hand side we have: + + (x :: xs) ++ (ys ++ zs) +-> + = x :: (xs ++ (ys ++ zs)) // by 2nd clause of ++ + +So this case (and with it, the property) is established. + +Exercise +======== + +Show by induction on `xs` that `xs ++ Nil = xs`. + +How many equations do you need for the inductive step? + + O 2 + O 3 + O 4 \ No newline at end of file diff --git a/lectures/progfun1-5-7.md b/lectures/progfun1-5-7.md new file mode 100644 index 0000000000000000000000000000000000000000..68687b4d4cd1d13c89e9acbe2d74345c662cc7a7 --- /dev/null +++ b/lectures/progfun1-5-7.md @@ -0,0 +1,102 @@ +% A Larger Equational Proof on Lists +% +% + +A Law of Reverse +================ + +For a more difficult example, let's consider the `reverse` function. + +We pick its inefficient definition, because its more amenable to equational proofs: + + Nil.reverse = Nil // 1st clause + (x :: xs).reverse = xs.reverse ++ List(x) // 2nd clause + +We'd like to prove the following proposition + + xs.reverse.reverse = xs + +Proof +===== + +By induction on \verb@xs@. The base case is easy: + + Nil.reverse.reverse + = Nil.reverse // by 1st clause of reverse + = Nil // by 1st clause of reverse +-> +For the induction step, let's try: + + (x :: xs).reverse.reverse + = (xs.reverse ++ List(x)).reverse // by 2nd clause of reverse +-> +We can't do anything more with this expression, therefore we turn to the right-hand side: + + x :: xs + = x :: xs.reverse.reverse // by induction hypothesis + +Both sides are simplified in different expressions. + +To Do +===== + +We still need to show: + + (xs.reverse ++ List(x)).reverse = x :: xs.reverse.reverse + +Trying to prove it directly by induction doesn't work. + +We must instead try to \red{generalize} the equation. For \red{any} list `ys`, + + (ys ++ List(x)).reverse = x :: ys.reverse + +This equation can be proved by a second induction argument on `ys`. + + +Auxiliary Equation, Base Case +============================= + + (Nil ++ List(x)).reverse // to show: = x :: Nil.reverse +-> + = List(x).reverse // by 1st clause of ++ +-> + = (x :: Nil).reverse // by definition of List +-> + = Nil ++ (x :: Nil) // by 2nd clause of reverse +-> + = x :: Nil // by 1st clause of ++ +-> + = x :: Nil.reverse // by 1st clause of reverse + + + +Auxiliary Equation, Inductive Step +================================== + + ((y :: ys) ++ List(x)).reverse // to show: = x :: (y :: ys).reverse +-> + = (y :: (ys ++ List(x))).reverse // by 2nd clause of ++ +-> + = (ys ++ List(x)).reverse ++ List(y) // by 2nd clause of reverse +-> + = (x :: ys.reverse) ++ List(y) // by the induction hypothesis +-> + = x :: (ys.reverse ++ List(y)) // by 1st clause of ++ +-> + = x :: (y :: ys).reverse // by 2nd clause of reverse + +This establishes the auxiliary equation, and with it the main proposition. + +Exercise (Open-Ended, Harder) +============================= + +Prove the following distribution law for `map` over concatenation. + +For any lists `xs`, `ys`, function `f`: + + (xs ++ ys) map f = (xs map f) ++ (ys map f) + +You will need the clauses of ++ as well as the following clauses for `map`: + + Nil map f = Nil + (x :: xs) map f = f(x) :: (xs map f) diff --git a/lectures/progfun1-6-1.md b/lectures/progfun1-6-1.md new file mode 100644 index 0000000000000000000000000000000000000000..5e2eb4ab24bcb7d7b90f2ceea00f8fbd133e91c8 --- /dev/null +++ b/lectures/progfun1-6-1.md @@ -0,0 +1,154 @@ +% Other Collections +% +% +Other Sequences +=============== + +We have seen that lists are _linear_: 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, `Vector`. + +This one has more evenly balanced access patterns than `List`. + + + +Operations on Vectors +===================== + +Vectors are created analogously to lists: + + val nums = Vector(1, 2, 3, -88) + val people = Vector("Bob", "James", "Peter") + +They support the same operations as lists, with the exception of `::` + +Instead of `x :: xs`, there is + +\begin{tabular}{lp{8.7cm}} + \verb` x +: xs` & Create a new vector with leading element \verb`x`, followed by all elements of \verb`xs`. +\\ \verb` xs :+ x` & Create a new vector with trailing element \verb`x`, preceded by all elements of \verb`xs`. +\end{tabular} +(Note that the `:` always points to the sequence.) + + +Collection Hierarchy +==================== + +A common base class of `List` and `Vector` is `Seq`, the class of all _sequences_. + +`Seq` itself is a subclass of `Iterable`. + + + + + +Arrays and Strings +================== + +Arrays and Strings support the same operations as `Seq` and can +implicitly be converted to sequences where needed. + +(They cannot be subclasses of `Seq` because they come from Java) + + val xs: Array[Int] = Array(1, 2, 3) + xs map (x => 2 * x) + + val ys: String = "Hello world!" + ys filter (_.isUpper) + +Ranges +====== + +Another simple kind of sequence is the _range_. + +It represents a sequence of evenly spaced integers. + +Three operators: + +`to` (inclusive), `until` (exclusive), `by` (to +determine step value): + + val r: Range = 1 until 5 + val s: Range = 1 to 5 + 1 to 10 by 3 + 6 to 1 by -2 + +Ranges a represented as single objects with three fields: +lower bound, upper bound, step value. + +Some more Sequence Operations: +============================== + +\begin{tabular}{lp{8.7cm}} + \verb` xs exists p ` & \verb`true` if there is an element \verb`x` of \verb`xs` such that \verb`p(x)` holds, \verb`false` otherwise. +\\ \verb` xs forall p ` & \verb`true` if \verb`p(x)` holds for all elements \verb`x` of \verb`xs`, \verb`false` otherwise. +\\ \verb` xs zip ys ` & A sequence of pairs drawn from corresponding elements of sequences \verb`xs` and \verb`ys`. +\\ \verb` xs.unzip ` & Splits a sequence of pairs \verb`xs` into two sequences consisting of the first, respectively second halves of all pairs. +\\ \verb` xs.flatMap f ` & Applies collection-valued function \verb`f` to all elements of \verb`xs` and concatenates the results +\\ \verb` xs.sum ` & The sum of all elements of this numeric collection. +\\ \verb` xs.product ` & The product of all elements of this numeric collection +\\ \verb` xs.max ` & The maximum of all elements of this collection (an \verb`Ordering` must exist) +\\ \verb` xs.min ` & The minimum of all elements of this collection +\end{tabular} + +Example: Combinations +===================== + +To list all combinations of numbers `x` and `y` where `x` is drawn from `1..M` and `y` is drawn from `1..N`: + + (1 to M) flatMap (x => + +Example: Combinations +===================== + +To list all combinations of numbers `x` and `y` where `x` is drawn from `1..M` and `y` is drawn from `1..N`: + + (1 to M) flatMap (x => (1 to N) map (y => (x, y))) + +Example: Scalar Product +======================= + +To compute the scalar product of two vectors: + + def scalarProduct(xs: Vector[Double], ys: Vector[Double]): Double = + (xs zip ys).map(xy => xy._1 * xy._2).sum +-> +An alternative way to write this is with a \red{pattern matching function value}. + + def scalarProduct(xs: Vector[Double], ys: Vector[Double]): Double = + (xs zip ys).map{ case (x, y) => x * y }.sum + +Generally, the function value + + { case p1 => e1 ... case pn => en } + +is equivalent to + + x => x match { case p1 => e1 ... case pn => en } + +Exercise: +========= + +A number `n` is \red{prime} if the only divisors of `n` are `1` and `n` itself. + +What is a high-level way to write a test for primality of numbers? For +once, value conciseness over efficiency. + + def isPrime(n: Int): Boolean = ??? + + +Exercise: +========= + +A number `n` is \red{prime} if the only divisors of `n` are `1` and `n` itself. + +What is a high-level way to write a test for primality of numbers? For +once, value conciseness over efficiency. + + def isPrime(n: Int): Boolean = + +\quiz + + + diff --git a/lectures/progfun1-6-2.md b/lectures/progfun1-6-2.md new file mode 100644 index 0000000000000000000000000000000000000000..2fbe678689582d8453fc62ae8c15989e2b9d4b7b --- /dev/null +++ b/lectures/progfun1-6-2.md @@ -0,0 +1,155 @@ +% Combinatorial Search and For-Expressions +% +% +Handling Nested Sequences +========================= + +We can extend the usage of higher order functions on sequences to many +calculations which are usually expressed using nested loops. + +\example: Given a positive integer `n`, find all pairs of positive integers `i` and `j`, with `1 <= j < i < n` such that `i + j` is prime. + +For example, if `n = 7`, the sought pairs are + +$$\ba{c|lllllll} +i & 2 & 3 & 4 & 4 & 5 & 6 & 6\\ +j & 1 & 2 & 1 & 3 & 2 & 1 & 5\\ \hline +i + j & 3 & 5 & 5 & 7 & 7 & 7 & 11 +\ea$$ + +Algorithm +========= + +A natural way to do this is to: + +- Generate the sequence of all pairs of integers `(i, j)` such that `1 <= j < i < n`. +- Filter the pairs for which `i + j` is prime. + +One natural way to generate the sequence of pairs is to: + +- Generate all the integers `i` between `1` and `n` (excluded). +- For each integer `i`, generate the list of pairs `(i, 1), ..., (i, i-1)`. + +This can be achieved by combining `until` and `map`: + + (1 until n) map (i => + (1 until i) map (j => (i, j))) + + +Generate Pairs +============== + +The previous step gave a sequence of sequences, let's call it `xss`. + +We can combine all the sub-sequences using `foldRight` with `++`: + + (xss foldRight Seq[Int]())(_ ++ _) + +Or, equivalently, we use the built-in method `flatten` + + xss.flatten + +This gives: + + ((1 until n) map (i => + (1 until i) map (j => (i, j)))).flatten + +Generate Pairs (2) +================== + +Here's a useful law: + + xs flatMap f = (xs map f).flatten + +Hence, the above expression can be simplified to + + (1 until n) flatMap (i => + (1 until i) map (j => (i, j))) + + +Assembling the pieces +===================== + +By reassembling the pieces, we obtain the following expression: + + (1 until n) flatMap (i => + (1 until i) map (j => (i, j))) filter ( pair => + isPrime(pair._1 + pair._2)) + +This works, but makes most people's head hurt. + +Is there a simpler way? + + +For-Expressions +=============== + +Higher-order functions such as `map`, `flatMap` or `filter` provide powerful constructs for manipulating lists. + +But sometimes the level of abstraction required by these function make the program difficult to understand. + +In this case, Scala's `for` expression notation can help. + +For-Expression Example +====================== + +Let `persons` be a list of elements of class `Person`, with fields `name` and `age`. + + case class Person(name: String, age: Int) + +To obtain the names of persons over 20 years old, you can write: + + for ( p <- persons if p.age > 20 ) yield p.name + +which is equivalent to: + + persons filter (p => p.age > 20) map (p => p.name) + +The for-expression is similar to loops in imperative languages, except +that it builds a list of the results of all iterations. + +Syntax of For +============= + +A for-expression is of the form + + for ( s ) yield e + +where `s` is a sequence of \red{generators} and \red{filters}, +and `e` is an expression whose value is returned by an iteration. + +- A \red{generator} is of the form `p <- e`, + where `p` is a pattern and `e` an expression whose value is a collection. +- A \red{filter} is of the form `if f` where `f` is a boolean expression. +- The sequence must start with a generator. +- If there are several generators in the sequence, the last generators vary faster than the first. + +Instead of `( s )`, braces `{ s }` can also be used, and then the +sequence of generators and filters can be written on multiple lines +without requiring semicolons. + +Use of For +========== + +Here are two examples which were previously solved with higher-order functions: + +Given a positive integer `n`, find all the pairs of positive integers +`(i, j)` such that `1 <= j < i < n`, and `i + j` is prime. + + for { + i <- 1 until n + j <- 1 until i + if isPrime(i + j) + } yield (i, j) + + +Exercise +======== + +Write a version of `scalarProduct` (see last session) that makes use of +a `for`: + + def scalarProduct(xs: List[Double], ys: List[Double]) : Double = +-> + (for ((x, y) <- xs zip ys) yield x * y).sum +\quiz diff --git a/lectures/progfun1-6-3.md b/lectures/progfun1-6-3.md new file mode 100644 index 0000000000000000000000000000000000000000..826285276a86787614606223ab895a06f56db69d --- /dev/null +++ b/lectures/progfun1-6-3.md @@ -0,0 +1,87 @@ +% Combinatorial Search Example +% +% +Sets +==== + +Sets are another basic abstraction in the Scala collections. + +A set is written analogously to a sequence: + + val fruit = Set("apple", "banana", "pear") + val s = (1 to 6).toSet + +Most operations on sequences are also available on sets: + + s.map(_ + 2) + fruit.filter(_.startsWith == "app") + s.nonEmpty + +(see `Iterable`s Scaladoc for a list of all supported operations) + +Sets vs Sequences +================= + +The principal differences between sets and sequences are: + +$1.$ Sets are unordered; the elements of a set do not have a predefined order in which they appear in the set + +$2.$ sets do not have duplicate elements: + + s.map(_ / 2 ) // Set(2, 0, 3, 1) + +$3.$ The fundamental operation on sets is `contains`: + + s.contains(5) // true + +Example: N-Queens +================= + +The eight queens problem is to place eight queens on a chessboard so that no queen is threatened by another. + +- In other words, there can't be two queens in the same row, column, or diagonal. + +We now develop a solution for a chessboard of any size, not just 8. + +One way to solve the problem is to place a queen on each row. + +Once we have placed `k - 1` queens, one must place the `k`th queen in a column where it's not ``in check" with any other queen on the board. + +Algorithm +========= + +We can solve this problem with a recursive algorithm: + +- Suppose that we have already generated all the solutions consisting of placing `k-1` queens on a board of size `n`. +- Each solution is represented by a list (of length `k-1`) containing the numbers of columns (between `0` and `n-1`). +- The column number of the queen in the `k-1`th row comes first in the list, followed by the column number of the queen in row `k-2`, etc. +- The solution set is thus represented as a set of lists, with one element for each solution. +- Now, to place the `k`th queen, we generate all possible extensions of each solution preceded by a new queen: + +Implementation +============== + + def queens(n: Int) = + def placeQueens(k: Int): Set[List[Int]] = + if k == 0 Set(List()) + else + for + queens <- placeQueens(k - 1) + col <- 0 until n + if isSafe(col, queens) + yield + col :: queens + placeQueens(n) + +Exercise +======== + +Write a function + + def isSafe(col: Int, queens: List[Int]): Boolean + +which tests if a queen placed in an indicated column `col` is secure amongst the other placed queens. + +It is assumed that the new queen is placed in the next available row after the other placed queens (in other words: in row `queens.length`). + +\quiz diff --git a/lectures/progfun1-6-4.md b/lectures/progfun1-6-4.md new file mode 100644 index 0000000000000000000000000000000000000000..dfcc4c813042d9ef75c2b016a6761a25bb5341b2 --- /dev/null +++ b/lectures/progfun1-6-4.md @@ -0,0 +1,193 @@ +% Maps +% +% +Map +=== + +Another fundamental collection type is the \red{map}. + +A map of type `Map[Key, Value]` is a data structure +that associates keys of type `Key` with values of type `Value`. + +Examples: + + val romanNumerals = Map("I" -> 1, "V" -> 5, "X" -> 10) + val capitalOfCountry = Map("US" -> "Washington", "Switzerland" -> "Bern") + +Maps are Iterables +================== + +Class `Map[Key, Value]` extends the collection type \newline +`Iterable[(Key, Value)]`. + +Therefore, maps support the same collection operations as other iterables do. Example: + + val countryOfCapital = capitalOfCountry map { + case(x, y) => (y, x) + } // Map("Washington" -> "US", "Bern" -> "Switzerland") + +Note that maps extend iterables of key/value _pairs_. + +In fact, the syntax `key -> value` is just an alternative way to write the pair `(key, value)`. + + +Maps are Functions +================== + +Class `Map[Key, Value]` also extends the function type `Key => Value`, so maps can be used everywhere functions can. + +In particular, maps can be applied to key arguments: + + capitalOfCountry("US") // "Washington" + + +Querying Map +============ + +Applying a map to a non-existing key gives an error: + + capitalOfCountry("Andorra") + // java.util.NoSuchElementException: key not found: Andorra + +To query a map without knowing beforehand whether it contains a given key, you can use the `get` operation: + + capitalOfCountry get "US" // Some("Washington") + capitalOfCountry get "Andorra" // None + +The result of a `get` operation is an `Option` value. + +The `Option` Type +================= + +The `Option` type is defined as: + + trait Option[+A] + case class Some[+A](value: A) extends Option[A] + object None extends Option[Nothing] + +The expression `map get key` returns + +- `None` \gap if `map` does not contain the given `key`, +- `Some(x)` \ \ if `map` associates the given `key` with the value `x`. + +Decomposing Option +================== + +Since options are defined as case classes, they can be decomposed +using pattern matching: + + def showCapital(country: String) = capitalOfCountry.get(country) match { + case Some(capital) => capital + case None => "missing data" + } + + showCapital("US") // "Washington" + showCapital("Andorra") // "missing data" + +Options also support quite a few operations of the other collections. + +I invite you to try them out! + +Sorted and GroupBy +================== + +Two useful operation of SQL queries in addition to for-expressions are +`groupBy` and `orderBy`. + +`orderBy` on a collection can be expressed by `sortWith` and `sorted`. + + val fruit = List("apple", "pear", "orange", "pineapple") + fruit sortWith (_.length < _.length) // List("pear", "apple", "orange", "pineapple") + fruit.sorted // List("apple", "orange", "pear", "pineapple") + +`groupBy` is available on Scala collections. It partitions a collection +into a map of collections according to a _discriminator function_ `f`. + +\example: + + fruit groupBy (_.head) //> Map(p -> List(pear, pineapple), + //| a -> List(apple), + //| o -> List(orange)) +Map Example +=========== + +A polynomial can be seen as a map from exponents to coefficients. + +For instance, $\btt x^3 - 2x + 5$ can be represented with the map. + + Map(0 -> 5, 1 -> -2, 3 -> 1) + +Based on this observation, let's design a class `Polynom` that represents polynomials as maps. + +Default Values +============== + +So far, maps were \red{partial functions}: Applying a map to a key +value in `map(key)` could lead to an exception, if the key was not stored +in the map. + +There is an operation `withDefaultValue` that turns a map into a +total function: + + val cap1 = capitalOfCountry withDefaultValue "<unknown>" + cap1("Andorra") // "<unknown>" + +Variable Length Argument Lists +============================== + +It's quite inconvenient to have to write + + Polynom(Map(1 -> 2.0, 3 -> 4.0, 5 -> 6.2)) + +Can one do without the `Map(...)`? + +Problem: The number of `key -> value` pairs passed to `Map` can vary. +-> +We can accommodate this pattern using a \red{repeated parameter}: + + def Polynom(bindings: (Int, Double)*) = + new Polynom(bindings.toMap withDefaultValue 0) + + Polynom(1 -> 2.0, 3 -> 4.0, 5 -> 6.2) + +Inside the `Polynom` function, `bindings` is seen as a +`Seq[(Int, Double)]`. + +Final Implementation of Polynom +=============================== + + class Poly(terms0: Map[Int, Double]) { + def this(bindings: (Int, Double)*) = this(bindings.toMap) + val terms = terms0 withDefaultValue 0.0 + def + (other: Poly) = new Poly(terms ++ (other.terms map adjust)) + def adjust(term: (Int, Double)): (Int, Double) = { + val (exp, coeff) = term + exp -> (coeff + terms(exp)) + } + + override def toString = + (for ((exp, coeff) <- terms.toList.sorted.reverse) + yield coeff+"x^"+exp) mkString " + " + } + +Exercise +======== + +The `+` operation on `Poly` used map concatenation with `++`. +Design another version of `+` in terms of `foldLeft`: + + def + (other: Poly) = + new Poly((other.terms foldLeft ???)(addTerm) + + def addTerm(terms: Map[Int, Double], term: (Int, Double)) = + ??? + +Which of the two versions do you believe is more efficient? + + O The version using ++ + O The version using foldLeft + +-> + +\quiz + diff --git a/lectures/progfun1-6-5.md b/lectures/progfun1-6-5.md new file mode 100644 index 0000000000000000000000000000000000000000..1b3d92a4eaba7af58d146cbc5b9153890151d54c --- /dev/null +++ b/lectures/progfun1-6-5.md @@ -0,0 +1,51 @@ +% Putting the Pieces Together +% +% +Task +==== + +Phone keys have mnemonics assigned to them. + + val mnemonics = Map( + '2' -> "ABC", '3' -> "DEF", '4' -> "GHI", '5' -> "JKL", + '6' -> "MNO", '7' -> "PQRS", '8' -> "TUV", '9' -> "WXYZ") + +Assume you are given a dictionary `words` as a list of words. + +Design a method `translate` such that + + translate(phoneNumber) + +produces all phrases of words +that can serve as mnemonics for the phone number. + +\example: The phone number "7225247386" should have the mnemonic +`Scala is fun` as one element of the set of solution phrases. + +Background +========== + +This example was taken from: +\begin{quote} +Lutz Prechelt: An Empirical Comparison of Seven Programming Languages. IEEE Computer 33(10): 23-29 (2000) +\end{quote} + +Tested with Tcl, Python, Perl, Rexx, Java, C++, C. + +Code size medians: + +- 100 loc for scripting languages +- 200-300 loc for the others + +The Future? +=========== + +Scala's immutable collections are: + +- _easy to use_: few steps to do the job. +- _concise_: one word replaces a whole loop. +- _safe_: type checker is really good at catching errors. +- _fast_: collection ops are tuned, can be parallelized. +- _universal_: one vocabulary to work on all kinds of collections. + +This makes them a very attractive tool for software development diff --git a/lectures/progfun2-1-1.md b/lectures/progfun2-1-1.md new file mode 100644 index 0000000000000000000000000000000000000000..333e92de9edde53381210044ee3c48036f9a356e --- /dev/null +++ b/lectures/progfun2-1-1.md @@ -0,0 +1,118 @@ +% Queries with For +% +% +Queries with `for` +================== + +The for notation is essentially equivalent to the common operations of +query languages for databases. + +\example: Suppose that we have a database `books`, represented as a list of books. + + case class Book(title: String, authors: List[String]) + +A Mini-Database +=============== + + val books: List[Book] = List( + Book(title = "Structure and Interpretation of Computer Programs", + authors = List("Abelson, Harald", "Sussman, Gerald J.")), + Book(title = "Introduction to Functional Programming", + authors = List("Bird, Richard", "Wadler, Phil")), + Book(title = "Effective Java", + authors = List("Bloch, Joshua")), + Book(title = "Java Puzzlers", + authors = List("Bloch, Joshua", "Gafter, Neal")), + Book(title = "Programming in Scala", + authors = List("Odersky, Martin", "Spoon, Lex", "Venners, Bill"))) + +Some Queries +============ + +To find the titles of books whose author's name is "Bird": + + for (b <- books; a <- b.authors if a startsWith "Bird,") + yield b.title + +To find all the books which have the word ``Program'' in the title: + + for (b <- books if b.title indexOf "Program" >= 0) + yield b.title + +Another Query +============= + +To find the names of all authors who have written at least two books present in the database. + + for { + b1 <- books + b2 <- books + if b1 != b2 + a1 <- b1.authors + a2 <- b2.authors + if a1 == a2 + } yield a1 +-> +Why do solutions show up twice? + +How can we avoid this? + +Modified Query +============== + +To find the names of all authors who have written at least two books present in the database. + + for { + b1 <- books + b2 <- books + if b1.title < b2.title + a1 <- b1.authors + a2 <- b2.authors + if a1 == a2 + } yield a1 + +Problem +======= + +What happens if an author has published three books? + + O The author is printed once + O The author is printed twice + O The author is printed three times + O The author is not printed at all + +-> +\quiz + +Modified Query (2) +================== + +\red{Solution}: We must remove duplicate authors who are in the results list twice. + +This is achieved using the `distinct` method on sequences: + + { for { + b1 <- books + b2 <- books + if b1.title < b2.title + a1 <- b1.authors + a2 <- b2.authors + if a1 == a2 + } yield a1 + }.distinct + +Modified Query +============== + +\red{Better alternative}: Compute with sets instead of sequences: + + val bookSet = books.toSet + for { + b1 <- bookSet + b2 <- bookSet + if b1 != b2 + a1 <- b1.authors + a2 <- b2.authors + if a1 == a2 + } yield a1 + diff --git a/lectures/progfun2-1-2.md b/lectures/progfun2-1-2.md new file mode 100644 index 0000000000000000000000000000000000000000..0df769a1c533473da770172ba8c91e1fdc39db50 --- /dev/null +++ b/lectures/progfun2-1-2.md @@ -0,0 +1,123 @@ +% Translation of For +% +% +For-Expressions and Higher-Order Functions +========================================== + +The syntax of `for` is closely related to the higher-order functions `map`, `flatMap` and `filter`. + +First of all, these functions can all be defined in terms of `for`: + + def mapFun[T, U](xs: List[T], f: T => U): List[U] = + for (x <- xs) yield f(x) + + def flatMap[T, U](xs: List[T], f: T => Iterable[U]): List[U] = + for (x <- xs; y <- f(x)) yield y + + def filter[T](xs: List[T], p: T => Boolean): List[T] = + for (x <- xs if p(x)) yield x + +Translation of For (1) +====================== + +In reality, the Scala compiler expresses for-expressions in terms of `map`, `flatMap` and a lazy variant of `filter`. + +Here is the translation scheme used by the compiler (we limit ourselves here to simple variables in generators) + +$1.$ A simple for-expression + + for (x <- e1) yield e2 + +is translated to + + e1.map(x => e2) + +Translation of For (2) +====================== + +$2.$ A for-expression + + for (x <- e1 if f; s) yield e2 + +where `f` is a filter and `s` is a (potentially empty) sequence of generators and filters, is translated to + + for (x <- e1.withFilter(x => f); s) yield e2 + +(and the translation continues with the new expression) + +You can think of `withFilter` as a variant of `filter` that does not +produce an intermediate list, but instead filters the following `map` or `flatMap` function application. + +Translation of For (3) +====================== + +$3.$ A for-expression + + for (x <- e1; y <- e2; s) yield e3 + +where `s` is a (potentially empty) sequence of generators and filters, is translated into + + e1.flatMap(x => for (y <- e2; s) yield e3) + +(and the translation continues with the new expression) + +Example +======= + +Take the for-expression that computed pairs whose sum is prime: + + for { + i <- 1 until n + j <- 1 until i + if isPrime(i + j) + } yield (i, j) + +Applying the translation scheme to this expression gives: + + (1 until n).flatMap(i => + (1 until i).withFilter(j => isPrime(i+j)) + .map(j => (i, j))) + +This is almost exactly the expression which we came up with first! + +Exercise +======== + +Translate + + for (b <- books; a <- b.authors if a startsWith "Bird") + yield b.title + +into higher-order functions. + +-> +\quiz + + +Generalization of `for` +======================= + +Interestingly, the translation of for is not limited to lists or +sequences, or even collections; + +It is based solely on the presence of +the methods `map`, `flatMap` and `withFilter`. + +This lets you use the for syntax for your own types as well -- you must only define `map`, `flatMap` and `withFilter` for these types. + +There are many types for which this is useful: arrays, iterators, databases, XML data, optional values, parsers, etc. + +For and Databases +================= + +For example, `books` might not be a list, but a database stored on some server. + +As long as the client interface to the database defines the methods `map`, `flatMap` and `withFilter`, we can use the `for` syntax for querying the database. + +This is the basis of the Scala data base connection frameworks +ScalaQuery and Slick. + +Similar ideas underly Microsoft's LINQ. + + + diff --git a/lectures/progfun2-1-3.md b/lectures/progfun2-1-3.md new file mode 100644 index 0000000000000000000000000000000000000000..d503314a5530292ad8b43ed8fc0e446b28257924 --- /dev/null +++ b/lectures/progfun2-1-3.md @@ -0,0 +1,272 @@ +% Functional Random Generators +% Martin Odersky +% + +Other Uses of For-Expressions +============================= + +Operations of sets, or databases, or options. + +\red{Question:} Are for-expressions tied to collections? + +\red{Answer:} No! All that is required is some interpretation of +`map`, `flatMap` and `withFilter`. + +There are many domains outside collections that afford such an +interpretation. + +Example: random value generators. + +Random Values +============= + +You know about random numbers: + + import java.util.Random + val rand = new Random + rand.nextInt() + +Question: What is a systematic way to get random values for other domains, such as + + - booleans, strings, pairs and tuples, lists, sets, trees + +? + +Generators +========== + +Let's define a trait `Generator[T]` that generates random values of type `T`: + + trait Generator[+T] { + def generate: T + } + +Some instances: + + val integers = new Generator[Int] { + val rand = new java.util.Random + def generate = rand.nextInt() + } + +Generators +========== + +Let's define a trait `Generator[T]` that generates random values of type `T`: + + trait Generator[+T] { + def generate: T + } + +Some instances: + + val booleans = new Generator[Boolean] { + def generate = integers.generate > 0 + } + +Generators +========== + +Let's define a trait `Generator[T]` that generates random values of type `T`: + + trait Generator[+T] { + def generate: T + } + +Some instances: + + val pairs = new Generator[(Int, Int)] { + def generate = (integers.generate, integers.generate) + } + +Streamlining It +=============== + +Can we avoid the `new Generator ... ` boilerplate? + +Ideally, we would like to write: + + val booleans = for (x <- integers) yield x > 0 + + def pairs[T, U](t: Generator[T], u: Generator[U]) = for { + x <- t + y <- u + } yield (x, y) + +What does this expand to? + +Streamlining It +=============== + +Can we avoid the `new Generator ... ` boilerplate? + +Ideally, we would like to write: + + val booleans = integers map (x => x > 0) + + def pairs[T, U](t: Generator[T], u: Generator[U]) = + t flatMap (x => u map (y => (x, y))) + +Need `map` and `flatMap` for that! + +Generator with `map` and `flatMap` +================================== + +Here's a more convenient version of `Generator`: + + trait Generator[+T] { + self => // an alias for "this". + + def generate: T + + def map[S](f: T => S): Generator[S] = new Generator[S] { + def generate = f(self.generate) + } +-> + def flatMap[S](f: T => Generator[S]): Generator[S] = new Generator[S] { + def generate = f(self.generate).generate + } + } + +The `booleans` Generator +======================== + +What does this definition resolve to? + + val booleans = for (x <- integers) yield x > 0 +-> + val booleans = integers map { x => x > 0 } +-> + val booleans = new Generator[Boolean] { + def generate = (x: Int => x > 0)(integers.generate) + } +-> + val booleans = new Generator[Boolean] { + def generate = integers.generate > 0 + } + +The `pairs` Generator +===================== + + def pairs[T, U](t: Generator[T], u: Generator[U]) = t flatMap { + x => u map { y => (x, y) } } +-> + def pairs[T, U](t: Generator[T], u: Generator[U]) = t flatMap { + x => new Generator[(T, U)] { def generate = (x, u.generate) } } +-> + def pairs[T, U](t: Generator[T], u: Generator[U]) = new Generator[(T, U)] { + def generate = (new Generator[(T, U)] { + def generate = (t.generate, u.generate) + }).generate } +-> + def pairs[T, U](t: Generator[T], u: Generator[U]) = new Generator[(T, U)] { + def generate = (t.generate, u.generate) + } + +Generator Examples +================== + + def single[T](x: T): Generator[T] = new Generator[T] { + def generate = x + } + + def choose(lo: Int, hi: Int): Generator[Int] = + for (x <- integers) yield lo + x % (hi - lo) + + def oneOf[T](xs: T*): Generator[T] = + for (idx <- choose(0, xs.length)) yield xs(idx) + +A `List` Generator +================== + +A list is either an empty list or a non-empty list. + + def lists: Generator[List[Int]] = for { + isEmpty <- booleans + list <- if isEmpty then emptyLists else nonEmptyLists + } yield list +-> + def emptyLists = single(Nil) +-> + def nonEmptyLists = for { + head <- integers + tail <- lists + } yield head :: tail + + +A `Tree` Generator +================== + +Can you implement a generator that creates random `Tree` objects? + + trait Tree + + case class Inner(left: Tree, right: Tree) extends Tree + + case class Leaf(x: Int) extends Tree + +Hint: a tree is either a leaf or an inner node. + + +Application: Random Testing +=========================== + +You know about units tests: + +- Come up with some some test inputs to program functions + and a \red{postcondition}. +- The postcondition is a property of the expected result. +- Verify that the program satisfies the postcondition. + +\red{Question:} Can we do without the test inputs? + +Yes, by generating random test inputs. + +Random Test Function +==================== + +Using generators, we can write a random test function: + + def test[T](g: Generator[T], numTimes: Int = 100) + (test: T => Boolean): Unit = { + for (i <- 0 until numTimes) { + val value = g.generate + assert(test(value), "test failed for "+value) + } + println("passed "+numTimes+" tests") + } + +Random Test Function +==================== + +Example usage: + + test(pairs(lists, lists)) { + case (xs, ys) => (xs ++ ys).length > xs.length + } + +\question: Does the above property always hold? + + O Yes + O No + +\quiz + + +ScalaCheck +========== + +Shift in viewpoint: Instead of writing tests, write \red{properties} that are assumed to hold. + +This idea is implemented in the `ScalaCheck` tool. + + forAll { (l1: List[Int], l2: List[Int]) => + l1.size + l2.size == (l1 ++ l2).size + } + +It can be used either stand-alone or as part of ScalaTest. + +See ScalaCheck tutorial on the course page. + + + + diff --git a/lectures/progfun2-1-4.md b/lectures/progfun2-1-4.md new file mode 100644 index 0000000000000000000000000000000000000000..0b8390780fdd3e5d0bacd1819056c4b39493cf50 --- /dev/null +++ b/lectures/progfun2-1-4.md @@ -0,0 +1,288 @@ +% Monads +% Martin Odersky +% +Monads +====== + +Data structures with `map` and `flatMap` seem to be quite common. + +In fact there's a name that describes this class of a data structures +together with some algebraic laws that they should have. + +They are called \red{monads}. + +What is a Monad? +================ + +A monad `M` is a parametric type M[T] with two operations, `flatMap` and `unit`, that +have to satisfy some laws. + + trait M[T] { + def flatMap[U](f: T => M[U]): M[U] + } + + def unit[T](x: T): M[T] + +In the literature, `flatMap` is more commonly called `bind`. + + +Examples of Monads +================== + + - `List` is a monad with `unit(x) = List(x)` + + - `Set` is monad with `unit(x) = Set(x)` + + - `Option` is a monad with `unit(x) = Some(x)` + + - `Generator` is a monad with `unit(x) = single(x)` + +`flatMap` is an operation on each of these types, whereas `unit` in Scala is different +for each monad. + + +Monads and map +============== + +`map` can be defined for every monad as a combination of `flatMap` and `unit`: + + m map f == m flatMap (x => unit(f(x))) + == m flatMap (f andThen unit) + +Monad Laws +========== + +To qualify as a monad, a type has to satisfy three laws: + +\red{Associativity:} + + m flatMap f flatMap g == m flatMap (x => f(x) flatMap g) + +\red{Left unit} + + unit(x) flatMap f == f(x) + +\red{Right unit} + + m flatMap unit == m + +Checking Monad Laws +=================== + +Let's check the monad laws for Option. + +Here's `flatMap` for `Option`: + + abstract class Option[+T] { + + def flatMap[U](f: T => Option[U]): Option[U] = this match { + case Some(x) => f(x) + case None => None + } + } + +Checking the Left Unit Law +========================== + +Need to show: `Some(x) flatMap f == f(x)` + + Some(x) flatMap f +-> + == Some(x) match { + case Some(x) => f(x) + case None => None + } +-> + == f(x) + +Checking the Right Unit Law +=========================== + +Need to show: `opt flatMap Some == opt` + + opt flatMap Some +-> + == opt match { + case Some(x) => Some(x) + case None => None + } +-> + == opt + + +Checking the Associative Law +============================ + +Need to show: `opt flatMap f flatMap g == opt flatMap (x => f(x) flatMap g)` + + opt flatMap f flatMap g +-> + == opt match { case Some(x) => f(x) case None => None } + match { case Some(y) => g(y) case None => None } +-> + == opt match { + case Some(x) => + f(x) match { case Some(y) => g(y) case None => None } + case None => + None match { case Some(y) => g(y) case None => None } + } + + +Checking the Associative Law (2) +================================ + + == opt match { + case Some(x) => + f(x) match { case Some(y) => g(y) case None => None } + case None => None + } +-> + == opt match { + case Some(x) => f(x) flatMap g + case None => None + } +-> + == opt flatMap (x => f(x) flatMap g) + +Significance of the Laws for For-Expressions +============================================ + +We have seen that monad-typed expressions are typically written as `for` expressions. + +What is the significance of the laws with respect to this? + +$1.$ Associativity says essentially that one can "inline" nested for expressions: + + for (y <- for (x <- m; y <- f(x)) yield y + z <- g(y)) yield z + + == for (x <- m; + y <- f(x) + z <- g(y)) yield z + +Significance of the Laws for For-Expressions +============================================ + +$2.$ Right unit says: + + for (x <- m) yield x + + == m + +$3.$ Left unit does not have an analogue for for-expressions. + +Another type: Try +================= + +In the later parts of this course we will need a type named `Try`. + +`Try` resembles `Option`, but instead of `Some/None` there is a `Success` case with a value and a `Failure` case that contains an exception: + + abstract class Try[+T] + case class Success[T](x: T) extends Try[T] + case class Failure(ex: Exception) extends Try[Nothing] + +`Try` is used to pass results of computations that can fail with an +exception between threads and computers. + +Creating a `Try` +================ + +You can wrap up an arbitrary computation in a `Try`. + + Try(expr) // gives Success(someValue) or Failure(someException) + +Here's an implementation of `Try`: + + object Try { + def apply[T](expr: => T): Try[T] = + try Success(expr) + catch { + case NonFatal(ex) => Failure(ex) + } + +Composing `Try` +=============== + +Just like with `Option`, `Try`-valued computations can be composed in for expresssions. + + for { + x <- computeX + y <- computeY + } yield f(x, y) + +If `computeX` and `computeY` succeed with results `Success(x)` and `Success(y)`, this will return `Success(f(x, y))`. + +If either computation fails with an exception `ex`, this will return `Failure(ex)`. + +Definition of `flatMap` and `map` on `Try` +========================================== + + abstract class Try[T] { + def flatMap[U](f: T => Try[U]): Try[U] = this match { + case Success(x) => try f(x) catch { case NonFatal(ex) => Failure(ex) } + case fail: Failure => fail + } + + def map[U](f: T => U): Try[U] = this match { + case Success(x) => Try(f(x)) + case fail: Failure => fail + }} + +So, for a `Try` value `t`, + + t map f == t flatMap (x => Try(f(x))) + == t flatMap (f andThen Try) + +Exercise +======== + +It looks like `Try` might be a monad, with `unit` = `Try`. + +Is it? + + O Yes + O No, the associative law fails + O No, the left unit law fails + O No, the right unit law fails + O No, two or more monad laws fail. + +\quiz + +Solution +======== + +It turns out the left unit law fails. + + Try(expr) flatMap f != f(expr) + +Indeed the left-hand side will never raise a non-fatal exception whereas the +right-hand side will raise any exception thrown by `expr` or `f`. + +Hence, `Try` trades one monad law for another law which is more useful in this context: + +\begin{quote}\it +An expression composed from `Try`, `map`, `flatMap` will never throw a +non-fatal exception. +\end{quote} + +Call this the "bullet-proof" principle. + + +Conclusion +========== + +We have seen that for-expressions are useful not only for collections. + +Many other types also define `map`,`flatMap`, and `withFilter` operations +and with them for-expressions. + +Examples: `Generator`, `Option`, `Try`. + +Many of the types defining `flatMap` are monads. + +(If they also define `withFilter`, they are called "monads with zero"). + +The three monad laws give useful guidance in the design of library APIs. + + diff --git a/lectures/progfun2-1-recap1.md b/lectures/progfun2-1-recap1.md new file mode 100644 index 0000000000000000000000000000000000000000..2bd77ec0210d2e09e5457d6ca7904e6edcdc210a --- /dev/null +++ b/lectures/progfun2-1-recap1.md @@ -0,0 +1,241 @@ +% Recap: Functions and Pattern Matching +% Martin Odersky +% +Recap: Case Classes +=================== + +Case classes are Scala's preferred way to define complex data. + +\example: Representing JSON (Java Script Object Notation) + +\begin{lstlisting} + { "firstName" : "John", + "lastName" : "Smith", + "address": { + "streetAddress": "21 2nd Street", + "state": "NY", + "postalCode": 10021 + }, + "phoneNumbers": [ + { "type": "home", "number": "212 555-1234" }, + { "type": "fax", "number": "646 555-4567" } + ] + } +\end{lstlisting} + +Representation of JSON in Scala +=============================== + + abstract class JSON + case class JSeq (elems: List[JSON]) extends JSON + case class JObj (bindings: Map[String, JSON]) extends JSON + case class JNum (num: Double) extends JSON + case class JStr (str: String) extends JSON + case class JBool(b: Boolean) extends JSON + case object JNull extends JSON + +Example +======= + + val data = JObj(Map( + "firstName" -> JStr("John"), + "lastName" -> JStr("Smith"), + "address" -> JObj(Map( + "streetAddress" -> JStr("21 2nd Street"), + "state" -> JStr("NY"), + "postalCode" -> JNum(10021) + )), + "phoneNumbers" -> JSeq(List( + JObj(Map( + "type" -> JStr("home"), "number" -> JStr("212 555-1234") + )), + JObj(Map( + "type" -> JStr("fax"), "number" -> JStr("646 555-4567") + )) )) )) + +Pattern Matching +================ + +Here's a method that returns the string representation JSON data: + + def show(json: JSON): String = json match { + case JSeq(elems) => + "[" + (elems map show mkString ", ") + "]" + case JObj(bindings) => + val assocs = bindings map { + case (key, value) => "\"" + key + "\": " + show(value) + } + "{" + (assocs mkString ", ") + "}" + case JNum(num) => num.toString + case JStr(str) => '\"' + str + '\"' + case JBool(b) => b.toString + case JNull => "null" + } + +Case Blocks +=========== + +\question: What's the type of: + + { case (key, value) => key + ": " + value } + +-> +Taken by itself, the expression is not typable. + +We need to prescribe an expected type. + +The type expected by `map` on the last slide is + + JBinding => String, + +the type of functions from pairs of strings and `JSON` data to `String`. +where `JBinding` is + + type JBinding = (String, JSON) + +Functions Are Objects +===================== + +In Scala, every concrete type is the type of some class or trait. + +The function type is no exception. A type like + + JBinding => String + +is just a shorthand for + + scala.Function1[JBinding, String] + +where `scala.Function1` is a trait and `JBinding` and `String` are its type arguments. + +The Function1 Trait +=================== + +Here's an outline of trait `Function1`: + + trait Function1[-A, +R] { + def apply(x: A): R + } + +The pattern matching block + + { case (key, value) => key + ": " + value } + +expands to the `Function1` instance + + new Function1[JBinding, String] { + def apply(x: JBinding) = x match { + case (key, value) => key + ": " + show(value) + } + } + +Subclassing Functions +===================== + +One nice aspect of functions being traits is that we can subclass the +function type. + +For instance, maps are functions from keys to values: + + trait Map[Key, Value] extends (Key => Value) ... +-> +Sequences are functions from `Int` indices to values: + + trait Seq[Elem] extends Int => Elem + +That's why we can write + + elems(i) + +for sequence (and array) indexing. + +Partial Matches +=============== + +We have seen that a pattern matching block like + + { case "ping" => "pong" } + +can be given type `String => String`. + + val f: String => String = { case "ping" => "pong" } + +But the function is not defined on all its domain! + + f("pong") // gives a MatchError + +Is there a way to find out whether the function can be applied to a +given argument before running it? + +Partial Functions +================= + +Indeed there is: + + val f: PartialFunction[String, String] = { case "ping" => "pong" } + f.isDefinedAt("ping") // true + f.isDefinedAt("pong") // false + +The partial function trait is defined as follows: + + trait PartialFunction[-A, +R] extends Function1[-A, +R] { + def apply(x: A): R + def isDefinedAt(x: A): Boolean + } + +Partial Function Objects +======================== + +If the expected type is a `PartialFunction`, the Scala compiler will expand + + { case "ping" => "pong" } + +as follows: + + new PartialFunction[String, String] { + def apply(x: String) = x match { + case "ping" => "pong" + } + def isDefinedAt(x: String) = x match { + case "ping" => true + case _ => false + } + } + + +Exercise +======== + +Given the function + + val f: PartialFunction[List[Int], String] = { + case Nil => "one" + case x :: y :: rest => "two" + } + +What do you expect is the result of + +` f.isDefinedAt(List(1, 2, 3))` \tab ? + + O true + O false + +\quiz + +Exercise(2) +=========== + +How about the following variation: + + val g: PartialFunction[List[Int], String] = { + case Nil => "one" + case x :: rest => + rest match { + case Nil => "two" + } + } + +` g.isDefinedAt(List(1, 2, 3))` \tab gives: + + O true + O false diff --git a/lectures/progfun2-1-recap2.md b/lectures/progfun2-1-recap2.md new file mode 100644 index 0000000000000000000000000000000000000000..5e1fc5b3c2b56bf8f8ba66c81138c68d623860ed --- /dev/null +++ b/lectures/progfun2-1-recap2.md @@ -0,0 +1,181 @@ +% Recap: Collections +% Martin Odersky +% +Recap: Collections +================== + +Scala has a rich hierarchy of collection classes. + + + +Recap: Collection Methods +========================= + +All collection types share a common set of general methods. + +Core methods: + + map + flatMap + filter + +and also + + foldLeft + foldRight + +Idealized Implementation of `map` on Lists +========================================== + + abstract class List[+T] { + + def map[U](f: T => U): List[U] = this match { + case x :: xs => f(x) :: xs.map(f) + case Nil => Nil + } + } + +Idealized Implementation of `flatMap` on Lists +============================================== + + abstract class List[+T] { + + def flatMap[U](f: T => List[U]): List[U] = this match { + case x :: xs => f(x) ++ xs.flatMap(f) + case Nil => Nil + } + } + +Idealized Implementation of `filter` on Lists +============================================= + + abstract class List[+T] { + + def filter(p: T => Boolean): List[T] = this match { + case x :: xs => + if p(x) then x :: xs.filter(p) else xs.filter(p) + case Nil => Nil + } + } +-> +In practice, the implementation and type of these methods are different in order to + + - make them apply to arbitrary collections, not just lists, + - make them tail-recursive on lists. + + +For-Expressions +=============== + +Simplify combinations of core methods `map`, `flatMap`, `filter`. + +Instead of: + + (1 until n) flatMap (i => + (1 until i) filter (j => isPrime(i + j)) map + (j => (i, j))) + +one can write: + + for { + i <- 1 until n + j <- 1 until i + if isPrime(i + j) + } yield (i, j) + + +Translation of For (1) +====================== + +The Scala compiler translates for-expressions in terms of `map`, `flatMap` +and a lazy variant of `filter`. + +Here is the translation scheme used by the compiler + +$1.$ A simple `for`-expression + + for (x <- e1) yield e2 + +is translated to + + e1.map(x => e2) + +Translation of For (2) +====================== + +$2.$ A `for`-expression + + for (x <- e1 if f; s) yield e2 + +where `f` is a filter and `s` is a (potentially empty) sequence of generators and filters, is translated to + + for (x <- e1.withFilter(x => f); s) yield e2 + +(and the translation continues with the new expression) + +You can think of `withFilter` as a variant of `filter` that does not +produce an intermediate list, but instead filters the following `map` or `flatMap` function application. + +Translation of For (3) +====================== + +$3.$ A for-expression + + for (x <- e1; y <- e2; s) yield e3 + +where `s` is a (potentially empty) sequence of generators and filters, is translated into + + e1.flatMap(x => for (y <- e2; s) yield e3) + +(and the translation continues with the new expression) + +For-expressions and Pattern Matching +==================================== + +The left-hand side of a generator may also be a pattern. + +\example + + val data: List[JSON] = ... + for { + JObj(bindings) <- data + JSeq(phones) = bindings("phoneNumbers") + JObj(phone) <- phones + JStr(digits) = phone("number") + if digits startsWith "212" + } yield (bindings("firstName"), bindings("lastName")) + +Translation of Pattern Matching in For +====================================== + +If `pat` is a pattern with a single variable `x`, we translate + + pat <- expr + +to: + + x <- expr withFilter { + case pat => true + case _ => false + } map { + case pat => x + } + +Exercise +======== + + for { + x <- 2 to N + y <- 2 to x + if (x % y == 0) + } yield (x, y) + +The expression above expands to which of the following two expressions? + + O (2 to N) flatMap (x => + (2 to x) withFilter (y => + x % y == 0) map (y => (x, y))) + + O (2 to N) map (x => + (2 to x) flatMap (y => + if (x % y) then == 0 (x, y) diff --git a/lectures/progfun2-2-1.md b/lectures/progfun2-2-1.md new file mode 100644 index 0000000000000000000000000000000000000000..86559ce58f8944480d67331f6ee77565524b5a68 --- /dev/null +++ b/lectures/progfun2-2-1.md @@ -0,0 +1,241 @@ +% Structural Induction on Trees +% +% +Structural Induction on Trees +============================= + +Structural induction is not limited to lists; it applies to any tree structure. + +The general induction principle is the following: + +To prove a property $\btt P(t)$ for all trees $\btt t$ of a certain type, + + - show that $\btt P(l)$ holds for all leaves $\btt l$ of a tree, + - for each type of internal node $\btt t$ with subtrees $\btt s_1, ..., s_n$, show that +\begin{quote} + $\btt P(s_1) \wedge ... \wedge P(s_n)$ implies $P(t)$. +\end{quote} + +Example: IntSets +================ + +Recall our definition of \verb@IntSet@ with the operations \verb@contains@ and \verb@incl@: + + abstract class IntSet { + def incl(x: Int): IntSet + def contains(x: Int): Boolean + } + + object Empty extends IntSet { + def contains(x: Int): Boolean = false + def incl(x: Int): IntSet = NonEmpty(x, Empty, Empty) + } + +Example: IntSets (2) +==================== + + case class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet { + + def contains(x: Int): Boolean = + if x < elem then left contains x + else if x > elem then right contains x + else true + + def incl(x: Int): IntSet = + if x < elem then NonEmpty(elem, left incl x, right) + else if x > elem then NonEmpty(elem, left, right incl x) + else this + } + + +The Laws of IntSet +================== + +What does it mean to prove the correctness of this implementation? + +One way to define and show the correctness of an implementation +consists of proving the laws that it respects. + +In the case of \verb@IntSet@, we have the following three laws: + +For any set \verb@s@, and elements \verb@x@ and \verb@y@: + + Empty contains x = false + (s incl x) contains x = true + (s incl x) contains y = s contains y if x != y + + +(In fact, we can show that these laws completely characterize the desired data type). + +Proving the Laws of IntSet (1) +============================== + +How can we prove these laws? + +\red{Proposition 1}: `Empty contains x = false`. + +\red{Proof:} According to the definition of `contains` in `Empty`. + +Proving the Laws of IntSet (2) +============================== + +\red{Proposition 2:} `(s incl x) contains x = true` + +Proof by structural induction on `s`. + +\BaseCase{\tt Empty} + + + (Empty incl x) contains x +-> + = NonEmpty(x, Empty, Empty) contains x // by definition of Empty.incl +-> + = true // by definition of NonEmpty.contains + +Proving the Laws of IntSet (3) +============================== + +\InductionStep{\tt NonEmpty(x, l, r)} + + + (NonEmpty(x, l, r) incl x) contains x +-> + = NonEmpty(x, l, r) contains x // by definition of NonEmpty.incl +-> + = true // by definition of NonEmpty.contains + +Proving the Laws of IntSet (4) +============================== + +\InductionStep{{\tt NonEmpty(y, l, r)} where {\tt y < x}} + + (NonEmpty(y, l, r) incl x) contains x +-> + = NonEmpty(y, l, r incl x) contains x // by definition of NonEmpty.incl +-> + = (r incl x) contains x // by definition of NonEmpty.contains +-> + = true // by the induction hypothesis +-> +\medskip + +\InductionStep{{\tt NonEmpty(y, l, r)} where {\tt y > x}} is analogous + +Proving the Laws of IntSet (5) +============================== + +\red{Proposition 3}: If `x != y` then + + (xs incl y) contains x = xs contains x. + +Proof by structural induction on `s`. Assume that `y < x` (the dual case `x < y` is analogous). + +\BaseCase{\tt Empty} + + (Empty incl y) contains x // to show: = Empty contains x +-> + = NonEmpty(y, Empty, Empty) contains x // by definition of Empty.incl +-> + = Empty contains x // by definition of NonEmpty.contains + +Proving the Laws of IntSet (6) +============================== + +For the inductive step, we need to consider a tree `NonEmpty(z, l, r)`. We distinguish five cases: + + 1. `z = x` + 2. `z = y` + 3. `z < y < x` + 4. `y < z < x` + 5. `y < x < z` + +First Two Cases: `z = x, z = y` +=============================== + +\InductionStep{\tt NonEmpty(x, l, r)} + + (NonEmpty(x, l, r) incl y) contains x // to show: = NonEmpty(x, l, r) contains x +-> + = NonEmpty(x, l incl y, r) contains x // by definition of NonEmpty.incl +-> + = true // by definition of NonEmpty.contains +-> + = NonEmpty(x, l, r) contains x // by definition of NonEmpty.contains +-> +\InductionStep{\tt NonEmpty(y, l, r)} + + (NonEmpty(y, l, r) incl y) contains x // to show: = NonEmpty(y, l, r) contains x +-> + = NonEmpty(y, l, r) contains x // by definition of NonEmpty.incl + +Case `z < y` +============ + +\InductionStep{{\tt NonEmpty(z, l, r)} where {\tt z < y < x}} + + (NonEmpty(z, l, r) incl y) contains x // to show: = NonEmpty(z, l, r) contains x +-> + = NonEmpty(z, l, r incl y) contains x // by definition of NonEmpty.incl +-> + = (r incl y) contains x // by definition of NonEmpty.contains +-> + = r contains x // by the induction hypothesis +-> + = NonEmpty(z, l, r) contains x // by definition of NonEmpty.contains + +Case `y < z < x` +================ + +\InductionStep{{\tt NonEmpty(z, l, r)} where {\tt y < z < x}} + + (NonEmpty(z, l, r) incl y) contains x // to show: = NonEmpty(z, l, r) contains x +-> + = NonEmpty(z, l incl y, r) contains x // by definition of NonEmpty.incl +-> + = r contains x // by definition of NonEmpty.contains +-> + = NonEmpty(z, l, r) contains x // by definition of NonEmpty.contains + +Case `x < z` +============ + +\InductionStep{{\tt NonEmpty(z, l, r)} where {\tt y < x < z}} + + (NonEmpty(z, l, r) incl y) contains x // to show: = NonEmpty(z, l, r) contains x +-> + = NonEmpty(z, l incl y, r) contains x // by definition of NonEmpty.incl +-> + = (l incl y) contains x // by definition of NonEmpty.contains +-> + = l contains x // by the induction hypothesis +-> + = NonEmpty(z, l, r) contains x // by definition of NonEmpty.contains + +These are all the cases, so the proposition is established. + +Exercise (Hard) +=============== + +Suppose we add a function `union` to `IntSet`: + + abstract class IntSet { ... + def union(other: IntSet): IntSet + } + object Empty extends IntSet { ... + def union(other: IntSet) = other + } + class NonEmpty(x: Int, l: IntSet, r: IntSet) extends IntSet { ... + def union(other: IntSet): IntSet = (l union (r union (other))) incl x + } + +Exercise (Hard) +=============== + +The correctness of `union` can be translated into the following law: + +\red{Proposition 4}: + + (xs union ys) contains x = xs contains x || ys contains x + +Show proposition 4 by using structural induction on `xs`. + diff --git a/lectures/progfun2-2-2.md b/lectures/progfun2-2-2.md new file mode 100644 index 0000000000000000000000000000000000000000..ac371c1d539fa6080110708c51aff82130826256 --- /dev/null +++ b/lectures/progfun2-2-2.md @@ -0,0 +1,203 @@ +% Streams +% +% + +Collections and Combinatorial Search +==================================== + +We've seen a number of immutable collections that provide powerful operations, in particular for +combinatorial search. + +For instance, to find the second prime number between 1000 and 10000: + + ((1000 to 10000) filter isPrime)(1) + +This is _much_ shorter than the recursive alternative: + + def secondPrime(from: Int, to: Int) = nthPrime(from, to, 2) + def nthPrime(from: Int, to: Int, n: Int): Int = + if from >= to then throw new Error("no prime") + else if isPrime(from) then + if n == 1 then from else nthPrime(from + 1, to, n - 1) + else nthPrime(from + 1, to, n) + +Performance Problem +=================== + +But from a standpoint of performance, + + ((1000 to 10000) filter isPrime)(1) + +is pretty bad; it constructs _all_ prime numbers between `1000` and +`10000` in a list, but only ever looks at the first two elements of that list. + +Reducing the upper bound would speed things up, but risks that we miss the +second prime number all together. + +Delayed Evaluation +================== + +However, we can make the short-code efficient by using a trick: + +> Avoid computing the tail of a sequence until it is needed for the evaluation result (which might be never) + +This idea is implemented in a new class, the `Stream`. + +Streams are similar to lists, but their tail is evaluated only _on demand_. + +Defining Streams +================ + +Streams are defined from a constant `Stream.empty` and a constructor `Stream.cons`. + +For instance, + + val xs = Stream.cons(1, Stream.cons(2, Stream.empty)) + +They can also be defined like the other collections by using the +object `Stream` as a factory. + + Stream(1, 2, 3) + +The `toStream` method on a collection will turn the collection into a stream: + +\begin{worksheet} + \verb` (1 to 1000).toStream ` \wsf res0: Stream[Int] = Stream(1, ?) +\end{worksheet} + + +Stream Ranges +============= + +Let's try to write a function that returns `(lo until hi).toStream` directly: + + def streamRange(lo: Int, hi: Int): Stream[Int] = + if lo >= hi then Stream.empty + else Stream.cons(lo, streamRange(lo + 1, hi)) + +Compare to the same function that produces a list: + + def listRange(lo: Int, hi: Int): List[Int] = + if lo >= hi then Nil + else lo :: listRange(lo + 1, hi) + +Comparing the Two Range Functions +================================= + +The functions have almost identical structure yet they evaluate quite differently. + +- `listRange(start, end)` will produce a list with `end - start` elements and return it. + +- `streamRange(start, end)` returns a single object of type `Stream` with `start` as head element. + +- The other elements are only computed when they are needed, where + "needed" means that someone calls `tail` on the stream. + +Methods on Streams +================== + +`Stream` supports almost all methods of `List`. + +For instance, to find the second prime number between 1000 and 10000: + + ((1000 to 10000).toStream filter isPrime)(1) + +Stream Cons Operator +==================== + +The one major exception is `::`. + +`x :: xs` always produces a list, never a stream. + +There is however an alternative operator `#::` which produces a stream. + + x #:: xs == Stream.cons(x, xs) + +`#::` can be used in expressions as well as patterns. + +Implementation of Streams +========================= + +The implementation of streams is quite close to the one of lists. + +Here's the trait `Stream`: + + trait Stream[+A] extends Seq[A] { + def isEmpty: Boolean + def head: A + def tail: Stream[A] + ... + } + +As for lists, all other methods can be defined in terms of these three. + +Implementation of Streams(2) +============================ + +Concrete implementations of streams are defined in the `Stream` companion object. +Here's a first draft: + + object Stream { + def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { + def isEmpty = false + def head = hd + def tail = tl + override def toString = "Stream(" + hd + ", ?)" + } + val empty = new Stream[Nothing] { + def isEmpty = true + def head = throw new NoSuchElementException("empty.head") + def tail = throw new NoSuchElementException("empty.tail") + override def toString = "Stream()" + } + } + +Difference to List +================== + +The only important difference between the implementations of `List` and `Stream` +concern `tl`, the second parameter of `Stream.cons`. + +For streams, this is a by-name parameter. + +That's why the second argument to `Stream.cons` is not evaluated at the point of call. + +Instead, it will be evaluated each time someone calls `tail` on a `Stream` object. + +Other Stream Methods +==================== + +The other stream methods are implemented analogously to their list counterparts. + +For instance, here's `filter`: + + class Stream[+T] { + ... + def filter(p: T => Boolean): Stream[T] = + if isEmpty then this + else if p(head) then cons(head, tail.filter(p)) + else tail.filter(p) + } + +Exercise +======== + +Consider this modification of `streamRange`. + + def streamRange(lo: Int, hi: Int): Stream[Int] = { + print(lo+" ") + if lo >= hi then Stream.empty + else Stream.cons(lo, streamRange(lo + 1, hi)) + } + +When you write `streamRange(1, 10).take(3).toList` \newline +what gets printed? + + O Nothing + O 1 + O 1 2 3 + O 1 2 3 4 + O 1 2 3 4 5 6 7 8 9 +-> + +\quiz \ No newline at end of file diff --git a/lectures/progfun2-2-3.md b/lectures/progfun2-2-3.md new file mode 100644 index 0000000000000000000000000000000000000000..bb90d121eaab5ef5ba1172be9775b97dfeeddcc4 --- /dev/null +++ b/lectures/progfun2-2-3.md @@ -0,0 +1,176 @@ +% Lazy Evaluation +% +% + +Lazy Evaluation +=============== + +The proposed implementation suffers from a serious potential performance +problem: If `tail` is called several times, the corresponding stream +will be recomputed each time. + +This problem can be avoided by storing the result of the first +evaluation of `tail` and re-using the stored result instead of recomputing `tail`. + +This optimization is sound, since in a purely functional language an +expression produces the same result each time it is evaluated. + +We call this scheme \red{lazy evaluation} (as opposed to \red{by-name evaluation} in the case where everything is recomputed, and \red{strict evaluation} for normal parameters and `val` definitions.) + +Lazy Evaluation in Scala +======================== + +Haskell is a functional programming language that uses lazy evaluation by default. + +Scala uses strict evaluation by default, but allows lazy evaluation of value definitions +with the `lazy val` form: + + lazy val x = expr + + +Exercise: +========= + +Consider the following program: + + def expr = { + val x = { print("x"); 1 } + lazy val y = { print("y"); 2 } + def z = { print("z"); 3 } + z + y + x + z + y + x + } + expr + +If you run this program, what gets printed as a side effect of evaluating `expr`? + + O zyxzyx O xzyz + O xyzz O zyzz + O something else + + + + +Lazy Vals and Streams +===================== + +Using a lazy value for `tail`, `Stream.cons` can be implemented more efficiently: + + def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { + def head = hd + lazy val tail = tl + ... + } + +Seeing it in Action +=================== + +To convince ourselves that the implementation of streams +really does avoid unnecessary computation, let's observe the +execution trace of the expression: + + (streamRange(1000, 10000) filter isPrime) apply 1 +-> + --> (if 1000 >= 10000 then empty // by expanding streamRange + else cons(1000, streamRange(1000 + 1, 10000)) + .filter(isPrime).apply(1) +-> + --> cons(1000, streamRange(1000 + 1, 10000)) // by evaluating if + .filter(isPrime).apply(1) + +Evaluation Trace (2) +==================== + +Let's abbreviate `cons(1000, streamRange(1000 + 1, 10000))` to `C1`. + + C1.filter(isPrime).apply(1) +-> + --> (if C1.isEmpty then C1 // by expanding filter + else if isPrime(C1.head) then cons(C1.head, C1.tail.filter(isPrime)) + else C1.tail.filter(isPrime)) + .apply(1) +-> + --> (if isPrime(C1.head) then cons(C1.head, C1.tail.filter(isPrime)) + else C1.tail.filter(isPrime)) // by eval. if + .apply(1) +-> + --> (if isPrime(1000) then cons(C1.head, C1.tail.filter(isPrime)) + else C1.tail.filter(isPrime)) // by eval. head + .apply(1) + +Evaluation Trace (3) +==================== + + -->> (if false then cons(C1.head, C1.tail.filter(isPrime)) // by eval. isPrime + else C1.tail.filter(isPrime)) + .apply(1) +-> + --> C1.tail.filter(isPrime).apply(1) // by eval. if +-> + -->> streamRange(1001, 10000) // by eval. tail + .filter(isPrime).apply(1) + +The evaluation sequence continues like this until: +-> + -->> streamRange(1009, 10000) + .filter(isPrime).apply(1) + + --> cons(1009, streamRange(1009 + 1, 10000)) // by eval. streamRange + .filter(isPrime).apply(1) + +Evaluation Trace (4) +==================== + +Let's abbreviate `cons(1009, streamRange(1009 + 1, 10000))` to `C2`. + + C2.filter(isPrime).apply(1) +-> + --> cons(1009, C2.tail.filter(isPrime)).apply(1) +-> + --> if 1 == 0 then cons(1009, C2.tail.filter(isPrime)).head // by eval. apply + else cons(1009, C2.tail.filter(isPrime)).tail.apply(0) + +Assuming `apply` is defined like this in `Stream[T]`: + + def apply(n: Int): T = + if n == 0 then head + else tail.apply(n-1) + +Evaluation Trace (4) +==================== + +Let's abbreviate `cons(1009, streamRange(1009 + 1, 10000))` to `C2`. + + C2.filter(isPrime).apply(1) + + -->> cons(1009, C2.tail.filter(isPrime)).apply(1) // by eval. filter + + --> if 1 == 0 then cons(1009, C2.tail.filter(isPrime)).head // by eval. apply + else cons(1009, C2.tail.filter(isPrime)).tail.apply(0) + + --> cons(1009, C2.tail.filter(isPrime)).tail.apply(0) // by eval. if +-> + --> C2.tail.filter(isPrime).apply(0) // by eval. tail +-> + --> streamRange(1010, 10000).filter(isPrime).apply(0) // by eval. tail + +Evaluation Trace (5) +==================== + +The process continues until + + ... + --> streamRange(1013, 10000).filter(isPrime).apply(0) +-> + --> cons(1013, streamRange(1013 + 1, 10000)) // by eval. streamRange + .filter(isPrime).apply(0) + +Let `C3` be a shorthand for `cons(1013, streamRange(1013 + 1, 10000)`. + + == C3.filter(isPrime).apply(0) +-> + -->> cons(1013, C3.tail.filter(isPrime)).apply(0) // by eval. filter +-> + --> 1013 // by eval. apply + +Only the part of the stream necessary to compute the result has been constructed. + diff --git a/lectures/progfun2-2-4.md b/lectures/progfun2-2-4.md new file mode 100644 index 0000000000000000000000000000000000000000..38b93c704a8484d12b6ce27c3bcb0c94c8d030e8 --- /dev/null +++ b/lectures/progfun2-2-4.md @@ -0,0 +1,92 @@ +% Computing with Infinite Sequences +% +% + +Infinite Streams +================ + +You saw that all elements of a stream except the first one are computed +only when they are needed to produce a result. + +This opens up the possibility to define infinite streams! + +For instance, here is the stream of all integers starting from a given number: + + def from(n: Int): Stream[Int] = n #:: from(n+1) + +The stream of all natural numbers: +-> + val nats = from(0) + +The stream of all multiples of 4: +-> + nats map (_ * 4) + +The Sieve of Eratosthenes +========================= + +The Sieve of Eratosthenes is an ancient technique to calculate prime numbers. + +The idea is as follows: + + - Start with all integers from 2, the first prime number. + - Eliminate all multiples of 2. + - The first element of the resulting list is 3, a prime number. + - Eliminate all multiples of 3. + - Iterate forever. At each step, the first number in the list is a prime number + and we eliminate all its multiples. + +The Sieve of Eratosthenes in Code +================================= + +Here's a function that implements this principle: + + def sieve(s: Stream[Int]): Stream[Int] = + s.head #:: sieve(s.tail filter (_ % s.head != 0)) + + val primes = sieve(from(2)) + +To see the list of the first `N` prime numbers, you can write + + (primes take N).toList + +Back to Square Roots +==================== + +Our previous algorithm for square roots always used a `isGoodEnough` test to tell when to terminate the iteration. + +With streams we can now express the concept of a converging sequence without +having to worry about when to terminate it: + + def sqrtStream(x: Double): Stream[Double] = { + def improve(guess: Double) = (guess + x / guess) / 2 + lazy val guesses: Stream[Double] = 1 #:: (guesses map improve) + guesses + } + +Termination +=========== + +We can add `isGoodEnough` later. + + def isGoodEnough(guess: Double, x: Double) = + math.abs((guess * guess - x) / x) < 0.0001 + + sqrtStream(4) filter (isGoodEnough(_, 4)) + +Exercise: +========= + +Consider two ways to express the infinite stream of multiples of a given number `N`: + + val xs = from(1) map (_ * N) + + val ys = from(1) filter (_ % N == 0) + +Which of the two streams generates its results faster? + + O from(1) map (_ * N) + O from(1) filter (_ % N == 0) + +-> +\quiz \ No newline at end of file diff --git a/lectures/progfun2-2-5.md b/lectures/progfun2-2-5.md new file mode 100644 index 0000000000000000000000000000000000000000..ecc009ce47ff9c51d1ec2ab7f9a84bc8c1cda736 --- /dev/null +++ b/lectures/progfun2-2-5.md @@ -0,0 +1,41 @@ +% Case Study +% +% +The Water Pouring Problem +========================= + + + +States and Moves +================ + +Glass: `Int` + +State: `Vector[Int]` (one entry per glass) + +Moves: + + Empty(glass) + Fill(glass) + Pour(from, to) + +Variants +======== + +In a program of the complexity of the pouring program, there are many +choices to be made. + +Choice of representations. + + - Specific classes for moves and paths, or some encoding? + - Object-oriented methods, or naked data structures with functions? + +The present elaboration is just one solution, and not necessarily the +shortest one. + +Guiding Principles for Good Design +================================== + + - Name everything you can. + - Put operations into natural scopes. + - Keep degrees of freedom for future refinements. diff --git a/lectures/progfun2-3-1.md b/lectures/progfun2-3-1.md new file mode 100644 index 0000000000000000000000000000000000000000..64edea43afdab6e0c31dd73e6deea84f7306bc4a --- /dev/null +++ b/lectures/progfun2-3-1.md @@ -0,0 +1,208 @@ +% Functions and State +% Martin Odersky +% + +Functions and State +=================== + +Until now, our programs have been side-effect free. + +Therefore, the concept of \red{time} wasn't important. + +For all programs that terminate, any sequence of actions would have given the same results. + +This was also reflected in the substitution model of computation. + +Reminder: Substitution Model +============================ + +Programs can be evaluated by \red{rewriting}. + +The most important rewrite rule covers function applications: + +$$ +\begin{array}{ll} + &\btt def\ f (x_1, ..., x_n) = B ;\ ...\ f (v_1, ..., v_n) \\ + \rightarrow & \\ + &\btt def\ f (x_1, ..., x_n) = B ;\ ...\ [v_1/x_1, ..., v_n/x_n]\,B +\end{array} +$$ + +Rewriting Example: +================== + +Say you have the following two functions `iterate` and `square`: + + def iterate(n: Int, f: Int => Int, x: Int) = + if n == 0 then x else iterate(n-1, f, f(x)) + def square(x: Int) = x * x + +Then the call `iterate(1, square, 3)` gets rewritten as follows: +-> + $\rightarrow$ ` if 1 == 0 then 3 else iterate(1-1, square, square(3))` +-> + $\rightarrow$ ` iterate(0, square, square(3))` +-> + $\rightarrow$ ` iterate(0, square, 3 * 3)` +-> + $\rightarrow$ ` iterate(0, square, 9)` +-> + $\rightarrow$ ` if 0 == 0 then 9 else iterate(0-1, square, square(9)) ` $\rightarrow$ ` 9` + +Observation: +============ + +Rewriting can be done anywhere in a term, and all rewritings which +terminate lead to the same solution. + +This is an important result of the $\lambda$-calculus, the theory +behind functional programming. + +Example: + + if 1 == 0 then 3 else iterate(1 - 1, square, square(3)) +-> + \mbox{} + + iterate(0, square, square(3)) + + +Observation: +============ + +Rewriting can be done anywhere in a term, and all rewritings which +terminate lead to the same solution. + +This is an important result of the $\lambda$-calculus, the theory +behind functional programming. + +Example: + + if 1 == 0 then 3 else iterate(1 - 1, square, square(3)) + + \mbox{} + + iterate(0, square, square(3)) if 1 == 0 then 3 + else iterate(1 - 1, square, 3 * 3) + + + +Stateful Objects +================ + +One normally describes the world as a set of objects, some of which +have state that \red{changes} over the course of time. + +An object \red{has a state} if its behavior is influenced by its +history. + +\example: +a bank account has a state, because the answer to the question +\begin{quote} + ``can I withdraw 100 CHF ?'' +\end{quote} +may vary over the course of the lifetime of the account. + +Implementation of State +======================= + +Every form of mutable state is constructed from variables. + +A variable definition is written like a value definition, but with the +keyword `var` in place of `val`: + + var x: String = "abc" + var count = 111 + +Just like a value definition, a variable definition associates a value +with a name. + +However, in the case of variable definitions, this association can be +changed later through an \red{assignment}, like in Java: + + x = "hi" + count = count + 1 + +State in Objects +================ + +In practice, objects with state are usually represented by objects that +have some variable members. \example: Here is a class modeling a bank account. + + class BankAccount { + private var balance = 0 + def deposit(amount: Int): Unit = { + if amount > 0 then balance = balance + amount + } + def withdraw(amount: Int): Int = + if 0 < amount && amount <= balance then { + balance = balance - amount + balance + } else throw new Error("insufficient funds") + } + +State in Objects (2) +==================== + +The class `BankAccount` defines a variable `balance` that contains the +current balance of the account. + +The methods `deposit` and `withdraw` change the value of the `balance` +through assignments. + +Note that `balance` is `private` in the `BankAccount` +class, it therefore cannot be accessed from outside the class. + +To create bank accounts, we use the usual notation for object creation: + + val account = new BankAccount + +Working with Mutable Objects +============================ +Here is a worksheet that manipulates bank accounts. + + val account = new BankAccount // account: BankAccount = BankAccount + account deposit 50 // + account withdraw 20 // res1: Int = 30 + account withdraw 20 // res2: Int = 10 + account withdraw 15 // java.lang.Error: insufficient funds + +Applying the same operation to an account twice in a row produces different results. Clearly, accounts are stateful objects. + +Statefulness and Variables +========================== + +Remember the definition of streams (lazy sequences) in \blue{week 7, \#progfun]}. Instead of using a lazy val, we could also +implement non-empty streams using a mutable variable: + + def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] { + def head = hd + private var tlOpt: Option[Stream[T]] = None + def tail: T = tlOpt match { + case Some(x) => x + case None => tlOpt = Some(tl); tail + }} + +\question: Is the result of `cons` a stateful object? + + O Yes + O No + +\quiz + +Statefulness and Variables (2) +============================== + +Consider the following class: + + class BankAccountProxy(ba: BankAccount) { + def deposit(amount: Int): Unit = ba.deposit(amount) + def withdraw(amount: Int): Int = ba.withdraw(amount) + } + +\question: Are instances of `BankAccountProxy` stateful objects? + + O Yes + O No + +\quiz diff --git a/lectures/progfun2-3-2.md b/lectures/progfun2-3-2.md new file mode 100644 index 0000000000000000000000000000000000000000..53ca27b3f16baadbb1989ecc75dab26668867f5b --- /dev/null +++ b/lectures/progfun2-3-2.md @@ -0,0 +1,153 @@ +% Identity and Change +% Martin Odersky +% + +Identity and Change +=================== + +Assignment poses the new problem of deciding whether two expressions +are "the same" + +When one excludes assignments and one writes: + + val x = E; val y = E + +where `E` is an arbitrary expression, then it is reasonable to assume that `x` and `y` are the same. That is to say that we could have also written: + + val x = E; val y = x + +(This property is usually called \red{referential transparency}) + +Identity and Change (2) +======================= + +But once we allow the assignment, the two formulations are different. For example: + + val x = new BankAccount + val y = new BankAccount + +\question: Are `x` and `y` the same? + + O Yes + O No + +\quiz + + +Operational Equivalence +======================= + +To respond to the last question, we must specify what is meant by "the same". + +The precise meaning of "being the same" is defined by the property of +\red{operational equivalence}. + +In a somewhat informal way, this property is stated as follows. + +Suppose we have two definitions `x` and `y`. + +`x` and `y` are operationally equivalent if \red{no possible test} can +distinguish between them. + +Testing for Operational Equivalence +=================================== + +To test if `x` and `y` are the same, we must + +- Execute the definitions followed by an arbitrary sequence `f` of operations that involves `x` and `y`, observing the possible outcomes. + + val x = new BankAccount + val y = new BankAccount + f(x, y) + +Testing for Operational Equivalence +=================================== + +To test if `x` and `y` are the same, we must + +- Execute the definitions followed by an arbitrary sequence of operations that involves `x` and `y`, observing the possible outcomes. + + val x = new BankAccount val x = new BankAccount + val y = new BankAccount val y = new BankAccount + f(x, y) f(x, x) + +- Then, execute the definitions with another sequence `S'` obtained by renaming all occurrences of `y` by `x` in `S` +-> +- If the results are different, then the expressions `x` and `y` are certainly different. +-> +- On the other hand, if all possible pairs of sequences `(S, S')` produce the same result, then `x` and `y` are the same. + +Counterexample for Operational Equivalence +========================================== + +Based on this definition, let's see if the expressions + + val x = new BankAccount + val y = new BankAccount + +define values `x` and `y` that are the same. + +Let's follow the definitions by a test sequence: + + val x = new BankAccount + val y = new BankAccount + x deposit 30 // val res1: Int = 30 + y withdraw 20 // java.lang.Error: insufficient funds + +Counterexample for Operational Equivalence (2) +============================================== + +Now rename all occurrences of `y` with `x` in this sequence. We obtain: + + val x = new BankAccount + val y = new BankAccount + x deposit 30 // val res1: Int = 30 + x withdraw 20 // val res2: Int = 10 + +The final results are different. We conclude that `x` and `y` are not the same. + +Establishing Operational Equivalence +==================================== + +On the other hand, if we define + + val x = new BankAccount + val y = x + +then no sequence of operations can distinguish between `x` and `y`, so +`x` and `y` are the same in this case. + +Assignment and Substitution Model +================================= + +The preceding examples show that our model of computation by +substitution cannot be used. + +Indeed, according to this model, one can always replace the name of a value by the expression that defines it. For example, in + + val x = new BankAccount + val y = x + +the `x` in the definition of `y` could be replaced by `new BankAccount` + +Assignment and The Substitution Model +===================================== + +The preceding examples show that our model of computation by +substitution cannot be used. + +Indeed, according to this model, one can always replace the name of a value by the expression that defines it. For example, in + + val x = new BankAccount val x = new BankAccount + val y = x val y = new BankAccount + +the `x` in the definition of `y` could be replaced by `new BankAccount` + +But we have seen that this change leads to a different program! + +The substitution model ceases to be valid when we add the assignment. + +It is possible to adapt the substitution model by introducing a \red{store}, +but this becomes considerably more complicated. +\bigskip\bigskip + diff --git a/lectures/progfun2-3-3.md b/lectures/progfun2-3-3.md new file mode 100644 index 0000000000000000000000000000000000000000..8a2d173093c0ec48e66ac29a6061804df1f259fb --- /dev/null +++ b/lectures/progfun2-3-3.md @@ -0,0 +1,109 @@ +% Loops +% Martin Odersky +% + +Loops +===== + +\red{Proposition:} Variables are enough to model all imperative programs. + +But what about control statements like loops? + +We can model them using functions. + +\example: Here is a Scala program that uses a `while` loop: + + def power (x: Double, exp: Int): Double = { + var r = 1.0 + var i = exp + while (i > 0) { r = r * x; i = i - 1 } + r + } + +In Scala, `while` is a keyword. + +But how could we define `while` using a function (call it `WHILE`)? + + +Definition of `while` +==================== + +The function `WHILE` can be defined as follows: + + def WHILE(condition: => Boolean)(command: => Unit): Unit = + if condition then { + command + WHILE(condition)(command) + } + else () + +\red{Note:} The condition and the command must be passed by name so that they're reevaluated in each iteration. + +\red{Note:} `WHILE` is tail recursive, so it can operate with a constant +stack size. + +Exercise +======== + +Write a function implementing a `repeat` loop that is used as follows: + + REPEAT { + command + } ( condition ) + +It should execute `command` one or more times, until `condition` is true. +\quiz +-> +The `REPEAT` function starts like this: + + def REPEAT(command: => Unit)(condition: => Boolean) = + +Exercise (open-ended) +===================== + +Is it also possible to obtain the following syntax? + + REPEAT { + command + } UNTIL ( condition ) + +? + + +For-Loops +========= + +The classical `for` loop in Java can _not_ be modeled simply by +a higher-order function. + +The reason is that in a Java program like + + for (int i = 1; i < 3; i = i + 1) { System.out.print(i + " "); } + +the arguments of `for` contain the \red{declaration} of the variable +`i`, which is visible in other arguments and in the body. + +However, in Scala there is a kind of `for` loop similar to Java's extended for loop: + + for (i <- 1 until 3) { System.out.print(i + " ") } + +This displays `1 2`. + +Translation of For-Loops +======================== + +For-loops translate similarly to for-expressions, but using the `foreach` combinator instead of `map` and `flatMap`. + +`foreach` is defined on collections with elements of type `T` as follows: + + def foreach(f: T => Unit): Unit = + // apply `f` to each element of the collection + +\example + + for (i <- 1 until 3; j <- "abc") println(i + " " + j) + +translates to: + + (1 until 3) foreach (i => "abc" foreach (j => println(i + " " + j))) + diff --git a/lectures/progfun2-3-4.md b/lectures/progfun2-3-4.md new file mode 100644 index 0000000000000000000000000000000000000000..cd27a203a80e519ae732dc49bd9b5c298b0399ce --- /dev/null +++ b/lectures/progfun2-3-4.md @@ -0,0 +1,127 @@ +% Imperative Event Handling: The Observer Pattern +% Martin Odersky +% +The Observer Pattern +==================== + +The Observer Pattern is widely used when views need to react to changes in +a model. + +Variants of it are also called + + - publish/subscribe + - model/view/controller (MVC). + + +A Publisher Trait +================= + + trait Publisher { + + private var subscribers: Set[Subscriber] = Set() + + def subscribe(subscriber: Subscriber): Unit = + subscribers += subscriber + + def unsubscribe(subscriber: Subscriber): Unit = + subscribers -= subscriber + + def publish(): Unit = + subscribers.foreach(_.handler(this)) + } + +A Subscriber Trait +================== + + trait Subscriber { + def handler(pub: Publisher) + } + + +Observing Bank Accounts +======================= + +Let's make `BankAccount` a `Publisher`: + + class BankAccount extends Publisher { + private var balance = 0 + + def deposit(amount: Int): Unit = + if amount > 0 then { + balance = balance + amount + + } + def withdraw(amount: Int): Unit = + if 0 < amount && amount <= balance then { + balance = balance - amount + + } else throw new Error("insufficient funds") + } + +Observing Bank Accounts +======================= + +Let's make `BankAccount` a `Publisher`: + + class BankAccount extends Publisher { + private var balance = 0 + def currentBalance: Int = balance // <--- + def deposit(amount: Int): Unit = + if amount > 0 then { + balance = balance + amount + publish() // <--- + } + def withdraw(amount: Int): Unit = + if 0 < amount && amount <= balance then { + balance = balance - amount + publish() // <--- + } else throw new Error("insufficient funds") + } + + +An Observer +=========== + +A `Subscriber` to maintain the total balance of a list of accounts: + + class Consolidator(observed: List[BankAccount]) extends Subscriber { + observed.foreach(_.subscribe(this)) + + private var total: Int = _ + compute() + + private def compute() = + total = observed.map(_.currentBalance).sum + + def handler(pub: Publisher) = compute() + + def totalBalance = total + } + +Observer Pattern, The Good +========================== + + - Decouples views from state + - Allows to have a varying number of views of a given state + - Simple to set up + +Observer Pattern, The Bad +========================= + + - Forces imperative style, since handlers are `Unit`-typed + - Many moving parts that need to be co-ordinated + - Concurrency makes things more complicated + - Views are still tightly bound to one state; view update happens immediately. + +To quantify (Adobe presentation from 2008): + + - $1/3^{rd}$ of the code in Adobe's desktop applications is devoted to event handling. + - $1/2$ of the bugs are found in this code. + + +How to Improve? +=============== + +During the rest of this week we will explore a different way, namely +*functional reactive programming*, in which we can improve on the +imperative view of reactive programming embodied in the observer pattern. diff --git a/lectures/progfun2-3-5.md b/lectures/progfun2-3-5.md new file mode 100644 index 0000000000000000000000000000000000000000..268f5355a0f58c689d68948323294211f6afc1e6 --- /dev/null +++ b/lectures/progfun2-3-5.md @@ -0,0 +1,221 @@ +% Functional Reactive Programming +% Martin Odersky +% +What is FRP? +============ + +Reactive programming is about reacting to sequences of _events_ that happen in _time_. + +Functional view: Aggregate an event sequence into a _signal_. + +- A signal is a value that changes over time. + +- It is represented as a function from time to the value domain. + +- Instead of propagating updates to mutable state, we define new signals in terms of existing ones. + +Example: Mouse Positions +======================== + +\red{Event-based view:} + +Whenever the mouse moves, an event + + MouseMoved(toPos: Position) + +is fired. + +\red{FRP view:} + +A signal, + + mousePosition: Signal[Position] + +which at any point in time represents the current mouse position. + + +Origins of FRP +============== + +FRP started in 1997 with the paper [Functional Reactive Animation](http://conal.net/papers/icfp97/) +by Conal Elliott and Paul Hudak and the [Fran](http://conal.net/fran/tutorial.htm) library. + +There have been many FRP systems since, both standalone languages and embedded libraries. + +Some examples are: [Flapjax](http://www.flapjax-lang.org/), [Elm](http://elm-lang.org/), [Bacon.js](https://baconjs.github.io/), +[React4J](https://bitbucket.org/yann_caron/react4j/wiki/Home). + +Event streaming dataflow programming systems such as Rx (which we will see in two weeks), +are related but the term FRP is not commonly used for them. + +We will introduce FRP by means of of a minimal class, `frp.Signal` whose implementation is explained at the end of this module. + +frp.Signal is modelled after Scala.react, which is described in the paper [Deprecating the Observer Pattern](http://infoscience.epfl.ch/record/176887/files/DeprecatingObservers2012.pdf). + + +Fundamental Signal Operations +============================= + +There are two fundamental operations over signals: + +$1.$ Obtain the value of the signal at the current time.\newline +In our library this is expressed by `()` application. + + + mousePosition() // the current mouse position + +-> + +$2.$ Define a signal in terms of other signals.\newline +\tab In our library, this is expressed by the `Signal` constructor. + + + def inReactangle(LL: Position, UR: Position): Signal[Boolean] = + Signal { + val pos = mousePosition() + LL <= pos && pos <= UR + } + + +Constant Signals +================ + +The `Signal(...)` syntax can also be used to define a signal that has always the same +value: + + val sig = Signal(3) // the signal that is always 3. + +Time-Varying Signals +==================== + +How do we define a signal that varies in time? + + - We can use externally defined signals, such as `mousePosition` +and map over them. + + - Or we can use a `Var`. + +Variable Signals +================ + +Values of type `Signal` are immutable. + +But our library also defines a subclass `Var` of `Signal` for signals that +can be changed. + +`Var` provides an "update" operation, which allows to redefine +the value of a signal from the current time on. + + val sig = Var(3) + sig.update(5) // From now on, sig returns 5 instead of 3. + +Aside: Update Syntax +==================== + +In Scala, calls to `update` can be written as assignments. + +For instance, for an array `arr` + + arr(i) = 0 + +is translated to + + arr.update(i, 0) + +which calls an `update` method which can be thought of as follows: + + class Array[T] { + def update(idx: Int, value: T): Unit + ... + } + +Aside: Update Syntax +==================== + +Generally, an indexed assignment like +$\btt f(E_1, ..., E_n) = E$ + +is translated to +$\btt f.update(E_1, ..., E_n, E)$. + +This works also if $n = 0$: $\btt f() = E$ is shorthand for $\btt f.update(E)$. + +Hence, + + sig.update(5) + +can be abbreviated to + + sig() = 5 + +Signals and Variables +===================== + +Signals of type `Var` look a bit like mutable variables, where + + sig() + +is dereferencing, and + + sig() = newValue + +is update. + +But there's a crucial difference: + +We can _map_ over signals, which gives us a relation between two signals that is maintained +automatically, at all future points in time. + +No such mechanism exists for mutable variables; we have to propagate all updates manually. + +Example +======= + +Repeat the `BankAccount` example of last section with signals. + +Add a signal `balance` to `BankAccount`s. + +Define a function `consolidated` which produces the sum of all balances +of a given list of accounts. + +What savings were possible compared to the publish/subscribe implementation? + +Signals and Variables (2) +========================= + +Note that there's an important difference between the variable assignment + + v = v + 1 + +and the signal update + + s() = s() + 1 + +In the first case, the _new_ value of `v` becomes the _old_ value of `v` plus 1. + +In the second case, we try define a signal `s` to be _at all points in time_ one larger than itself. + +This obviously makes no sense! + + +Exercise +======== + +Consider the two code fragments below + +(1) + val num = Signal(1) + val twice = Signal(num() * 2) + num() = 2 + +(2) + var num = Signal(1) + val twice = Signal(num() * 2) + num = Signal(2) + +So they yield the same final value for `twice()`? + + O yes + O no + + \ No newline at end of file diff --git a/lectures/progfun2-3-6.md b/lectures/progfun2-3-6.md new file mode 100644 index 0000000000000000000000000000000000000000..479ba34fcd4edde8cd429a00bf2b797d040e3c2d --- /dev/null +++ b/lectures/progfun2-3-6.md @@ -0,0 +1,285 @@ +% A Simple FRP Implementation +% Martin Odersky +% + +A Simple FRP Implementation +=========================== + +We now develop a simple implementation of `Signal`s and `Var`s, which together +make up the basis of our approach to functional reactive programming. + +The classes are assumed to be in a package `frp`. + +Their user-facing APIs are summarized in the next slides. + +Summary: The Signal API +======================= + + class Signal[T](expr: => T) { + def apply(): T = ??? + } + object Signal { + def apply[T](expr: => T) = new Signal(expr) + } + +Summary: The Var API +==================== + + class Var[T](expr: => T) extends Signal[T](expr) { + def update(expr: => T): Unit = ??? + } + + object Var { + def apply[T](expr: => T) = new Var(expr) + } + +Implementation Idea +=================== + +Each signal maintains + + - its current value, + - the current expression that defines the signal value, + - a set of _observers_: the other signals that depend on its value. + +Then, if the signal changes, all observers need to be re-evaluated. + +Dependency Maintenance +====================== + +How do we record dependencies in `observers`? + + - When evaluating a signal-valued expression, need to know which + signal `caller` gets defined or updated by the expression. + + - If we know that, then executing a `sig()` means adding `caller` to the + `observers` of `sig`. + + - When signal `sig`'s value changes, all previously observing signals are + re-evaluated and the set `sig.observers` is cleared. + + - Re-evaluation will re-enter a calling signal `caller` in `sig.observers`, + as long as `caller`'s value still depends on `sig`. + +Who's Calling? +============== + +How do we find out on whose behalf a signal expression is evaluated? + +One simple (simplistic?) way to do this is to maintain a global data structure referring to +the current caller. (We will discuss and refine this later). + +That data structure is accessed in a stack-like fashion because one evaluation of +a signal might trigger others. + +Stackable Variables +=================== + +Here's a class for stackable variables: + + class StackableVariable[T](init: T) { + private var values: List[T] = List(init) + def value: T = values.head + def withValue[R](newValue: T)(op: => R): R = { + values = newValue :: values + try op finally values = values.tail + } + } + +You access it like this + + val caller = new StackableVar(initialSig) + caller.withValue(otherSig) { ... } + ... caller.value ... + + +Set Up in Object `Signal` +======================== + +We also evaluate signal expressions at the top-level when there is no other signal +that's defined or updated. + +We use the "sentinel" object `NoSignal` as the `caller` for these expressions. + +Together: + + object NoSignal extends Signal[Nothing](???) { ... } + + object Signal { + val caller = new StackableVariable[Signal[_]](NoSignal) + def apply[T](expr: => T) = new Signal(expr) + } + +The `Signal` Class +================== + + class Signal[T](expr: => T) { + import Signal._ + private var myExpr: () => T = _ + private var myValue: T = _ + private var observers: Set[Signal[_]] = Set() + update(expr) +-> + protected def update(expr: => T): Unit = { + myExpr = () => expr + computeValue() + } +-> + protected def computeValue(): Unit = { + myValue = caller.withValue(this)(myExpr()) + } + +The Signal Class, ctd +===================== + + def apply() = { + observers += caller.value + assert(!caller.value.observers.contains(this), "cyclic signal definition") + myValue + } + } + +Exercise +======== + +The `Signal` class still lacks an essential part. Which is it? + + O Error handling + O Reevaluating callers + O Constructing observers + +Reevaluating Callers +==================== + +A signal's current value can change when + + - somebody calls an update operation on a `Var`, or + - the value of a dependent signal changes + +Propagating requires a more refined implementation of `computeValue`: + + protected def computeValue(): Unit = + myValue = caller.withValue(this)(myExpr()) + +Reevaluating Callers +==================== + +A signal's current value can change when + + - somebody calls an update operation on a `Var`, or + - the value of a dependent signal changes + +Propagating changes requires a more refined implementation of `computeValue`: + + protected def computeValue(): Unit = { + val newValue = caller.withValue(this)(myExpr()) + if myValue != newValue then { + myValue = newValue + val obs = observers + observers = Set() + obs.foreach(_.computeValue()) + } + } + +Handling NoSignal +================= + +`computeValue` needs to be disabled for `NoSignal` because we cannot evaluate +an expression of type `Nothing`: + + object NoSignal extends Signal[Nothing](???) { + override def computeValue() = () + } + +Handling `Var`s +=============== + +Recall that `Var` is a `Signal` which can be updated by the client program. + +In fact, all necessary functionality is already present in class `Signal`; we just need to expose it: + + class Var[T](expr: => T) extends Signal[T](expr) { + override def update(expr: => T): Unit = super.update(expr) + } + + object Var { + def apply[T](expr: => T) = new Var(expr) + } + +Discussion +========== + +Our implementation of FRP is quite stunning in its simplicity. + +But you might argue that it is too simplistic. + +In particular, it makes use of the worst kind of state: global state. + + object Signal { + val caller = new StackableVariable[Signal[_]](NoSignal) + ... + } + +One immediate problem is: What happens if we try to evaluate several signal +expressions in parallel? +-> + - The caller signal will become "garbled" by concurrent updates. + +Thread-Local State +================== + +One way to get around the problem of concurrent accesses to global state is to +use synchronization. + +But this blocks threads, can be slow, and can lead to deadlocks. + +Another solution is to replace global state by thread-local state. + + - Thread-local state means that each thread accesses a separate copy of a variable. + + - It is supported in Scala through class `scala.util.DynamicVariable`. + +Using Thread-Local State +======================== + +The API of `DynamicVariable` matches the one of `StackableVariable` + +So we can simply swap it into our `Signal` implementation: + + object Signal { + val caller = new DynamicVariable[Signal[_]](NoSignal) + ... + } + +Another Solution: Implicit Parameters +===================================== + +Thread-local state still comes with a number of disadvantages: + + - Its imperative nature often produces hidden dependencies which are hard to manage. + - Its implementation on the JDK involves a global hash table lookup, which can be a performance problem. + - It does not play well in situations where threads are multiplexed between several tasks. +-> +A cleaner solution involves implicit parameters. + + - Instead of maintaining a thread-local variable, pass its current value into + a signal expression as an implicit parameter. + - This is purely functional. But it currently requires more boilerplate than the thread-local solution. + - Future versions of Scala might solve that problem. + +Summary +======= + +We have given a quick tour of functional reactive programming, +with some usage examples and an implementation. + +This is just a taster, there's much more to be discovered. + +In particular, we only covered one particular style of FRP: Discrete +signals changed by events. + +Some variants of FRP also treat continuous signals. + +Values in these systems are often computed by sampling instead of +event propagation. + diff --git a/lectures/progfun2-4-1.md b/lectures/progfun2-4-1.md new file mode 100644 index 0000000000000000000000000000000000000000..7a04b63e834c9a3203a1d55d996709f5f42c14d2 --- /dev/null +++ b/lectures/progfun2-4-1.md @@ -0,0 +1,127 @@ +% Implicit Programming — Motivating Example +% +% + +Sorting Lists of Numbers +======================== + +Consider a method `sort` that takes as parameter a `List[Int]` and +returns another `List[Int]` containing the same elements, but sorted: + +~~~ +def sort(xs: List[Int]): List[Int] = { + ... + ... if x < y then ... + ... +} +~~~ + +At some point, this method has to compare two elements `x` and `y` +of the given list. + +Making `sort` more General +========================== + +Problem: How to parameterize `sort` so that it can also be +used for lists with elements other than `Int`, such as `Double` +or `String`? + +A straightforward approach would be to use a polymorphic type +`A` for the type of elements: + +~~~ +def sort[A](xs: List[A]): List[A] = ... +~~~ + +But this does not work, because the comparison `<` is not defined for +all arbitrary types `A`. + +Parameterization of `sort` +========================== + +The most flexible design is to pass the comparison operation +as an additional parameter: + +~~~ +def sort[A](xs: List[A])(lessThan: (A, A) => Boolean): List[A] = { + ... + ... if lessThan(x, y) then ... + ... +} +~~~ + +Calling Parameterized `sort` +============================ + +We can now call `sort` as follows: + +~~~ +scala> val xs = List(-5, 6, 3, 2, 7) +scala> val strings = List("apple", "pear", "orange", "pineapple") + +scala> sort(xs)((x, y) => x < y) +res0: List[Int] = List(-5, 2, 3, 6, 7) + +scala> sort(strings)((f1, f2) => f1.compareTo(f2) < 0) +res1: List[String] = List(apple, orange, pear, pineapple) +~~~ + +Parameterization with Ordering +============================== + +There is already a class in the standard library that represents orderings: + +~~~ +scala.math.Ordering[A] +~~~ + +Provides ways to compare elements of type `A`. So, instead of +parameterizing with the `lessThan` function, we could parameterize +with `Ordering` instead: + +~~~ +def sort[A](xs: List[A])(ord: Ordering[A]): List[A] = { + ... + ... if ord.lt(x, y) then ... + ... +} +~~~ + +Ordering Instances +================== + +Calling the new `sort` can be done like this: + +~~~ +import scala.math.Ordering + +sort(xs)(Ordering.Int) +sort(strings)(Ordering.String) +~~~ + +This makes use of the values `Int` and `String` defined in the +`scala.math.Ordering` object, which produce the right +orderings on integers and strings. + +~~~ +object Ordering { + val Int = new Ordering[Int] { + def lt(x: Int, y: Int) = x - y < 0 + } +} +~~~ + +Reducing Boilerplate +==================== + +Problem: Passing around `Ordering` values is cumbersome. + +~~~ +sort(xs)(Ordering.Int) +sort(ys)(Ordering.Int) +sort(strings)(Ordering.String) +~~~ + +Sorting a `List[Int]` instance always uses the same `Ordering.Int` value, +sorting a `List[String]` instance always uses the same `Ordering.String` +value, and so on… diff --git a/lectures/progfun2-4-2.md b/lectures/progfun2-4-2.md new file mode 100644 index 0000000000000000000000000000000000000000..0013ab1f437cd394ef0de549428548393084497f --- /dev/null +++ b/lectures/progfun2-4-2.md @@ -0,0 +1,255 @@ +% Implicits +% +% + +Reminder: General `sort` Operation +================================== + +~~~ +def sort[A](xs: List[A])(ord: Ordering[A]): List[A] = ... +~~~ + +Problem: Passing around `Ordering` values is cumbersome. + +~~~ +sort(xs)(Ordering.Int) +sort(ys)(Ordering.Int) +sort(strings)(Ordering.String) +~~~ + +Sorting a `List[Int]` instance always uses the same `Ordering.Int` value, +sorting a `List[String]` instance always uses the same `Ordering.String` +value, and so on… + +Implicit Parameters +=================== + +We can reduce the boilerplate by making `ord` an **implicit** parameter. + +~~~ +def sort[A](xs: List[A])(implicit ord: Ordering[A]): List[A] = ... +~~~ + +- A method can have only one implicit parameter list, and it must be the last + parameter list given. + +Then calls to `sort` can omit the `ord` parameter: + +~~~ +sort(xs) +sort(ys) +sort(strings) +~~~ + +The compiler infers the implicit parameter value based on the +**queried type**. + +Implicit Parameters (2) +======================= + +~~~ +def sort[A](xs: List[A])(implicit ord: Ordering[A]): List[A] = ... + +val xs: List[Int] = ... +~~~ + +-> + +~~~ +sort(xs) +~~~ + +-> + +~~~ +sort[Int](xs) +~~~ + +-> + +~~~ +sort[Int](xs)(Ordering.Int) +~~~ + +In this case, the queried type is `Ordering[Int]`. + +Implicit Parameters Resolution +============================== + +Say, a function takes an implicit parameter of type `T`. + +The compiler will search an implicit **definition** that: + +- is marked `implicit`, +- has a type compatible with `T`, +- is visible at the point of the function call, or is defined + in a companion object associated with `T`. + +If there is a single (most specific) definition, it will be taken +as actual arguments for the implicit parameter. + +Otherwise it’s an error. + +Implicit Definitions +==================== + +For the previous example to work, the `Ordering.Int` value definition +must be marked `implicit`: + +~~~ +object Ordering { + + implicit val Int: Ordering[Int] = ... + +} +~~~ + +Implicit Search +=============== + +The implicit search for a type `T` includes: + +- all the implicit definitions that are visible (inherited, imported, + or defined in an enclosing scope), +- the *implicit scope* of type `T`, made of implicit definitions found + in a companion object *associated* with `T`. In essence$^*$, the types + associated with a type `T` are: + - if `T` is a compound type $T_1 with T_2 ... with T_n$, the union + of the parts of $T_1$, ... $T_n$ as well as $T$ itself, + - if `T` is a parameterized type $S[T_1, T_2, ..., T_n]$, the union + of the parts of $S$ and $T_1$, ..., $T_n$, + - otherwise, just `T` itself. + +In the case of the `sort(xs)` call, the compiler looks for an implicit +`Ordering[Int]` definition, which is found in the `Ordering` companion +object. + +Implicit Not Found +================== + +If there is no available implicit definition matching the queried type, +an error is reported: + +~~~ +scala> def f(implicit n: Int) = () +scala> f + ^ +error: could not find implicit value for parameter n: Int +~~~ + +Ambiguous Implicit Definitions +============================== + +If more than one implicit definition are eligible, an **ambiguity** is reported: + +~~~ +scala> implicit val x: Int = 0 +scala> implicit val y: Int = 1 +scala> def f(implicit n: Int) = () +scala> f + ^ +error: ambiguous implicit values: + both value x of type => Int + and value y of type => Int + match expected type Int +~~~ + +Priorities +========== + +Actually, several implicit definitions matching the same type don’t generate an +ambiguity if one is **more specific** than the other. + +In essence$^{*}$, a definition `a: A` is more specific than a definition `b: B` if: + +- type `A` is a subtype of type `B`, +- type `A` has more “fixed†parts, +- `a` is defined in a class or object which is a subclass of the class defining `b`. + +Priorities: Example (1) +======================= + +Which implicit definition matches the queried `Int` implicit parameter when +the `f` method is called? + +~~~ +implicit def universal[A]: A = ??? +implicit def int: Int = ??? + +def f(implicit n: Int) = () + +f +~~~ + +Priorities: Example (2) +======================= + +Which implicit definition matches the queried `Int` implicit parameter when +the `f` method is called? + +~~~ +trait A { + implicit val x: Int = 0 +} +trait B extends A { + implicit val y: Int = 1 + + def f(implicit n: Int) = () + + f +} +~~~ + +Context Bounds +============== + +A syntactic sugar allows the omission of the implicit parameter list: + +~~~ +def printSorted[A: Ordering](as: List[A]): Unit = { + println(sort(as)) +} +~~~ + +Type parameter `A` has one **context bound**: `Ordering`. There must be an +implicit value with type `Ordering[A]` at the point of application. + +More generally, a method definition such as: + +$def f[A: U_1 ... : U_n](ps): R = ...$ + +Is expanded to: + +$def f[A](ps)(implicit ev_1: U_1[A], ..., ev_n: U_n[A]): R = ...$ + +Implicit Query +============== + +At any point in a program, one can **query** an implicit value of +a given type by calling the `implicitly` operation: + +~~~ +scala> implicitly[Ordering[Int]] +res0: Ordering[Int] = scala.math.Ordering$Int$@73564ab0 +~~~ + +`implicitly` is not a special keyword, it is defined as a library operation: + +~~~ +def implicitly[A](implicit value: A): A = value +~~~ + +Summary +======= + +In this lecture we have introduced the concept of **implicit programming**, +a language mechanism that infers **values** by using **type** information. + +There has to be a **unique** implicit definition matching the queried type +for it to be used by the compiler. + +Implicit values are searched in the enclosing **lexical scope** (imports, +parameters, inherited members) as well as in the **implicit scope** made +of implicits defined in companion objects of types associated with the +queried type. + diff --git a/lectures/progfun2-4-3.md b/lectures/progfun2-4-3.md new file mode 100644 index 0000000000000000000000000000000000000000..ed712bcc118ca09ab341a9079c6a55d73e17b5b4 --- /dev/null +++ b/lectures/progfun2-4-3.md @@ -0,0 +1,173 @@ +% Type Classes vs Inheritance +% +% + +Type Classes +============ + +In the previous lectures we have seen a particular pattern of code +to achieve *ad hoc* polymorphism: + +~~~ +trait Ordering[A] { def lt(a1: A, a2: A): Boolean } + +object Ordering { + implicit val Int: Ordering[Int] = (x, y) => x < y + implicit val String: Ordering[String] = (s, t) => (s compareTo t) < 0 +} + +def sort[A: Ordering](xs: List[A]): List[A] = ... +~~~ + +We say that `Ordering` is a **type class**. + +Alternatively, Using *Subtyping Polymorphism* +============================================= + +~~~ +trait Ordered { + def lt(other: Ordered): Boolean +} +~~~ + +~~~ +def sort2(xs: List[Ordered]): List[Ordered] = { + ... + ... if x lt y then ... + ... +} +~~~ + +How do these approaches compare? + +Usage +===== + +(Assuming that `Int <: Ordered`) + +~~~ +val sortedInts: List[Int] = sort2(ints) +~~~ + +-> + +~~~ + ^^^^^^^^^^^ +error: type mismatch; + found : List[Ordered] + required: List[Int] +~~~ + +-> + +~~~ +def sort2(xs: List[Ordered]): List[Ordered] +~~~ + +First Improvement +================= + +~~~ +def sort2[A <: Ordered](xs: List[A]): List[A] = // ... same implementation +~~~ + +-> + +~~~ +val sortedInts: List[Int] = sort2(ints) // OK +val sortedString: List[String] = sort2(strings) // OK +~~~ + +... assuming `Int <: Ordered` and `String <: Ordered`! + +Subtyping Polymorphism Causes Strong Coupling +============================================= + +Subtyping polymorphism imposes a strong coupling between *operations* +(`Ordered`) and *data types* (`Int`, `String`) that support them. + +By contrast, type classes encourage separating the definition of data types +and the operations they support: the `Ordering[Int]` and `Ordering[String]` +instances don’t have to be defined in the same unit as the `Int` and `String` +data types. + +Type classes support *retroactive* extension: the ability to extend a data +type with new operations without changing the original definition of the data type. + +Implementing an Operation for a Custom Data Type +================================================ + +~~~ +case class Rational(num: Int, denom: Int) +~~~ + +-> + +~~~ +case class Rational(num: Int, denom: Int) extends Ordered { + def lt(other: Ordered) = other match { + case Rational(otherNum, otherDenom) => num * otherDenom < otherNum * denom + case _ => ??? + } +} +~~~ + +-> + +We want to compare rational numbers with other rational numbers only! + +Second Improvement: F-Bounded Polymorphism +========================================== + +~~~ +trait Ordered[This <: Ordered[This]] { + def lt(other: This): Boolean +} + +case class Rational(num: Int, denom: Int) extends Ordered[Rational] { + def lt(other: Rational) = num * other.denom < other.num * denom +} + +def sort2[A <: Ordered[A]](xs: List[A]): List[A] = { + ... + ... if x lt y then ... + ... +} +~~~ + +Implementing an Operation for a Custom Data Type (2) +==================================================== + +~~~ +case class Rational(num: Int, denom: Int) + +object Rational { + implicit val ordering: Ordering[Rational] = + (x, y) => x.num * y.denom < y.num * x.denom +} + +sort(Rational(1, 2), Rational(1, 3)) +~~~ + +-> + +(The `Ordering[Rational]` instance definition could be in a different project) + +Dispatch Time +============= + +Another difference between subtyping polymorphism and type classes is **when** +the dispatch happens. + +- With type classes, the implicit instance is resolved at **compilation time**, +- With subtyping polymorphism, the actual implementation of a method is resolved + at **run time**. + +Summary +======= + +In this lecture we have seen that type classes support retroactive +extensibility. + +The dispatch happens at compile time with type classes, whereas it +happens at run time with subtype polymorphism. diff --git a/lectures/progfun2-4-4-type-level.md b/lectures/progfun2-4-4-type-level.md new file mode 100644 index 0000000000000000000000000000000000000000..e462024fefdca802a3b285e262b1418b0c18167d --- /dev/null +++ b/lectures/progfun2-4-4-type-level.md @@ -0,0 +1,203 @@ +% Type-Level Programming +% +% + +Inductive Implicits (1) +======================= + +An arbitrary number of implicit definitions can be combined +until the search hits a “terminal†definition: + +~~~ +implicit def a: A = ... +implicit def aToB(implicit a: A): B = ... +implicit def bToC(implicit b: B): C = ... +implicit def cToD(implicit c: C): D = ... + +implicitly[D] +~~~ + +Inductive Implicits (2) +======================= + +~~~ +trait Nat +trait Z extends Nat +trait S[N <: Nat] extends Nat +~~~ + +~~~ +implicit def zero: Z = null +implicit def one (implicit zero: Z): S[Z] = null +implicit def two (implicit one: S[Z]): S[S[Z]] = null +implicit def three(implicit two: S[S[Z]]): S[S[S[Z]]] = null + +implicitly[S[S[S[Z]]]] +~~~ + +Inductive Implicits (3) +======================= + +~~~ +trait Nat +trait Z extends Nat +trait S[N <: Nat] extends Nat +~~~ + +~~~ +implicit def zero: Z = null +implicit def succ[N <: Nat](implicit n: N): S[N] = null + +implicitly[S[S[S[Z]]]] +~~~ + +Recursive Implicits +=================== + +~~~ +trait A +implicit def loop(implicit a: A): A = a + +implicitly[A] +~~~ + +-> + +~~~ + ^ +error: diverging implicit expansion for type A +starting with method loop +~~~ + +Computing Types +=============== + +~~~ +trait Water +trait Ice + +trait Melted[A, R] + +object Melted { + implicit def meltedIce: Melted[Ice, Water] = null +} + +def meltedIce[R](implicit m: Melted[Ice, R]): R = ??? +~~~ + +~~~ +> def water = meltedIce +water: Water +~~~ + +From Types to Values (1) +======================== + +~~~ +type `0` = Z +type `1` = S[`0`] +type `2` = S[`1`] +type `3` = S[`2`] +... +~~~ + +~~~ +val x: `2` = `2` + ^ +error: not found: value `2` +~~~ + +From Types to Values (2) +======================== + +~~~ +case class ValueOf[N <: Nat] private (get: Int) + +object ValueOf { + + implicit def base: ValueOf[Z] = ValueOf(0) + + implicit def induc[A <: Nat](implicit h: ValueOf[A]): ValueOf[S[A]] = + ValueOf(h.get + 1) + +} +~~~ + +~~~ +> implicitly[ValueOf[`3`]].get +res0: Int = 3 +~~~ + +Example: Sized Collections (1) +============================== + +~~~ +case class Sized[N <: Nat] private (elems: Seq[Int]) { + + def + (other: Sized[N]): Sized[N] = + Sized(elems.zip(other.elems).map { case (x, y) => x + y }) + +} +~~~ + +~~~ +def usage(xs: Sized[`3`], ys: Sized[`3`], zs: Sized[`4`]) = { + xs + ys // OK + xs + zs // Error: type mismatch; + // found : Sized[`4`] + // required: Sized[`3`] +} +~~~ + +Example: Sized Collections (2) +============================== + +~~~ +case class Sized[N <: Nat] private (elems: Seq[Int]) { + + def concat[M <: Nat](other: Sized[M]): Sized[???] + +} +~~~ + +Example: Sized Collections (3) +============================== + +~~~ +case class Sized[N <: Nat] private (elems: Seq[Int]) { + + def concat[M <: Nat, S <: Nat]( + other: Sized[M] + )(implicit + sum: Sum[N, M, S] + ): Sized[S] = + Sized(elems ++ other.elems) + +} +~~~ + +Example: Sized Collections (4) +============================== + +~~~ +trait Sum[A <: Nat, B <: Nat, R <: Nat] + +object Sum { + + implicit def base[B <: Nat]: Sum[Z, B, B] = null + + implicit def induc[A <: Nat, B <: Nat, R <: Nat](implicit + h: Sum[A, B, R] + ): Sum[S[A], B, S[R]] = null + +} +~~~ + +Summary +======= + +In this video, we have seen: + +- implicit definitions can be **inductive** +- types can be computed according to the result of the implicit + search diff --git a/lectures/progfun2-4-4.md b/lectures/progfun2-4-4.md new file mode 100644 index 0000000000000000000000000000000000000000..aea70840a2605bba1ab2a71f7c8130f1b7c101b1 --- /dev/null +++ b/lectures/progfun2-4-4.md @@ -0,0 +1,118 @@ +% Higher-Order Implicits +% +% + +Higher-Order Implicits (1) +========================== + +Consider how we order two `String` values: + +- `"abc" < "abd"`? + +\vspace{3cm} + +-> We compare the characters of each string, element-wise. + +-> **Problem**: How to generalize this process to sequences of any + element type `A` for which there is an implicit `Ordering[A]` + instance? + +Higher-Order Implicits (2) +========================== + +~~~ +implicit def listOrdering[A](implicit + ord: Ordering[A] +): Ordering[List[A]] = ... +~~~ + +Higher-Order Implicits (3) +========================== + +~~~ +implicit def listOrdering[A](implicit + ord: Ordering[A] +): Ordering[List[A]] = { (xs0, ys0) => + def loop(xs: List[A], ys: List[A]): Boolean = (xs, ys) match { + case (x :: xsTail, y :: ysTail) => ord.lt(x, y) && loop(xsTail, ysTail) + case (xs, ys) => ys.nonEmpty + } + loop(xs0, ys0) +} +~~~ + +~~~ +scala> sort(List(List(1, 2, 3), List(1), List(1, 1, 3))) +res0: List[List[Int]] = List(List(1), List(1, 1, 3), List(1, 2, 3)) +~~~ + +Higher-Order Implicits (4) +========================== + +~~~ +def sort[A](xs: List[A])(implicit ord: Ordering[A]): List[A] +implicit def listOrdering[A](implicit ord: Ordering[A]): Ordering[List[A]] + +val xss: List[List[Int]] = ... +sort(xss) +~~~ + +-> + +~~~ +sort[List[Int]](xss) +~~~ + +-> + +~~~ +sort[List[Int]](xss)(listOrdering) +~~~ + +-> + +~~~ +sort[List[Int]](xss)(listOrdering(Ordering.Int)) +~~~ + +Higher-Order Implicits (5) +========================== + +An arbitrary number of implicit definitions can be combined +until the search hits a “terminal†definition: + +~~~ +implicit def a: A = ... +implicit def aToB(implicit a: A): B = ... +implicit def bToC(implicit b: B): C = ... +implicit def cToD(implicit c: C): D = ... + +implicitly[D] +~~~ + +Recursive Implicits +=================== + +~~~ +trait A +implicit def loop(implicit a: A): A = a + +implicitly[A] +~~~ + +-> + +~~~ + ^ +error: diverging implicit expansion for type A +starting with method loop +~~~ + +Summary +======= + +In this lecture, we have seen: + +- implicit definitions can also take implicit parameters +- an arbitrary number of implicitly definitions can be chained + until a terminal definition is reached diff --git a/lectures/progfun2-4-5.md b/lectures/progfun2-4-5.md new file mode 100644 index 0000000000000000000000000000000000000000..193b441c9979c9f8e3af358f6ff75f41478c9cae --- /dev/null +++ b/lectures/progfun2-4-5.md @@ -0,0 +1,219 @@ +% Implicit Conversions +% +% + +Implicit Conversions +==================== + +The last implicit-related mechanism of the language is implicit +**conversions**. + +They make it possible to convert an expression to a different +type. + +This mechanism is usually used to provide more ergonomic APIs: + +~~~ +// { "name": "Paul", "age": 42 } +Json.obj("name" -> "Paul", "age" -> 42) + + +val delay = 15.seconds +~~~ + +Type Coercion: Motivation (1) +============================= + +~~~ +sealed trait Json +case class JNumber(value: BigDecimal) extends Json +case class JString(value: String) extends Json +case class JBoolean(value: Boolean) extends Json +case class JArray(elems: List[Json]) extends Json +case class JObject(fields: (String, Json)*) extends Json +~~~ + +-> + +~~~ +// { "name": "Paul", "age": 42 } +JObject("name" -> JString("Paul"), "age" -> JNumber(42)) +~~~ + +Problem: Constructing JSON objects is too verbose. + +Type Coercion: Motivation (2) +============================= + +~~~ +sealed trait Json +case class JNumber(value: BigDecimal) extends Json +case class JString(value: String) extends Json +case class JBoolean(value: Boolean) extends Json +case class JArray(elems: List[Json]) extends Json +case class JObject(fields: (String, Json)*) extends Json +~~~ + +~~~ +// { "name": "Paul", "age": 42 } +Json.obj("name" -> "Paul", "age" -> 42) +~~~ + +How could we support the above user-facing syntax? + +Type Coercion: Motivation (3) +============================= + +~~~ +// { "name": "Paul", "age": 42 } +Json.obj("name" -> "Paul", "age" -> 42) +~~~ + +What could be the type signature of the `obj` constructor? + +-> + +~~~ +def obj(fields: (String, Any)*): Json +~~~ + +-> + +Allows invalid JSON objects to be constructed! + +~~~ +Json.obj("name" -> ((x: Int) => x + 1)) +~~~ + +We want invalid code to be signaled to the programmer with a +compilation error. + +Type Coercion (1) +================= + +~~~ +object Json { + + def obj(fields: (String, JsonField)*): Json = + JObject(fields.map(_.toJson): _*) + + trait JsonField { + def toJson: Json + } + +} +~~~ + +Type Coercion (2) +================= + +~~~ +trait JsonField { + def toJson: Json +} + +object JsonField { + implicit def stringToJsonField(s: String): JsonField = () => JString(s) + implicit def intToJsonField(n: Int): JsonField = () => JNumber(n) + ... + implicit def jsonToJsonField(j: Json): JsonField = () => j +} +~~~ + +Type Coercion: Usage +==================== + +~~~ +Json.obj("name" -> "Paul", "age" -> 42) +~~~ + +-> + +The compiler implicitly inserts the following conversions: + +~~~ +Json.obj( + "name" -> Json.JsonField.stringToJsonField("Paul"), + "age" -> Json.JsonField.intToJsonField(42) +) +~~~ + +Extension Methods: Motivation (1) +================================= + +~~~ +case class Duration(value: Int, unit: TimeUnit) +~~~ + +-> + +~~~ +val delay = Duration(15, TimeUnit.Second) +~~~ + +Extension Methods: Motivation (2) +================================= + +~~~ +case class Duration(value: Int, unit: TimeUnit) +~~~ + +~~~ +val delay = 15.seconds +~~~ + +How could we support the above user-facing syntax? + +Extension Methods +================= + +~~~ +case class Duration(value: Int, unit: TimeUnit) + +object Duration { + + object Syntax { + implicit def hasSeconds(n: Int): HasSeconds = new HasSeconds(n) + } + + class HasSeconds(n: Int) { + def seconds: Duration = Duration(n, TimeUnit.Second) + } + +} +~~~ + +Extension Methods: Usage +======================== + +~~~ +import Duration.Syntax._ + +val delay = 15.seconds +~~~ + +-> + +The compiler implicitly inserts the following conversion: + +~~~ +val delay = hasSeconds(15).seconds +~~~ + +Implicit Conversions +==================== + +The compiler looks for implicit conversions on an expression `e` of type `T` +in the following situations: + +- `T` does not conform to the expression’s expected type, +- in a selection `e.m`, if member `m` is not accessible on `T`, +- in a selection `e.m(args)`, if member `m` is accessible on `T` but is not + applicable to the arguments `args`. + +Note: at most one implicit conversion can be applied to a given expression. + +Summary +======= + +- Implicit conversions can improve the ergonomics of an API