The lesson in one paragraph. Functional programming (FP) builds programs out of pure functions and immutable values: no side effects, no imperative loops, recursion as the only way to iterate. This lesson introduces Scala 3 and its purely functional core: values and primitive types, function types and function literals, higher-order functions, pattern matching, methods with nesting and tail recursion, and currying. It then builds data with product types (case classes) and sum types (enums), which together form algebraic data types, explains subtyping for sum types and for functions, and shows how a module groups a type with its operations.
Pure OO approaches have limits. "Everything is an object" can be verbose and does not always fit; a function written as a class with one method (a functional interface) is verbose, and verbosity creates inertia; and OO code is not declarative, since it describes step-by-step behaviour rather than the goal. FP offers:
The possible disadvantage for software engineering: functional code is terser and requires higher competence. Mainstream OO languages have absorbed FP ideas in translated form: "no side effects" became "prefer immutability", "everything is a function" became "use lambdas and factories", "higher-order functions" became "use the Strategy pattern", and "abstract evaluation order" became "use lazy APIs", as with Java streams.
import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
public class ShowcaseJava8 {
public static void main(String[] args) {
// using a 'lambda function' to express a functional strategy
final Comparator<Integer> comparator = (a, b) -> a - b;
final Set<Integer> set = new TreeSet<>(comparator);
set.addAll(Arrays.asList(10, 20, 30, 40, 5, 6, 7, 60, 70));
System.out.println(set);
// declaratively working with streams: note str is an 'infinite' sequence!
final Random random = new Random();
final Stream<Integer> str = Stream.generate(() -> random.nextInt(6))
.map(i -> i + 1);
System.out.println(str.limit(20).collect(toList()));
// simulating large-scale data processing with parallel streams
final List<Double> l = IntStream.range(0, 10000000)
.mapToObj(x -> Math.random())
.collect(toList());
// sequential
long time = System.currentTimeMillis();
System.out.println(l.stream().reduce((a, b) -> Math.max(a, b)));
time = System.currentTimeMillis() - time;
System.out.println("Time: " + time);
// parallel
long time2 = System.currentTimeMillis();
System.out.println(l.stream().parallel().reduce((a, b) -> Math.max(a, b)));
time2 = System.currentTimeMillis() - time2;
System.out.println("Time2: " + time2);
System.out.println("Gain: " + (((double) time) / time2));
}
}
One run on a 4-core machine printed:
[5, 6, 7, 10, 20, 30, 40, 60, 70]
[6, 5, 2, 6, 6, 1, 3, 4, 1, 4, 3, 4, 5, 1, 1, 3, 6, 6, 6, 2]
Optional[0.9999993348556825]
Time: 826
Optional[0.9999993348556825]
Time2: 46
Gain: 17.956521739130434
Lambdas give a light syntax for functional interfaces that were already popular (Strategy, Iterator, Template Method), so it becomes natural to use them everywhere. Streams manipulate collections declaratively, neglect side effects, and exploit multi-core hardware transparently: parallel() is the only change between the two reductions. The flaw: the Stream API is very different from the List API, although both deal with sequences. Lesson 3 builds both with the same algorithms.
A gain of 18 on 4 cores is not a real speed-up. The sequential reduction runs first, while the JIT compiler is still warming up; a fair measurement warms up and repeats both versions, as JMH does.
Scala ("Scalable Language") is a strongly typed language that compiles to Java bytecode (and to JavaScript and native code). Martin Odersky started it at EPFL in 2001 to be both object-oriented and functional; version 3 came in 2021, and the course uses 3.3.5. Java 8+, C# and Kotlin inherited their functional side from it.
| Feature | What it means |
|---|---|
| Smooth integration of OOP and FP | OOP inside (because of the JVM), with FP sugar on top |
| Strong, expressive type system | structural, intersection, sum and self types; pervasive type inference |
| Purely object-oriented | every value is an object, every operation a method call; single inheritance, mixins via traits |
| Syntactic flexibility | expressive DSLs; common patterns written concisely and type-safely |
| High-level FP patterns | monadic constructions, higher-order kinds |
The course builds with SBT and experiments in the REPL, started with sbt console (or scala); IntelliJ IDEA is the reference IDE.
sbt console also loads the project's dependencies. Commands: :help, :reset, :type <expr>, :load file.scala, :quit (:q). Whole modules can be pasted in.object M extends App: makes an executable M.class.The first fragment of the language is T ::= Int | Double | Boolean | String | … and e ::= v | v1 op v2 | …. A val names a value and can never be reassigned.
// values can be associated to names, namely non-modifiable variables
val v = 1
// type indication is optional (typically inferred)
val w: Int = 1
// the right-hand side is a whole expression
val z = v + 5
// the usual way to print on the console (a non-FP hack)
println(z) // 6
val a = 1
println("result is " + a) // result is 1 (Java-style concatenation)
println(s"result is $a") // result is 1 (string interpolation)
println(s"result is ${a+1}") // result is 2 (interpolating an expression)
// primitive types as expected on the JVM
val i: Int = 10 + 5 // as in Java, to be read +(10,5)
val l: Long = 100_000_000_000L
val d: Double = 5.4 * 1.3
val f: Float = 3.0f
val b: Boolean = true && false
val s: String = "hello" concat " to all" // String methods as operators
val n: String = null // null can be passed to "objects", sorry
println(s"$i, $l, $d, $f, $b, $s, $n") // 15, 100000000000, 7.0200000000000005, 3.0, false, hello to all, null
val u: Unit = () // Unit has a single value, written ()
val u2: Unit = println("a") // prints a: Unit is the type of "no interesting result"
There is no real distinction between primitive and other types: every value is an object, and 10 + 5 is the method call 10.+(5). Types of values are usually left to inference. Unit has the single value () and types expressions whose result does not matter, such as println(…); null exists only for JVM interoperability.
Or simply, carefully mimic the slides.
Functions are values: the language grows by T ::= (T1,…,Tn) => T0 (function types) and e ::= (x1,…,xn) => e1 … en | ef(e1,…,en) (function literals and calls).
// a function variable: type (Int, Int) => Int, value an anonymous function (function literal)
val f: (Int, Int) => Int = (x: Int, y: Int) => x + y
val res = f(5, 10) // standard call syntax, "value passing"
println(res) // 15
// various syntaxes for function literals
val f1: (Int, Int) => Int = (x: Int, y: Int) => x + y // typed literal, typed val
val f2 = (x: Int, y: Int) => x + y // typed literal, untyped val
val f3: (Int, Int) => Int = (x, y) => x + y // untyped literal, typed val
val f4: (Int, Int) => Int = _ + _ // placeholders, typed val
val f5: (Int, Int) => Int = f4 // a function variable used as a value
// val f6 = _ + _ // does not type-check
println(s"${f1(10, 5)}, ${f4(10, 5)}") // 15, 15
val g: Int => Int =
(x: Int) => // a function literal with an imperative body
println("hello") // a statement (with a side effect)
x + 1 // the last expression is the result
println(g(10)) // prints hello, then 11
A function type (T1,…,Tn) => T0 describes functions from n inputs to one output (special cases T1 => T0 and () => T0). A function value or function literal (a lambda, in Java) (x1,…,xn) => e is an actual function. In each declaration above, the part before = is the val with its type, the part after is the function. There are four styles:
| Style | Example |
|---|---|
1. a val containing a function | f5 |
2. typed literal (the val type can then be inferred) | f1, f2 |
3. untyped literal (argument types from the val type) | f3 |
| 4. underscores, each the next argument | f4 |
Styles 3 and 4 need an expected type: val f6 = _ + _ fails with "Could not infer type for parameter _$1 of anonymous function". With several expressions in the body, as in g, the last one is the result.
// first order: no argument or result is a function
val f = (x: Int, y: Int) => x + y
// higher order: an input (or the output) is itself a function
val h: (Int, Int, (Int, Int) => Int) => Int = (a, b, f) => f(a, b)
println(h(10, 5, _ * _)) // 50
println(h(10, 5, (a, b) => a + b)) // 15
println(h(10, 5, f)) // 15
val i: (Int, Int => Int, Int => Int) => Int =
(n, f1, f2) => f1(f2(n))
println(i(10, _ + 1, _ * 2)) // 21
val l: (Int => Int) => (Int => Int) =
f => (i => f(f(i))) // without parens: f => i => f(f(i))
println(l(_ + 1)(10)) // 12
A higher-order function takes or returns a function. It is the quintessential FP pattern for making code reusable and configurable: the FP version of Strategy (or rather, Strategy is the OO version of it). i applies f2 then f1; l returns a function, which leads to currying (section 7). => is right-associative: a => b => c means a => (b => c).
Matching adds e ::= { case p1 => e1; … } | e match { case … }: a switch expression that can also define a function.
// matching against patterns: a switch expression
val v = 5
val res = v match
case n if n > 0 => "pos"
case 0 => "zero"
case _ => "neg" // default case: makes the function total
println(res) // pos
// a case-based function (partial, here)
// val f = (i: Int) => i match // alternative header
val f: Int => String = _ match
case n if n > 0 => "pos"
case 0 => "zero"
println(f(1)) // pos
println(f(0)) // zero
val g: Int => String = { case n if n > 0 => "pos" } // one-liner with {}
println(g(1)) // pos
// what f(-1) does: no case matches
try f(-1) catch case e: MatchError => println(e) // scala.MatchError: -1 (of class java.lang.Integer)
scala.MatchError; we normally neither throw nor catch it. match applies it to the value on its left, and case _ makes it total.if guard. A variable in a pattern is always a fresh one._ match with indented cases is the idiomatic Scala 3 case-based function; braces serve for one-liners.Because pattern variables are fresh, case zero => does not compare with an existing zero: it matches anything. Use backquotes or a guard:
val zero = 0
def isZero(n: Int): Boolean = n match
case `zero` => true // backquotes: compare with the existing value
case _ => false
def alwaysTrue(n: Int): Boolean = n match
case zero => true // a NEW variable named zero: matches anything
println((isZero(0), isZero(5), alwaysTrue(5))) // (true,false,true)object Indentation:
// idiomatic Scala 3 significant indentation
val res = 5 match
case n if n > 0 => "pos"
case 0 => "zero"
// indentation with an end marker (useful in complicated cases)
val res2 = 5 match
case n if n > 0 => "pos"
case 0 => "zero"
end res2
// Scala 2 (and Java) style: avoid it unless necessary
val res3 = 5 match {
case n if n > 0 => "pos"
case 0 => "zero"
}
// single line: curly braces and a semicolon
val res4 = 5 match { case n if n > 0 => "pos"; case 0 => "zero" }
// a single argument can be passed indented, after a colon
println:
5 + 10 // prints 15
end Indentation // an end marker for the object
Significant indentation was a much debated change from Scala 2; it works well, and the course uses it. Guidelines: braces only for one-liners where needed; end markers only where the code is complicated; an argument indented after a colon only with imperative-like code.
A method is declared as def f(x1: T1,…,xn: Tn): T0 = e, a UML-like syntax.
// a defined function (method), with UML-like syntax
def square(d: Double): Double = d * d
println(square(3.0)) // 9.0
// mostly equivalent code with a val
val square2: Double => Double = d => d * d
println(square2(3.0)) // 9.0
// recursion and pattern matching
def factorial(n: Int): Int = n match
case 0 | 1 => 1 // 0 or 1 gets mapped to 1
case _ => n * factorial(n - 1) // otherwise
println(factorial(6)) // 720
// a nested function: note the indentation
def abs(n: Int): Int =
def sign(m: Int): Int = m match
case i if i > 0 => 1
case 0 => 0
case _ => -1
n * sign(n)
println(s"${abs(-2)}, ${abs(2)}, ${abs(0)}") // 2, 2, 0
// tail recursion, checked, in a nested function
def factorial2(n: Int): Int =
@annotation.tailrec // only checks that the optimisation is possible
def _fact(n: Int, acc: Int): Int = n match
case 0 | 1 => acc
case _ => _fact(n - 1, n * acc)
_fact(n, 1)
println(factorial2(6)) // 720
A method is much like a val holding a function (compare square and square2), but it also allows currying, generics, annotations and nesting, and becomes a bytecode method of a class or object. Input types are mandatory, the return type optional but usually given. Methods can nest, which Java does not allow: sign exists only inside abs. case 0 | 1 matches either value.
Recursion is compiled into a loop only if it is tail recursion: every recursive call is the last expression evaluated before returning. factorial still has a multiplication to do after the call returns; _fact carries the partial product in an accumulator:
factorial(3) _fact(3, 1)
= 3 * factorial(2) = _fact(2, 3)
= 3 * (2 * factorial(1)) = _fact(1, 6)
= 3 * (2 * 1) = 6
= 6
The left column grows with the pending multiplications kept on the stack; the right one runs in constant space. @annotation.tailrec changes nothing in the code: it only makes compilation fail when the call is not tail ("Cannot rewrite recursive call: it is not in tail position", on factorial).
The optimisation happens with or without the annotation, and only for a method calling itself that cannot be overridden (a nested def, a method of an object, a final or private one), never for mutual recursion.
A method can have several parameter lists: def f(x1: T1,…)(…)(…): T0 = e.
// a standard function, no currying
def mult(x: Double, y: Double): Double = x * y
// a curried function: curriedMult actually has type Double => (Double => Double)
def curriedMult(x: Double)(y: Double): Double = x * y
// slightly different syntax at the call site
println(mult(10, 2)) // 20.0
println(curriedMult(10)(2)) // 20.0
// curriedMult can be partially applied
val twice: Double => Double = curriedMult(2)
println(twice(10)) // 20.0
// => is right-associative, so this type is Double => (Double => Double)
val curriedMultAsFunction: Double => Double => Double = x => y => x * y
println(curriedMultAsFunction(10)(2)) // 20.0
Currying performs a multi-argument application as successive applications of a function and its results: a function returning a function returning a function… It gives partial application: curriedMult(2) fixes the first argument and returns a Double => Double. (The results print 20.0 because they are Doubles.) Currying also groups arguments, for readability (a strategy argument in its own list) or for type inference, which proceeds one list at a time; foldLeft in lesson 3 relies on it.
An algebraic data type is built by composition from atomic types, in two ways: a product type (Cartesian product, a record) contains a part of type T1 and one of type T2; a sum type (disjoint union) is either a T1 or a T2, never both.
// a new product type, aka a record type
case class Point2D(x: Double, y: Double)
val p: Point2D = Point2D(10, 20)
println(p) // Point2D(10.0,20.0): a string representation for free
// matching to manipulate a point declaratively
def rotate(p: Point2D): Point2D = p match
case Point2D(x, y) => Point2D(y, -x)
// matching over couples
def sum(p1: Point2D, p2: Point2D): Point2D = (p1, p2) match
case (Point2D(x1, y1), Point2D(x2, y2)) => Point2D(x1 + x2, y1 + y2)
// partial matching
def getX(p: Point2D): Double = p match
case Point2D(x, _) => x
// a "fluent setter" that creates a new record
def chgX(p: Point2D, x: Double): Point2D = p match
case Point2D(_, y) => Point2D(x, y)
println(rotate(Point2D(10, 20))) // Point2D(20.0,-10.0)
println(sum(Point2D(10, 20), Point2D(3, 4))) // Point2D(13.0,24.0)
println(getX(Point2D(10, 20))) // 10.0
println(chgX(Point2D(10, 20), 11)) // Point2D(11.0,20.0)
A case class is a record that can be matched using values, variables or underscores for its fields, and gets a readable toString and structural equality. The tuple (p1, p2) is the case class Tuple2(p1, p2), which is how sum matches two points at once. (The deck's comments give 10 and Point2D(11,20) for the last two lines; the fields are Doubles, so the real output is 10.0 and Point2D(11.0,20.0).)
Records plus functions give almost everything an object gives, but FP keeps data and functions separate, and that is a feature.
A new operation is just one more function, without touching the type, whereas in OOP it means editing every class; conversely, a new case forces changes to every function that matches on it (the expression problem).
// a new sum type, defined by enumerating its cases
enum Person:
case Student(name: String, year: Int)
case Teacher(name: String, course: String)
// a value of type Person is either a Student OR a Teacher
val p: Person = Person.Student("mario", 2015)
// an algorithm over Person
def name(p: Person): String = p match
case Person.Student(n, _) => n
case Person.Teacher(n, _) => n
println(name(p)) // mario
enum WeekDay: // a Java-like enumeration
case Monday, Tuesday, Wednesday, Thursday, Friday
def preferredDay(d: WeekDay): Boolean =
d == WeekDay.Friday // could have used matching instead
println(preferredDay(WeekDay.Monday)) // false
enum IntList: // a recursive type: a linked list of Int
case Cons(head: Int, tail: IntList)
case Nil
def sum(l: IntList): Int = l match
case IntList.Cons(h, t) => h + sum(t)
case _ => 0
println(sum(IntList.Cons(10, IntList.Cons(20, IntList.Nil)))) // 30
An enum is the sum of some product types; a zero-argument case is a singleton. Hence three kinds: an enumeration of singletons like a Java enum (WeekDay), a sum of records like a class hierarchy (Person), a recursive sum like a linked list (IntList). Cases are qualified by the enum name unless imported. Enums cannot express every ADT, but suffice for now. The compiler checks that a match covers every case, with a warning:
enum Person:
case Student(name: String, year: Int)
case Teacher(name: String, course: String)
def name(p: Person): String = p match // warning: match may not be exhaustive.
// It would fail on pattern case: Person.Teacher(_, _)
case Person.Student(n, _) => n
T <: R (reflexive and transitive, a preorder) means a T can be passed where an R is expected. A sum type is a supertype of its cases (Student <: Person), and matching recovers the case at the receiving site. For functions, (T1,…,Tn) => T0 <: (R1,…,Rn) => R0 iff the result is covariant (T0 <: R0) and each argument contravariant (Ri <: Ti); no matching is needed.
enum Person:
case Student(name: String, year: Int)
case Teacher(name: String, course: String)
import Person.*
val s: Person = Student("mario", 2015) // Student <: Person: no conversion needed
// (Person => Student) <: (Student => Person)
val f: Student => Person = (p: Person) => Student("m", 2015)
println(f(Student("mario", 2015))) // Student(m,2015)
Callers of f pass a Student and use the result as a Person. A function accepting any Person handles the Student (argument may be wider), and the Student it returns is a Person (result may be narrower). The reverse is unsafe, since a caller of a Person => Student may pass a Teacher, and is rejected:
val g: Person => Student = (s: Student) => s
^^^^^^^^^^^^^^^^^
Found: Person.Student => Person.Student
Required: Person => Person.Student
T ::= Int | Double | Boolean | String primitives
| (T1,…,Tn) => T0 functions
| C | E case classes, enums
D ::= val x: T = v value
| def f(x1: T1,…,xn: Tn): T0 = e function
| case class C(x1: T1,…,xn: Tn) product type
| enum E: … case C(…) algebraic data type
e ::= v | v1 op v2 | ef(e1,…,en) | (x1,…,xn) => e1 … en functional
| { case p1 => e1; … } | e match { case … } matching
| C(e1,…,en) record
Products and sums together model articulate data structures and domains; higher-order functions generalise code into reusable components (generics, in lesson 3, go further). Very few constructs, in a declarative, match-based style; if and blocks can be used too, and syntax to control evaluation order comes in lesson 3.
A module groups definitions under a namespace, some public, some private; public ones are accessed as module.defname. The operations on a type go into a module with the type's name. In Scala a module is an object, and import ModuleName.* removes the prefix.
// an ADT: type + module
enum Person:
case Student(name: String, year: Int)
case Teacher(name: String, course: String)
object Person:
def name(p: Person): String = p match
case Student(n, _) => n
case Teacher(n, _) => n
println(Person.name(Person.Student("mario", 2015))) // mario
import Person.*
println(name(Student("mario", 2015))) // mario
// a method outside the Person module
def isStudent(p: Person): Boolean = p match
case Student(_, _) => true
case _ => false
println(isStudent(Student("mario", 2015))) // true
The object named like the enum is its companion: inside it the cases need no qualification, and the import brings in both operations and cases. Since the enum's structure is public, outside code such as isStudent can still match on it; lesson 3 hides it.
JUnit works in Scala if you want to keep TDD (proper Scala test frameworks come later). In FP a test typically contains only assertions, and case-class equality makes them natural; the three tests below pass.
import org.junit.*
import org.junit.Assert.*
class RecordTest:
case class Item(desc: String, code: Int)
val record = Item("a", 10)
@Test def testMatches() =
assertTrue(record match { case Item(x, 10) => true; case _ => false })
assertTrue: // significant indentation style
record match
case Item("a", 10) => true
case _ => false
@Test def testEquality() =
assertEquals(record, Item("a", 10))
assertNotEquals(record, Item("a", 11))
@Test def testChangeCode() =
def changeCode(item: Item)(f: Int => Int): Item = item match
case Item(desc, code) => Item(desc, f(code))
assertEquals(Item("a", 11), changeCode(Item("a", 10))(_ + 1))
The test is a class, although classes are only explained in lesson 5. This build file enables JUnit (and VS Code's JUnit plugin):
scalaVersion := "3.3.5"
lazy val root = (project in file("."))
.settings(
name := "pps-code-scala",
libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.2" % Test
)
The lab (fork unibo-pps/pps-lab02) practises this lesson; each solution below was compiled and run, and the comments show the real output.
object HelloScala extends App:
println("Hello, Scala") // Hello, Scala
In the REPL, try the lecture's functions and variations, such as a divide in both forms with edge cases:
def mult(x: Int, y: Int): Int = x * y
def multCurried(x: Int)(y: Int): Int = x * y
val multiplyBy3 = multCurried(3) // partial application: Int => Int
println((mult(3, 4), multCurried(3)(4), multiplyBy3(5))) // (12,12,15)
def divide(x: Double, y: Double): Double = x / y
def divideCurried(x: Double)(y: Double): Double = x / y
val half: Double => Double = divideCurried(_)(2) // fixes the SECOND argument
println((divide(1, 4), half(9), divide(1, 0), divide(0, 0))) // (0.25,4.5,Infinity,NaN)
Division by zero on Doubles gives Infinity or NaN instead of throwing; divideCurried(_)(2) fixes a later argument.
positive with match-cases, as a val and as a method// (i) a val holding a function literal
val positive: Int => String = _ match
case x if x >= 0 => "positive"
case _ => "negative"
// (ii) method syntax
def positiveM(x: Int): String = x match
case x if x >= 0 => "positive"
case _ => "negative"
println((positive(0), positive(-3), positiveM(7))) // (positive,negative,positive)
neg, a predicate transformer// type first: (String => Boolean) => (String => Boolean)
val neg: (String => Boolean) => (String => Boolean) = p => s => !p(s)
def negM(p: String => Boolean): String => Boolean = s => !p(s)
// generic version (a def can have type parameters)
def negG[A](p: A => Boolean): A => Boolean = a => !p(a)
val empty: String => Boolean = _ == "" // a predicate on strings
val notEmpty = neg(empty) // notEmpty: String => Boolean
println(notEmpty("foo")) // true
println(notEmpty("")) // false
println(notEmpty("foo") && !notEmpty("")) // true
println(negM(empty)("foo") && negG[Int](_ > 0)(-1)) // true
Only the method can be generic in the course's fragment of Scala: a val cannot take type parameters (Scala 3 does have polymorphic function values, [A] => (p: A => Boolean) => …, but the course does not use them).
x ≤ y = z in four variants, and function composition// x <= y = z, in four variants
val p1: Int => Int => Int => Boolean = x => y => z => x <= y && y == z // curried val
val p2: (Int, Int, Int) => Boolean = (x, y, z) => x <= y && y == z // non-curried val
def p3(x: Int)(y: Int)(z: Int): Boolean = x <= y && y == z // curried def
def p4(x: Int, y: Int, z: Int): Boolean = x <= y && y == z // non-curried def
println((p1(1)(2)(2), p2(1, 2, 3), p3(3)(2)(2), p4(2, 2, 2))) // (true,false,false,true)
// function composition: (f o g)(x) = f(g(x))
def compose(f: Int => Int, g: Int => Int): Int => Int = x => f(g(x))
println(compose(_ - 1, _ * 2)(5)) // 9
// the generic version works for any types that line up
def composeG[A, B, C](f: B => C, g: A => B): A => C = x => f(g(x))
println(composeG((s: String) => s.length, (i: Int) => "x" * i)(4)) // 4
Function types and literals look alike: Int => Int => Int => Boolean and x => y => z => ….
power and reverseNumber, with tail recursiondef power(base: Double, exponent: Int): Double = exponent match
case 0 => 1
case n => base * power(base, n - 1)
def powerTail(base: Double, exponent: Int): Double =
@annotation.tailrec
def loop(n: Int, acc: Double): Double = n match
case 0 => acc
case _ => loop(n - 1, acc * base)
loop(exponent, 1)
println((power(2, 3), power(5, 2), powerTail(2, 10))) // (8.0,25.0,1024.0)
def reverseNumber(n: Int): Int =
@annotation.tailrec
def loop(rest: Int, acc: Int): Int = rest match
case 0 => acc
case _ => loop(rest / 10, acc * 10 + rest % 10)
loop(n, 0)
println(reverseNumber(12345)) // 54321
powerTail multiplies into an accumulator; reverseNumber moves the last digit (rest % 10) onto the accumulator at each step.
Expr sum type with evaluate and show, test-firstenum Expr:
case Literal(value: Int)
case Add(left: Expr, right: Expr)
case Multiply(left: Expr, right: Expr)
object Expr:
def evaluate(expr: Expr): Int = expr match
case Literal(v) => v
case Add(l, r) => evaluate(l) + evaluate(r)
case Multiply(l, r) => evaluate(l) * evaluate(r)
def show(expr: Expr): String = expr match
case Literal(v) => v.toString
case Add(l, r) => "(" + show(l) + " + " + show(r) + ")"
case Multiply(l, r) => "(" + show(l) + " * " + show(r) + ")"
@main def tryExpr =
import Expr.*
val e = Multiply(Add(Literal(1), Literal(2)), Literal(4))
println(show(e)) // ((1 + 2) * 4)
println(evaluate(e)) // 12
import org.junit.*
import org.junit.Assert.*
import Expr.*
class ExprTest:
val e = Add(Literal(2), Multiply(Literal(3), Literal(4)))
@Test def testEvaluate() =
assertEquals(7, evaluate(Add(Literal(3), Literal(4))))
assertEquals(14, evaluate(e))
@Test def testShow() =
assertEquals("5", show(Literal(5)))
assertEquals("(2 + (3 * 4))", show(e))
mapInt and filter on an OptionalIntobject Optionals:
enum Optional:
case Just(value: Int)
case Empty()
object Optional:
def mapInt(optional: Optional)(f: Int => Int): Optional = optional match
case Just(v) => Just(f(v))
case _ => Empty()
def filter(optional: Optional)(p: Int => Boolean): Optional = optional match
case Just(v) if p(v) => optional
case _ => Empty()
@main def tryOptionals =
import Optionals.*, Optional.*
println(mapInt(Just(5))(_ + 1)) // Just(6)
println(mapInt(Empty())(_ + 1)) // Empty()
println(filter(Just(5))(_ > 2)) // Just(5)
println(filter(Just(5))(_ > 8)) // Empty()
println(filter(Empty())(_ > 2)) // Empty()
The function goes in a second parameter list, so a lambda such as _ + 1 can follow. Lesson 3 makes the type generic.
A function type describes functions, such as (Int, Int) => Int; a function literal is an actual function value, such as (x, y) => x + y. In val f: (Int, Int) => Int = (x, y) => x + y the type is before = and the literal after.
val f4: (Int, Int) => Int = _ + _ compile but val f6 = _ + _ not?The underscores expand to (_$1, _$2) => _$1 + _$2, whose parameter types must come from the expected type. f4 declares it; f6 has none, so the parameter types cannot be inferred.
A function that takes a function as an argument or returns one, such as h(10, 5, _ * _). It corresponds to the Strategy pattern: the varying behaviour is passed as a parameter.
val f: Int => String = _ match { case n if n > 0 => "pos"; case 0 => "zero" } called a partial function? What does f(-1) do?It is not defined on every input: no case matches negative numbers. f(-1) raises scala.MatchError: -1. Adding case _ => … makes it total.
@annotation.tailrec do?When it is the last expression evaluated before returning, with nothing left to do afterwards (_fact(n - 1, n * acc), not n * factorial(n - 1)). The annotation does not enable the optimisation; it makes compilation fail if the call is not in tail position.
curriedMult(2), given def curriedMult(x: Double)(y: Double): Double = x * y?Double => Double: partial application fixes x = 2 and returns the function that doubles its argument.
A product value contains all its components (a Cartesian product): case class Point2D(x: Double, y: Double). A sum value is exactly one of several alternatives (a disjoint union): enum Person: case Student(…); case Teacher(…). Algebraic data types combine the two.
(Person => Student) <: (Student => Person), but not the other way round?Function types are contravariant in the argument and covariant in the result. A function accepting any Person can handle the Student the caller passes, and the Student it returns is a Person. A Student => Student used as a Person => Student could receive a Teacher.
enum go in the FP style, and what does import Person.* bring into scope?In a module (an object) with the same name as the type, its companion, keeping data and operations separate. The import brings in both the operations (name) and the enum cases (Student, Teacher).
case zero => … do when a value zero already exists?It introduces a new variable named zero that matches anything. To compare with the existing value, write case `zero` => or use a guard.
Deck 02-fp-basic, "Basic Functional Programming" (all 48 slides: motivations, Scala, the core FP constructs, algebraic data types, subtyping, modules, JUnit) · Lab 02 (all tasks, solved here) · course code u02 (Values, AnonymousFunctions, HigherOrderFunctions, CaseMatch, Indentation, DefinedFunctions, Currying, ProductTypes, AlgebraicDataTypes, Modules, ShowcaseJava8.java, test RecordTest) and 02-code.txt. Every Scala example was compiled and run with Scala 3.3.6 and the JUnit tests executed; the Java example was run with Java 21. The printed outputs are the real ones.