Introduction to Functional Programming in Scala

Introduction

Functional Programming (FP) is one of Scala's most powerful paradigms, working seamlessly alongside object-oriented programming to create elegant, maintainable, and robust code. Unlike imperative programming that focuses on how to do things through step-by-step instructions, functional programming emphasizes what to compute through the composition of functions.

This lesson introduces you to the core concepts of functional programming in Scala, setting the foundation for advanced topics like higher-order functions, collections, and monads. You'll learn how to think functionally and why this approach leads to more predictable, testable, and scalable code.

Core Principles of Functional Programming

Immutability

Immutability means that once an object is created, it cannot be changed. Instead of modifying existing objects, you create new ones with the desired changes.

// Mutable approach (avoid in FP)
import scala.collection.mutable

val mutableList = mutable.ListBuffer(1, 2, 3)
mutableList += 4  // Modifies the existing list
mutableList(0) = 10  // Changes the first element
println(mutableList)  // ListBuffer(10, 2, 3, 4)

// Immutable approach (FP style)
val immutableList = List(1, 2, 3)
val newList = immutableList :+ 4  // Creates a new list
val anotherList = 10 +: immutableList.tail  // Creates another new list
println(immutableList)  // List(1, 2, 3) - unchanged
println(newList)       // List(1, 2, 3, 4)
println(anotherList)   // List(10, 2, 3)

// Immutable data structures
case class Person(name: String, age: Int, email: String)

val alice = Person("Alice", 30, "alice@example.com")
val olderAlice = alice.copy(age = 31)  // Creates new instance
val aliceWithNewEmail = alice.copy(email = "alice.smith@example.com")

println(alice)           // Person(Alice,30,alice@example.com) - unchanged
println(olderAlice)      // Person(Alice,31,alice@example.com)
println(aliceWithNewEmail)  // Person(Alice,30,alice.smith@example.com)

// Immutable collections operations
val numbers = List(1, 2, 3, 4, 5)
val doubled = numbers.map(_ * 2)        // Creates new list
val evens = numbers.filter(_ % 2 == 0)  // Creates new list
val sum = numbers.reduce(_ + _)         // Returns value, doesn't modify list

println(numbers)  // List(1, 2, 3, 4, 5) - original unchanged
println(doubled)  // List(2, 4, 6, 8, 10)
println(evens)    // List(2, 4)
println(sum)      // 15

Pure Functions

Pure functions are the cornerstone of functional programming. They have two key properties:

  1. Deterministic: Same input always produces the same output
  2. No side effects: Don't modify external state or perform I/O operations
// Pure functions - deterministic and no side effects
def add(x: Int, y: Int): Int = x + y

def multiply(x: Double, y: Double): Double = x * y

def isEven(n: Int): Boolean = n % 2 == 0

def square(x: Int): Int = x * x

// Testing pure functions - always predictable
println(add(5, 3))      // Always 8
println(multiply(2.5, 4.0))  // Always 10.0
println(isEven(42))     // Always true
println(square(7))      // Always 49

// More complex pure functions
def factorial(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial(n - 1)
}

def fibonacci(n: Int): Int = {
  if (n <= 1) n
  else fibonacci(n - 1) + fibonacci(n - 2)
}

def isPrime(n: Int): Boolean = {
  if (n <= 1) false
  else if (n <= 3) true
  else if (n % 2 == 0 || n % 3 == 0) false
  else {
    var i = 5
    while (i * i <= n) {
      if (n % i == 0 || n % (i + 2) == 0) return false
      i += 6
    }
    true
  }
}

// Pure function composition
def addOne(x: Int): Int = x + 1
def timesTen(x: Int): Int = x * 10

val composedFunction = (x: Int) => timesTen(addOne(x))
println(composedFunction(5))  // (5 + 1) * 10 = 60

// Testing pure functions is easy and reliable
val testCases = List(0, 1, 2, 3, 4, 5)
testCases.foreach { n =>
  println(s"factorial($n) = ${factorial(n)}")
  println(s"fibonacci($n) = ${fibonacci(n)}")
  println(s"isPrime($n) = ${isPrime(n)}")
}

