Chapter experiment

Getting started with functional programming in Scala

How can I turn Scala's basic syntax into reusable, type-guided pure functions instead of writing a separate solution for every case?

Lesson 1 of 8

Program and method context

What Scala structure must be in place before a method can call a local recursive helper?

The previous chapter explained why pure functions are easier to reason about. To write one in Scala, you first need a place to put it, and that place is an object. An object is a named, single-instance container for related definitions. You declare one with object MyProgram and indent its members to group them inside. Scala has no static keyword, so an object plays that role from Java. Declaring object MyProgram creates nothing while the program runs; it groups code under one name so other code can refer to it.

A complete program: object MyProgram groups its methods, printAbs is the @main entry point, and it calls abs and formatAbs.
scala
object MyProgram:
  def abs(n: Int): Int =
    if n < 0 then -n
    else n

  private def formatAbs(x: Int) =
    val msg = "The absolute value of %d is %d."
    msg.format(x, abs(x))

  @main def printAbs: Unit =
    println(formatAbs(-42))

Inside the object you define methods with def, a named method definition. The line def abs(n: Int): Int declares a method named abs. The def keyword starts the declaration, abs is the name, n: Int says the parameter n has type Int, and the trailing : Int declares the result type. A type annotation is a written type attached to a name, such as n: Int or that trailing : Int on the method. The declaration def abs only describes the method; it does not run the method when the program reads that line. The body of abs is the expression if n < 0 then -n else n, which produces either -n or n.

To use abs you write a method call: the name followed by arguments in parentheses, like abs(-42). A method call evaluates to the method's result. Inside MyProgram you can call abs without any prefix. From outside, dot notation selects it, as in MyProgram.abs(-42), where MyProgram is the namespace, meaning it qualifies its members. Writing import MyProgram.abs brings that member into local scope so you can call abs directly. A member marked private, like formatAbs, is visible only inside its own object, so another object in your program cannot call it.

A runnable program also needs an entry point, the method where execution starts. In the code above, @main def printAbs: Unit = ... is that entry point. Scala also accepts def main(args: Array[String]): Unit or extends App as alternate entry-point forms. The form you pick says nothing about whether your functions are pure; only the method bodies decide that. A val or def can be a local definition when you nest it inside a method or block. That is what lets a method like factorial define and call its own small helper, exposing nothing as an object member.

A local def helper inside factorial; the helper is not an object member.
scala
def factorial(n: Int): Int =
  def loop(i: Int, acc: Int): Int =
    if i <= 0 then acc
    else loop(i - 1, i * acc)
  loop(n, 1)

Notice that formatAbs calls msg.format(x, abs(x)). Here format is a method on the String named msg, and it replaces the two %d placeholders with x and the result of abs(x), respectively. Objects group methods, calls supply arguments, local definitions keep helpers close to the method that uses them, and a private helper stays inside its object.

Lesson 2 of 8

Expressions and purity

How do Scala expressions produce values that can be reasoned about by substitution?

Last lesson you declared methods and called them. Now let's open one up and look at what each line produces, because that is where Scala does something unusual. Most of what you write is an expression: code that evaluates to a value. An expression is not a statement that just does something and disappears. It hands back a result. That is the whole reason you can reason about a method by swapping results in and out instead of tracking hidden changes.

  1. 1

    Bind a result with val

    A val binds a name to the result of an expression and forbids reassignment. Write val n = 42 - 1 and n means 41 from then on; n = 5 won't compile. A var also names a value but permits reassignment, so var n = 42 followed by n = 5 works. Reach for val when the name should keep one result.
  2. 2

    Pick a branch with if

    An if expression is a conditional whose selected branch supplies its value. Writing val label = if (n < 0) "negative" else "non-negative" gives label the string from whichever branch runs. The if hands back a value, it does not just choose a path through the code.
  3. 3

    Sequence with a block

    A block is a group of expressions inside braces. It evaluates them in order and the final expression is its value. The earlier ones still run, but only the last supplies the result. That is how a method body can take a few steps and then hand back one value.
A method whose body is a block ending in an if expression
scala
def abs(n: Int): Int = {
  val sign = if (n < 0) -1 else 1
  n * sign
}

Call abs(-42). The block runs, sign binds -1, then n * sign produces 42. abs is a pure function: no observable side effects, and the same input always gives the same result. That matters because you can replace abs(-42) with 42 anywhere it appears and the program still means the same thing. This replace-by-result property is called referential transparency. One caveat: val supports immutable code, but using val alone does not make a function pure. A method can still read or write outside state while using val.

Pure expression

  • abs(-42) always gives 42
  • Replacing it with 42 changes nothing
  • Same input, same output, every time

Effectful expression

  • println("hi") prints to the console
  • It returns Unit, whose only value is ()
  • Replacing it with () erases the output

That contrast is the point. A side effect is an observable interaction beyond returning a value, and printing is one. println returns Unit, a type with a single value written (). Swap println("hi") for just () and the compiler is happy, but the printed output is gone, so the expression is not referentially transparent. Real programs still need effects somewhere; the discipline is to keep pure value computation separate from them. Scala expressions produce values, and purity makes those values enough to reason by substitution.