// Impure functions (avoid these patterns)
var globalCounter = 0

def impureIncrement(): Int = {  // Impure: modifies global state
  globalCounter += 1
  globalCounter
}

def impurePrint(message: String): Unit = {  // Impure: performs I/O
  println(s"Log: $message")
}

def impureRandom(): Double = {  // Impure: non-deterministic
  scala.util.Random.nextDouble()
}

// Better alternatives using pure approaches
def pureIncrement(current: Int): Int = current + 1

def createLogMessage(message: String): String = s"Log: $message"

def generateRandomSeed(seed: Long): (Double, Long) = {
  val random = new scala.util.Random(seed)
  (random.nextDouble(), seed + 1)
}

Referential Transparency

An expression is referentially transparent if it can be replaced by its value without changing the program's behavior. This property is crucial for reasoning about functional code.

// Referentially transparent expressions
val x = 5
val y = 10
val sum = x + y  // Can be replaced with 15

def pure(a: Int, b: Int): Int = a * 2 + b

val result1 = pure(3, 4)  // Can be replaced with 10
val result2 = pure(3, 4)  // Always the same as result1

// Demonstrating referential transparency
val list = List(1, 2, 3, 4, 5)
val doubled1 = list.map(x => x * 2)
val doubled2 = list.map(_ * 2)
val doubled3 = List(2, 4, 6, 8, 10)  // Could replace any of the above

println(doubled1 == doubled2)  // true
println(doubled1 == doubled3)  // true

// Function composition with referential transparency
def addTwo(x: Int): Int = x + 2
def multiplyByThree(x: Int): Int = x * 3

val number = 5
val step1 = addTwo(number)          // 7
val step2 = multiplyByThree(step1)  // 21

// Can be simplified to:
val directResult = multiplyByThree(addTwo(5))  // 21

// Or using function composition
val composed = (x: Int) => multiplyByThree(addTwo(x))
val composedResult = composed(5)  // 21

// All produce the same result due to referential transparency
println(step2 == directResult)     // true
println(directResult == composedResult)  // true

Higher-Order Functions Basics

Higher-order functions are functions that either take other functions as parameters or return functions as results. They're fundamental to functional programming.

Functions as Parameters

// Basic higher-order functions
def applyFunction(x: Int, f: Int => Int): Int = f(x)

def double(x: Int): Int = x * 2
def square(x: Int): Int = x * x
def increment(x: Int): Int = x + 1

val number = 5
println(applyFunction(number, double))     // 10
println(applyFunction(number, square))     // 25
println(applyFunction(number, increment))  // 6

// Using anonymous functions (lambdas)
println(applyFunction(number, x => x * 3))      // 15
println(applyFunction(number, x => x - 1))      // 4
println(applyFunction(number, _ + 10))          // 15

// Higher-order functions with multiple parameters
def combineNumbers(x: Int, y: Int, f: (Int, Int) => Int): Int = f(x, y)

val a = 8
val b = 3

println(combineNumbers(a, b, _ + _))      // 11 (addition)
println(combineNumbers(a, b, _ - _))      // 5 (subtraction)
println(combineNumbers(a, b, _ * _))      // 24 (multiplication)
println(combineNumbers(a, b, math.max))  // 8 (maximum)
println(combineNumbers(a, b, math.min))  // 3 (minimum)

// Custom operations
def modulo(x: Int, y: Int): Int = x % y
def power(x: Int, y: Int): Int = math.pow(x, y).toInt

println(combineNumbers(a, b, modulo))  // 2 (8 % 3)
println(combineNumbers(a, b, power))   // 512 (8^3)

// Working with collections using higher-order functions
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// map - transforms each element
val doubled = numbers.map(_ * 2)
val squared = numbers.map(x => x * x)
val stringified = numbers.map(_.toString)

println(doubled)      // List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
println(squared)      // List(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)
println(stringified)  // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// filter - selects elements based on predicate
val evens = numbers.filter(_ % 2 == 0)
val greaterThanFive = numbers.filter(_ > 5)
val primes = numbers.filter(isPrime)

println(evens)           // List(2, 4, 6, 8, 10)
println(greaterThanFive) // List(6, 7, 8, 9, 10)
println(primes)          // List(2, 3, 5, 7)

// reduce - combines elements using a binary operation
val sum = numbers.reduce(_ + _)
val product = numbers.reduce(_ * _)
val maximum = numbers.reduce(math.max)

println(sum)      // 55
println(product)  // 3628800
println(maximum)  // 10

Functions as Return Values

// Functions that return functions
def multiplyBy(factor: Int): Int => Int = {
  x => x * factor
}

val double = multiplyBy(2)
val triple = multiplyBy(3)
val halve = multiplyBy(0.5) // This won't compile due to type mismatch

// Correct version for doubles
def multiplyByDouble(factor: Double): Double => Double = {
  x => x * factor
}

val halveDouble = multiplyByDouble(0.5)

println(double(10))       // 20
println(triple(7))        // 21
println(halveDouble(8.0)) // 4.0

// More complex function factories
def createValidator(minLength: Int, maxLength: Int): String => Boolean = {
  text => text.length >= minLength && text.length <= maxLength
}

val passwordValidator = createValidator(8, 20)
val usernameValidator = createValidator(3, 15)

println(passwordValidator("secret"))      // false (too short)
println(passwordValidator("secretpassword"))  // true
println(usernameValidator("john"))        // true
println(usernameValidator("verylongusername")) // false (too long)

// Function composition helpers
def compose[A, B, C](f: B => C, g: A => B): A => C = {
  x => f(g(x))
}

val addOne = (x: Int) => x + 1
val multiplyByTwo = (x: Int) => x * 2

val addThenMultiply = compose(multiplyByTwo, addOne)
val multiplyThenAdd = compose(addOne, multiplyByTwo)

println(addThenMultiply(5))   // (5 + 1) * 2 = 12
println(multiplyThenAdd(5))   // (5 * 2) + 1 = 11

// Currying - converting multi-parameter functions to single-parameter functions
def add(x: Int, y: Int): Int = x + y

def curriedAdd(x: Int): Int => Int = y => x + y

val addFive = curriedAdd(5)
val addTen = curriedAdd(10)

println(addFive(3))  // 8
println(addTen(7))   // 17

// Scala's built-in currying support
def curriedMultiply(x: Int)(y: Int): Int = x * y

val timesFour = curriedMultiply(4) _  // Partial application
println(timesFour(6))  // 24

// Using curried functions with collections
val numbers = List(1, 2, 3, 4, 5)
val addedToAll = numbers.map(curriedAdd(10))
println(addedToAll)  // List(11, 12, 13, 14, 15)

Recursion: The Functional Loop

In functional programming, recursion replaces traditional loops. Well-designed recursive functions are often more expressive and easier to reason about than imperative loops.

Basic Recursion Patterns

// Tail recursion - efficient recursive pattern
import scala.annotation.tailrec

@tailrec
def factorialTailRec(n: Int, acc: Int = 1): Int = {
  if (n <= 1) acc
  else factorialTailRec(n - 1, n * acc)
}

@tailrec
def fibonacciTailRec(n: Int, a: Int = 0, b: Int = 1): Int = {
  if (n == 0) a
  else if (n == 1) b
  else fibonacciTailRec(n - 1, b, a + b)
}

// List processing with recursion
@tailrec
def sumList(list: List[Int], acc: Int = 0): Int = list match {
  case Nil => acc
  case head :: tail => sumList(tail, acc + head)
}

@tailrec
def findMax(list: List[Int], currentMax: Int = Int.MinValue): Int = list match {
  case Nil => currentMax
  case head :: tail => findMax(tail, math.max(head, currentMax))
}

def reverseList[A](list: List[A]): List[A] = {
  @tailrec
  def loop(remaining: List[A], acc: List[A]): List[A] = remaining match {
    case Nil => acc
    case head :: tail => loop(tail, head :: acc)
  }
  loop(list, Nil)
}