Lesson 3 of 8

Tail-recursive loops and concrete search

How can local recursive helpers express factorial and array search without mutable loop variables?

Last lesson you saw expressions and purity. Now let's build a loop. A loop needs state that changes each round, and a pure function can't mutate a variable to hold it. So instead, a local helper takes the current values as parameters and calls itself with the next ones. Those parameters are the loop state, the values carried from one call to the next.

Factorial as a local recursive helper. Trace factorial(5): go(5, 1), go(4, 5), go(3, 20), go(2, 60), go(1, 120), go(0, 120), then 120.
scala
def factorial(n: Int): Int =
  def go(n: Int, acc: Int): Int =
    if n <= 0 then acc
    else go(n - 1, n * acc)

  go(n, 1)

go is local, and its two parameters carry the state. n is how much is left to multiply. acc, short for accumulator, is the product so far. When n drops to 0, go returns acc with no further call. Otherwise go(n - 1, n * acc) computes the next state and recurses. Nothing is reassigned. The recursive call sits in tail position: the caller returns it directly and does no work with the result. When every recursive call is in tail position, that's tail recursion, and Scala can compile it like an iterative loop without a new stack frame per round. Add work after the call, say 1 + go(n - 1, n * acc), and the call is no longer in tail position. Writing @annotation.tailrec above the helper tells the compiler to error out if that expectation fails.

The same shape handles a search. Array[T] means an array whose elements all have type T. In findFirst below, the array holds String elements and the key is a String too, so the function is fixed to one type. A function specialized to one concrete type like that is called monomorphic. Once again the state lives in a parameter, this time the index to check next.

findFirst searches an Array[String] with a local loop. Trace it on ["cat", "dog", "bird"] for key "dog": loop(0) fails, loop(1) matches and returns 1. For "fish": loop(0), loop(1), loop(2) fail, loop(3) hits length and returns -1.
scala
def findFirst(ss: Array[String], key: String): Int =
  @annotation.tailrec
  def loop(n: Int): Int =
    if n >= ss.length then -1
    else if ss(n) == key then n
    else loop(n + 1)

  loop(0)

findFirst walks ss from index 0. When ss(n) equals key, it returns n, the matching index. When n reaches ss.length, it returns -1, the source's not-found convention. Other search designs could return something else, but this is the one the source uses. Every recursive call sits in tail position, so the annotation on loop can check the helper and error out if that stops holding. Same pattern, same guard: a local helper carries factorial or search state through its parameters, and @annotation.tailrec keeps each recursive step honest about being in tail position.

Lesson 4 of 8

Functions become values

How can a computation accept behavior as an argument?

The previous lesson built a factorial loop with a local helper and a tail-recursive call. This lesson answers a different question: how can a computation accept behavior as an argument? Start with two small formatting helpers from the running program, one for the absolute value of a number and one for its factorial. abs and factorial are the methods you defined earlier. The two helpers build a message with String.format and differ only in the computation applied to the number: abs in one, factorial in the other. String.format replaces each placeholder like %d in the message with the matching argument. The formatting work is the same, and that repetition is the clue.

Two formatting helpers that repeat the same String.format structure
scala
private def formatAbs(x: Int) =
  val msg = "The absolute value of %d is %d."
  msg.format(x, abs(x))

private def formatFactorial(n: Int) =
  val msg = "The factorial of %d is %d."
  msg.format(n, factorial(n))

A function value is a function treated as data. You can assign it to a variable, store it in a data structure, or pass it as an argument, just like an Int or a String. The type that describes it is a function type, written A => B, where A is the input type and B is the result type. Two inputs look like (A, B) => C. With those names in hand, the two helpers can become one method. Here f has the function type Int => Int, so it takes one Int and returns one Int.

One formatter with the computation supplied as f
scala
def formatResult(name: String, n: Int, f: Int => Int) =
  val msg = "The %s of %d is %d."
  msg.format(name, n, f(n))

Here f is a function parameter, meaning a parameter whose value is behavior. formatResult is a higher-order function because it takes a function as an argument. Inside the body, f(n) is function application: calling the function value f with the argument n. Passing abs or factorial as f does not run it. The caller supplies the function value first, and formatResult applies that value to n when it builds the message. So formatResult replaces both helpers. The shared formatting stays in one place, and the varying computation arrives through the parameter. One caveat: a higher-order function is not automatically pure. Purity depends on the functions it receives and on everything else it does.

Lesson 5 of 8

Anonymous functions and predicates

How can a caller provide a matching rule inline without repeating types the context already supplies?

You have passed named computations like abs and factorial to formatResult. That works when a computation already has a name. Often the rule is small, used once, and does not deserve a name. Write it where you need it instead. Take a search helper that walks an array from index 0 and returns the first index where an element matches, or -1 if there is no match. Array(7, 9, 13) is an array literal: it builds an array without the new keyword, so you can pass it straight to a call.