// Testing recursive functions
val numbers = List(5, 2, 8, 1, 9, 3)
println(s"Sum: ${sumList(numbers)}")           // Sum: 28
println(s"Max: ${findMax(numbers)}")           // Max: 9
println(s"Reversed: ${reverseList(numbers)}")  // Reversed: List(3, 9, 1, 8, 2, 5)

println(s"Factorial of 5: ${factorialTailRec(5)}")    // 120
println(s"10th Fibonacci: ${fibonacciTailRec(10)}")   // 55

// Tree recursion examples
sealed trait Tree[+A]
case object Empty extends Tree[Nothing]
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]

def treeSize[A](tree: Tree[A]): Int = tree match {
  case Empty => 0
  case Node(_, left, right) => 1 + treeSize(left) + treeSize(right)
}

def treeHeight[A](tree: Tree[A]): Int = tree match {
  case Empty => 0
  case Node(_, left, right) => 1 + math.max(treeHeight(left), treeHeight(right))
}

def treeSum(tree: Tree[Int]): Int = tree match {
  case Empty => 0
  case Node(value, left, right) => value + treeSum(left) + treeSum(right)
}

def treeContains[A](tree: Tree[A], target: A): Boolean = tree match {
  case Empty => false
  case Node(value, left, right) => 
    value == target || treeContains(left, target) || treeContains(right, target)
}

// Example tree:        5
//                    /   \
//                   3     8
//                  / \   / \
//                 1   4 7   9
val sampleTree = Node(5,
  Node(3, Node(1, Empty, Empty), Node(4, Empty, Empty)),
  Node(8, Node(7, Empty, Empty), Node(9, Empty, Empty))
)

println(s"Tree size: ${treeSize(sampleTree)}")         // 7
println(s"Tree height: ${treeHeight(sampleTree)}")     // 3
println(s"Tree sum: ${treeSum(sampleTree)}")           // 37
println(s"Contains 7: ${treeContains(sampleTree, 7)}") // true
println(s"Contains 6: ${treeContains(sampleTree, 6)}") // false

Function Composition and Pipelining

Function composition is the process of combining simple functions to build more complex ones. It's a fundamental technique in functional programming.

Basic Composition

// Function composition operators
val addOne = (x: Int) => x + 1
val multiplyByTwo = (x: Int) => x * 2
val square = (x: Int) => x * x

// Manual composition
val addThenMultiply = (x: Int) => multiplyByTwo(addOne(x))
val addThenSquare = (x: Int) => square(addOne(x))

println(addThenMultiply(5))  // (5 + 1) * 2 = 12
println(addThenSquare(3))    // (3 + 1)^2 = 16

// Using andThen for left-to-right composition
val pipeline1 = addOne andThen multiplyByTwo andThen square
println(pipeline1(2))  // ((2 + 1) * 2)^2 = 36

// Using compose for right-to-left composition
val pipeline2 = square compose multiplyByTwo compose addOne
println(pipeline2(2))  // Same result: 36

// Complex pipeline example
def isPositive(x: Int): Boolean = x > 0
def toString(x: Int): String = x.toString
def addPrefix(s: String): String = s"Number: $s"

val processNumber = addOne andThen multiplyByTwo andThen toString andThen addPrefix

println(processNumber(4))  // "Number: 10"

// Working with collections in pipelines
val words = List("hello", "world", "functional", "programming")

val pipeline = words
  .filter(_.length > 5)           // Keep words longer than 5 chars
  .map(_.toUpperCase)            // Convert to uppercase  
  .map(word => s"[$word]")       // Add brackets
  .sorted                        // Sort alphabetically

println(pipeline)  // List([FUNCTIONAL], [PROGRAMMING], [WORLD])

// Function composition with error handling
def safeDivide(x: Double, y: Double): Option[Double] = {
  if (y != 0) Some(x / y) else None
}

def addSafely(x: Option[Double], y: Double): Option[Double] = {
  x.map(_ + y)
}

def multiplySafely(x: Option[Double], y: Double): Option[Double] = {
  x.map(_ * y)
}

val safeCalculation = (x: Double, y: Double) => 
  safeDivide(x, y)
    .flatMap(result => addSafely(Some(result), 10))
    .flatMap(result => multiplySafely(result, 2))

println(safeCalculation(20, 4))  // Some(30.0) - (20/4 + 10) * 2
println(safeCalculation(20, 0))  // None - division by zero

Practical Functional Programming Examples

Data Processing Pipeline

// Sample data
case class Product(id: Int, name: String, category: String, price: Double, rating: Double)

val products = List(
  Product(1, "Laptop", "Electronics", 999.99, 4.5),
  Product(2, "Coffee Mug", "Kitchen", 12.99, 4.2),
  Product(3, "Book", "Education", 29.99, 4.8),
  Product(4, "Headphones", "Electronics", 199.99, 4.3),
  Product(5, "Desk Chair", "Furniture", 299.99, 4.1),
  Product(6, "Notebook", "Education", 5.99, 4.0),
  Product(7, "Monitor", "Electronics", 449.99, 4.6),
  Product(8, "Pen Set", "Office", 24.99, 3.9)
)

// Functional data processing
def analyzeProducts(products: List[Product]): Map[String, Double] = {
  products
    .filter(_.rating >= 4.0)                    // High-rated products only
    .groupBy(_.category)                        // Group by category
    .view.mapValues { categoryProducts =>       // Calculate average price per category
      val totalPrice = categoryProducts.map(_.price).sum
      val count = categoryProducts.length
      totalPrice / count
    }.toMap
}

def findTopProducts(products: List[Product], n: Int): List[Product] = {
  products
    .filter(_.price > 50)                       // Exclude very cheap items
    .sortBy(p => (-p.rating, p.price))          // Sort by rating desc, then price asc
    .take(n)                                    // Take top N
}

def categorySummary(products: List[Product]): List[(String, Int, Double, Double)] = {
  products
    .groupBy(_.category)
    .toList
    .map { case (category, categoryProducts) =>
      val count = categoryProducts.length
      val avgPrice = categoryProducts.map(_.price).sum / count
      val avgRating = categoryProducts.map(_.rating).sum / count
      (category, count, avgPrice, avgRating)
    }
    .sortBy(_._1)  // Sort by category name
}

// Using the functional pipelines
val averagePrices = analyzeProducts(products)
println("Average prices by category (high-rated products):")
averagePrices.foreach { case (category, avgPrice) =>
  println(f"  $category: $$${avgPrice}%.2f")
}

val topProducts = findTopProducts(products, 3)
println("\nTop 3 products:")
topProducts.foreach { product =>
  println(f"  ${product.name} - ${product.rating} stars - $$${product.price}%.2f")
}

val summary = categorySummary(products)
println("\nCategory Summary:")
summary.foreach { case (category, count, avgPrice, avgRating) =>
  println(f"  $category: $count products, avg price: $$${avgPrice}%.2f, avg rating: ${avgRating}%.1f")
}

Functional Configuration

// Functional approach to configuration
case class DatabaseConfig(host: String, port: Int, database: String)
case class ServerConfig(host: String, port: Int, ssl: Boolean)
case class AppConfig(database: DatabaseConfig, server: ServerConfig, debug: Boolean)

// Pure functions for configuration validation and transformation
def validatePort(port: Int): Either[String, Int] = {
  if (port > 0 && port <= 65535) Right(port)
  else Left(s"Invalid port: $port. Must be between 1 and 65535")
}

def validateHost(host: String): Either[String, String] = {
  if (host.nonEmpty && host.length <= 255) Right(host.toLowerCase)
  else Left(s"Invalid host: $host")
}