Passing an array literal and an inline matching test to a search call
scala
findFirst(Array(7, 9, 13), (x: Int) => x == 9)
// res2: Int = 1

Read the second argument carefully. (x: Int) => x == 9 is an anonymous function: an unnamed function expression that evaluates to a function value. Left of => declares the argument, right of => is the body. It returns a Boolean, so it is a predicate, a Boolean-valued test the caller supplies. The left side shows an explicit type: Int is written directly in the expression. That is useful when nothing around the call tells the reader what x must be.

Now shorten the same call. If the surrounding call already expects a function of type Int => Boolean, that expected type is enough for Scala to infer the parameter type, so you can write x => x == 9 and drop Int.

The same predicate with the parameter type inferred from context
scala
findFirst(Array(7, 9, 13), x => x == 9)
// res2: Int = 1

The two versions describe the same Int => Boolean. Explicit and inferred differ only in what you wrote, not in what the function accepts or returns. But inference needs context. Without an expected type, write x: Int or Scala cannot know what x is. Keep one caveat: inference is local, so omitting an annotation does not remove the underlying type contract. Writing the function expression does not run it either. The enclosing call applies it when the search reaches each element. And explicit signatures stay useful when they guide an implementation or document a public method.

Lesson 7 of 8

Following types to partial1

How does the type of partial1 lead to its implementation?

The last lesson showed that a signature like def findFirst[A](as: Array[A], p: A => Boolean): Int can work for any element type A. Here's the payoff: the type of a function can be specific enough to tell you the body. This lesson reads partial1's signature backward until only one implementation remains.

The type signature as specification is the written contract of a function: the input types, the output type, and the relationship between them. Before writing any body, read what the signature requires. partial1's signature is:

def partial1[A, B, C](a: A, f: (A, B) => C): B => C

A, B, and C are type parameters, placeholders the caller fills in. The first argument is a value of type A. The second is f, a function that takes an A and a B and returns a C. The return type is B => C, a function that takes a B and returns a C. So partial1 does not return a C directly. It returns a function.

  1. 1

    Start from the required result type

    The result must be a B => C, so start with b => ???. The b is a B you can use inside the inner function.
  2. 2

    List the values available

    Inside that function you have a: A, b: B, and f: (A, B) => C. You still need to produce a C.
  3. 3

    Use the only value that produces C

    Only f can make a C, and f needs an A and a B. You have a and b, so f(a, b) is the only body that fits.
Fixing a and returning a function that waits for b.
scala
def partial1[A, B, C](a: A, f: (A, B) => C): B => C =
  b => f(a, b)

Reading the signature first and letting the types narrow the possible bodies is type-driven reasoning. Once a is fixed, partial1 returns a function still waiting for a B. Fixing some arguments and getting back a function for the rest is partial application. It lets you reuse partial1 with the same a and different b values without repeating a each time.

One caveat: a signature narrows the implementation space, but it does not always pick one body. When several bodies type-check, laws and stated requirements still decide which is right. Here the polymorphic type is unusually tight, so f(a, b) is the only available way to produce C.

Lesson 8 of 8

Currying and composition

How can function shapes be transformed and composed without changing their type relationships?

The last lesson followed partial1's type to its body: a function that takes a and f, then returns b => f(a, b). That returned function is the seed for this lesson. Think about adding two numbers. plus(2, 3) takes both at once. A version that takes 2 and returns a helper waiting for 3 does the same job in a different shape. This lesson names those shapes, converts between them, and connects one function's output to another's input.

A curried function represents a two-argument computation as nested one-argument stages, so its caller supplies arguments one at a time. An uncurried function receives its arguments together. Neither shape changes the result. Only the way arguments arrive changes. The code below converts between the shapes. curry takes a two-argument function f and returns a => b => f(a, b). uncurry takes a nested function f and returns (a, b) => f(a)(b). The result type stays C in both directions.

curry and uncurry convert between the two argument shapes.
scala
def curry[A, B, C](f: (A, B) => C): A => (B => C) =
  a => b => f(a, b)

def uncurry[A, B, C](f: A => B => C): (A, B) => C =
  (a, b) => f(a)(b)

Function composition connects one function's output to another function's input. compose is the higher-order function that performs it. If f accepts B and produces C, and g accepts A and produces B, then compose(f, g) returns a function from A to C. The types must line up: g must produce the type f accepts. compose(f, g) applies g first and then f, because the argument enters g and g's result feeds f. If g returns a String but f expects an Int, the compiler rejects the call.

compose wires g's output into f's input.
scala
def compose[A, B, C](f: B => C, g: A => B): A => C =
  a => f(g(a))

Hold onto one distinction. Currying describes shape. Partial application describes supplying fewer arguments than a function declares and getting back a function. A curried function can be partially applied, but a function can be partially applied without being written in curried form. Also note that the source's curried result type is nested, A => (B => C), not a three-argument function type. Currying changes how arguments arrive, uncurry reverses that shape, and compose connects functions whose types match.