def parseConfig(env: Map[String, String]): Either[String, AppConfig] = {
  for {
    dbHost <- env.get("DB_HOST").toRight("Missing DB_HOST").flatMap(validateHost)
    dbPort <- env.get("DB_PORT").toRight("Missing DB_PORT").flatMap(port =>
      scala.util.Try(port.toInt).toEither.left.map(_ => s"Invalid DB_PORT: $port").flatMap(validatePort)
    )
    dbName <- env.get("DB_NAME").toRight("Missing DB_NAME")

    serverHost <- env.get("SERVER_HOST").toRight("Missing SERVER_HOST").flatMap(validateHost)
    serverPort <- env.get("SERVER_PORT").toRight("Missing SERVER_PORT").flatMap(port =>
      scala.util.Try(port.toInt).toEither.left.map(_ => s"Invalid SERVER_PORT: $port").flatMap(validatePort)
    )
    ssl = env.get("SSL_ENABLED").exists(_.toLowerCase == "true")
    debug = env.get("DEBUG").exists(_.toLowerCase == "true")
  } yield {
    AppConfig(
      DatabaseConfig(dbHost, dbPort, dbName),
      ServerConfig(serverHost, serverPort, ssl),
      debug
    )
  }
}

// Test configuration parsing
val validEnv = Map(
  "DB_HOST" -> "localhost",
  "DB_PORT" -> "5432",
  "DB_NAME" -> "myapp",
  "SERVER_HOST" -> "0.0.0.0",
  "SERVER_PORT" -> "8080",
  "SSL_ENABLED" -> "true",
  "DEBUG" -> "false"
)

val invalidEnv = Map(
  "DB_HOST" -> "",
  "DB_PORT" -> "99999",
  "DB_NAME" -> "myapp"
  // Missing server config
)

parseConfig(validEnv) match {
  case Right(config) => println(s"Valid config: $config")
  case Left(error) => println(s"Configuration error: $error")
}

parseConfig(invalidEnv) match {
  case Right(config) => println(s"Valid config: $config")
  case Left(error) => println(s"Configuration error: $error")
}

Benefits of Functional Programming

Testability

// Pure functions are easy to test
def calculateTax(amount: Double, rate: Double): Double = amount * rate

def calculateTotal(subtotal: Double, taxRate: Double, discount: Double): Double = {
  val tax = calculateTax(subtotal, taxRate)
  subtotal + tax - discount
}

// Simple, predictable tests
val testCases = List(
  (100.0, 0.08, 0.0, 108.0),    // Basic case
  (100.0, 0.0, 0.0, 100.0),     // No tax
  (100.0, 0.08, 10.0, 98.0),    // With discount
  (0.0, 0.08, 0.0, 0.0)         // Zero amount
)

testCases.foreach { case (subtotal, taxRate, discount, expected) =>
  val result = calculateTotal(subtotal, taxRate, discount)
  val passed = math.abs(result - expected) < 0.001
  println(s"Test ${if (passed) "PASSED" else "FAILED"}: $subtotal + tax($taxRate) - $discount = $result (expected $expected)")
}

Composability

// Small, composable functions
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val isEven = (n: Int) => n % 2 == 0
val square = (n: Int) => n * n
val greaterThan25 = (n: Int) => n > 25

// Compose operations declaratively
val result1 = numbers
  .filter(isEven)
  .map(square)
  .filter(greaterThan25)

println(result1)  // List(36, 64, 100)

// Easy to modify and extend
val isOdd = (n: Int) => n % 2 != 0
val cube = (n: Int) => n * n * n

val result2 = numbers
  .filter(isOdd)
  .map(cube)
  .filter(greaterThan25)

println(result2)  // List(27, 125, 343, 729)

Summary

In this lesson, you've learned the foundational concepts of functional programming in Scala:

Immutability: Creating new objects instead of modifying existing ones
Pure Functions: Deterministic functions without side effects
Referential Transparency: Expressions that can be replaced by their values
Higher-Order Functions: Functions that work with other functions
Recursion: The functional alternative to loops
Function Composition: Building complex operations from simple functions
Practical Benefits: Better testability, composability, and maintainability

These concepts form the foundation for more advanced functional programming topics like monads, functional collections, and concurrent programming.

What's Next

In the next lesson, we'll dive deeper into higher-order functions and explore Scala's powerful collection methods like map, filter, fold, and flatMap. You'll learn how to manipulate and transform data using these functional tools effectively.