Writing Reusable Code with Functions
Introduction
Functions are the building blocks of any Scala program. They allow you to break down complex problems into smaller, manageable pieces, promote code reuse, and make your programs more modular and testable.
In this lesson, you'll learn how to define functions in Scala, work with parameters and return types, and leverage Scala's powerful type inference to write clean, expressive code. We'll also explore how functions in Scala are first-class citizens, setting the foundation for functional programming concepts.
Basic Function Syntax
Simple Function Definition
The basic syntax for defining a function in Scala uses the def
keyword:
def functionName(parameter: Type): ReturnType = {
// function body
result
}
Let's start with a simple example:
def greet(name: String): String = {
s"Hello, $name!"
}
// Calling the function
val message = greet("Alice")
println(message) // "Hello, Alice!"
Single Expression Functions
For simple functions that contain only one expression, you can omit the curly braces:
def add(a: Int, b: Int): Int = a + b
def square(x: Int): Int = x * x
def isEven(n: Int): Boolean = n % 2 == 0
// Usage
println(add(5, 3)) // 8
println(square(4)) // 16
println(isEven(7)) // false
Type Inference
Scala's type inference can often determine return types automatically:
// Explicit return type (good practice for public methods)
def multiply(a: Int, b: Int): Int = a * b
// Inferred return type (compiler figures out it's Int)
def multiplyInferred(a: Int, b: Int) = a * b
// For simple functions, type inference works well
def max(a: Int, b: Int) = if (a > b) a else b
// But sometimes explicit types improve readability
def calculateArea(radius: Double): Double = math.Pi * radius * radius
Best Practice: Use explicit return types for:
- Public methods in APIs
- Complex functions where the return type isn't obvious
- Recursive functions (required)
Functions with Multiple Parameters
Multiple Parameter Lists
Scala allows functions to have multiple parameter lists:
def multiply(x: Int)(y: Int): Int = x * y
// Usage
val result = multiply(5)(3) // 15
// This enables currying (partial application)
val multiplyBy5 = multiply(5) _
val result2 = multiplyBy5(3) // 15
Default Parameters
You can provide default values for parameters:
def greetUser(name: String, greeting: String = "Hello", punctuation: String = "!") = {
s"$greeting, $name$punctuation"
}
// Usage with defaults
println(greetUser("Bob")) // "Hello, Bob!"
println(greetUser("Alice", "Hi")) // "Hi, Alice!"
println(greetUser("Charlie", "Hey", "?")) // "Hey, Charlie?"
Named Parameters
You can specify parameters by name, which allows you to change their order:
def createUser(name: String, age: Int, email: String, isActive: Boolean = true) = {
s"User($name, $age, $email, $isActive)"
}
// Positional arguments
val user1 = createUser("Alice", 30, "alice@example.com")
// Named arguments (order doesn't matter)
val user2 = createUser(
email = "bob@example.com",
name = "Bob",
age = 25
)
// Mix of positional and named
val user3 = createUser("Charlie", age = 35, email = "charlie@example.com")
Return Types and Unit
Functions that Return Values
def calculateCircleArea(radius: Double): Double = {
math.Pi * radius * radius
}
def getFullName(firstName: String, lastName: String): String = {
s"$firstName $lastName"
}
def factorial(n: Int): Long = {
if (n <= 1) 1L
else n * factorial(n - 1)
}
Functions that Perform Actions (Unit)
Functions that don't return meaningful values have return type Unit
:
def printWelcome(name: String): Unit = {
println(s"Welcome, $name!")
println("Please enjoy your stay.")
}
// Unit can be omitted (inferred)
def logMessage(message: String) = {
val timestamp = java.time.LocalDateTime.now()
println(s"[$timestamp] $message")
}
// Functions that only perform side effects
def saveToFile(filename: String, content: String): Unit = {
import java.io.PrintWriter
val writer = new PrintWriter(filename)
try {
writer.write(content)
} finally {
writer.close()
}
}
Function Bodies and Blocks
Single Expression vs Block Body
// Single expression (no braces needed)
def double(x: Int) = x * 2
// Block body for multiple statements
def processNumber(x: Int): String = {
val doubled = x * 2
val squared = x * x
val isEven = x % 2 == 0
s"Number: $x, Doubled: $doubled, Squared: $squared, Even: $isEven"
}
// Block can contain complex logic
def analyzeGrade(score: Int): String = {
val normalizedScore = math.max(0, math.min(100, score)) // Clamp to 0-100
val letterGrade = if (normalizedScore >= 90) "A"
else if (normalizedScore >= 80) "B"
else if (normalizedScore >= 70) "C"
else if (normalizedScore >= 60) "D"
else "F"
val status = if (normalizedScore >= 60) "Pass" else "Fail"
s"Score: $normalizedScore, Grade: $letterGrade, Status: $status"
}
Local Functions
You can define functions inside other functions (local functions):
def calculateTax(income: Double, state: String): Double = {
// Local function for federal tax
def federalTax(amount: Double): Double = {
if (amount <= 50000) amount * 0.1
else if (amount <= 100000) 5000 + (amount - 50000) * 0.2
else 15000 + (amount - 100000) * 0.3
}
// Local function for state tax
def stateTax(amount: Double, state: String): Double = state.toLowerCase match {
case "ca" => amount * 0.08
case "ny" => amount * 0.07
case "tx" => 0.0 // No state tax
case _ => amount * 0.05 // Default rate
}
federalTax(income) + stateTax(income, state)
}
val totalTax = calculateTax(75000, "CA")
println(f"Total tax: $$${totalTax}%.2f")
Variable Arguments (Varargs)
Functions can accept a variable number of arguments:
def sum(numbers: Int*): Int = {
numbers.sum
}
// Usage
println(sum(1, 2, 3)) // 6
println(sum(1, 2, 3, 4, 5)) // 15
println(sum()) // 0
// More complex example
def createMessage(prefix: String, words: String*): String = {
val content = words.mkString(" ")
s"$prefix: $content"
}
println(createMessage("Info", "System", "is", "running"))
// "Info: System is running"
// You can pass a collection as varargs using :_*
val wordList = List("Hello", "world")
println(createMessage("Greeting", wordList: _*))
// "Greeting: Hello world"
Recursive Functions
Recursive functions must have explicit return types:
def factorial(n: Int): Long = {
if (n <= 1) 1L
else n * factorial(n - 1)
}
def fibonacci(n: Int): Long = {
if (n <= 1) n.toLong
else fibonacci(n - 1) + fibonacci(n - 2)
}
// Tail recursion (more efficient)
def factorialTailRec(n: Int): Long = {
@annotation.tailrec
def helper(remaining: Int, accumulator: Long): Long = {
if (remaining <= 1) accumulator
else helper(remaining - 1, remaining * accumulator)
}
helper(n, 1L)
}
// Testing
println(factorial(5)) // 120
println(fibonacci(10)) // 55
println(factorialTailRec(5)) // 120
Function Overloading
You can define multiple functions with the same name but different parameters:
def format(value: Int): String = value.toString
def format(value: Double): String = f"$value%.2f"
def format(value: String): String = s"'$value'"
def format(value: Boolean): String = if (value) "YES" else "NO"
// Usage
println(format(42)) // "42"
println(format(3.14159)) // "3.14"
println(format("hello")) // "'hello'"
println(format(true)) // "YES"
// Overloading with different parameter counts
def max(a: Int, b: Int): Int = if (a > b) a else b
def max(a: Int, b: Int, c: Int): Int = max(max(a, b), c)
def max(numbers: Int*): Int = numbers.max
Practical Examples
Example 1: String Utilities
object StringUtils {
def capitalize(text: String): String = {
if (text.isEmpty) text
else text.head.toUpper + text.tail.toLowerCase
}
def truncate(text: String, maxLength: Int, suffix: String = "..."): String = {
if (text.length <= maxLength) text
else text.take(maxLength - suffix.length) + suffix
}
def countWords(text: String): Int = {
if (text.trim.isEmpty) 0
else text.trim.split("\\s+").length
}
def isPalindrome(text: String): Boolean = {
val cleaned = text.toLowerCase.replaceAll("[^a-z0-9]", "")
cleaned == cleaned.reverse
}
def generateSlug(title: String): String = {
title.toLowerCase
.replaceAll("[^a-z0-9\\s]", "")
.trim
.replaceAll("\\s+", "-")
}
}
// Usage
import StringUtils._
println(capitalize("hello world")) // "Hello world"
println(truncate("This is a long sentence", 10)) // "This is..."
println(countWords("Hello world from Scala")) // 4
println(isPalindrome("A man a plan a canal Panama")) // true
println(generateSlug("My First Scala Program!")) // "my-first-scala-program"
Example 2: Mathematical Functions
object MathUtils {
def isPrime(n: Int): Boolean = {
if (n <= 1) false
else if (n <= 3) true
else if (n % 2 == 0 || n % 3 == 0) false
else {
@annotation.tailrec
def checkDivisors(i: Int): Boolean = {
if (i * i > n) true
else if (n % i == 0 || n % (i + 2) == 0) false
else checkDivisors(i + 6)
}
checkDivisors(5)
}
}
def gcd(a: Int, b: Int): Int = {
@annotation.tailrec
def gcdHelper(x: Int, y: Int): Int = {
if (y == 0) x
else gcdHelper(y, x % y)
}
gcdHelper(math.abs(a), math.abs(b))
}
def lcm(a: Int, b: Int): Int = {
math.abs(a * b) / gcd(a, b)
}
def power(base: Double, exponent: Int): Double = {
@annotation.tailrec
def powerHelper(acc: Double, exp: Int): Double = {
if (exp == 0) acc
else if (exp % 2 == 0) powerHelper(acc * acc, exp / 2)
else powerHelper(acc * base, exp - 1)
}
if (exponent >= 0) powerHelper(1.0, exponent)
else 1.0 / powerHelper(1.0, -exponent)
}
}
// Usage
import MathUtils._
println(isPrime(17)) // true
println(isPrime(15)) // false
println(gcd(48, 18)) // 6
println(lcm(12, 18)) // 36
println(power(2.0, 10)) // 1024.0
Example 3: Data Processing
case class Student(name: String, grades: List[Int])
object GradeProcessor {
def calculateAverage(grades: List[Int]): Double = {
if (grades.isEmpty) 0.0
else grades.sum.toDouble / grades.length
}
def getLetterGrade(average: Double): String = {
if (average >= 90) "A"
else if (average >= 80) "B"
else if (average >= 70) "C"
else if (average >= 60) "D"
else "F"
}
def processStudent(student: Student): String = {
val avg = calculateAverage(student.grades)
val letter = getLetterGrade(avg)
val status = if (avg >= 60) "Passing" else "Failing"
f"${student.name}: Average ${avg}%.1f ($letter) - $status"
}
def findTopStudent(students: List[Student]): Option[Student] = {
if (students.isEmpty) None
else {
val bestStudent = students.maxBy(s => calculateAverage(s.grades))
Some(bestStudent)
}
}
def getClassStatistics(students: List[Student]): String = {
if (students.isEmpty) "No students"
else {
val averages = students.map(s => calculateAverage(s.grades))
val classAvg = averages.sum / averages.length
val highest = averages.max
val lowest = averages.min
val passingCount = averages.count(_ >= 60)
f"""Class Statistics:
|Students: ${students.length}
|Class Average: $classAvg%.1f
|Highest Average: $highest%.1f
|Lowest Average: $lowest%.1f
|Passing Students: $passingCount/${students.length}""".stripMargin
}
}
}
// Usage
val students = List(
Student("Alice", List(95, 87, 92, 88)),
Student("Bob", List(78, 82, 75, 80)),
Student("Charlie", List(55, 60, 58, 62)),
Student("Diana", List(90, 94, 89, 97))
)
students.foreach(student => println(GradeProcessor.processStudent(student)))
println("\n" + GradeProcessor.getClassStatistics(students))
GradeProcessor.findTopStudent(students) match {
case Some(student) => println(s"\nTop student: ${student.name}")
case None => println("\nNo students found")
}
Common Patterns and Best Practices
1. Pure Functions
Prefer pure functions (no side effects, same input always produces same output):
// Pure function - good
def calculateTip(bill: Double, percentage: Double): Double = {
bill * (percentage / 100)
}
// Impure function - has side effects
def calculateTipAndPrint(bill: Double, percentage: Double): Double = {
val tip = bill * (percentage / 100)
println(s"Tip: $$${tip}") // Side effect: printing
tip
}
// Better: Separate calculation from side effects
def formatTipMessage(bill: Double, tip: Double): String = {
f"Bill: $$${bill}%.2f, Tip: $$${tip}%.2f, Total: $$${bill + tip}%.2f"
}
val bill = 50.0
val tip = calculateTip(bill, 18.0)
println(formatTipMessage(bill, tip))
2. Function Composition
Break complex problems into smaller functions:
def validateEmail(email: String): Boolean = {
def hasAtSymbol(s: String): Boolean = s.contains("@")
def hasValidFormat(s: String): Boolean = s.matches(""".+@.+\..+""")
def isNotEmpty(s: String): Boolean = s.trim.nonEmpty
isNotEmpty(email) && hasAtSymbol(email) && hasValidFormat(email)
}
def processUserInput(input: String): String = {
def trimSpaces(s: String): String = s.trim
def toLowerCase(s: String): String = s.toLowerCase
def removeSpecialChars(s: String): String = s.replaceAll("[^a-z0-9]", "")
removeSpecialChars(toLowerCase(trimSpaces(input)))
}
3. Error Handling
Use return types that express possible failure:
// Using Option for functions that might not return a value
def safeDivide(a: Double, b: Double): Option[Double] = {
if (b == 0) None
else Some(a / b)
}
// Using Either for functions that can fail with an error message
def parseAge(input: String): Either[String, Int] = {
try {
val age = input.toInt
if (age < 0) Left("Age cannot be negative")
else if (age > 150) Left("Age seems unrealistic")
else Right(age)
} catch {
case _: NumberFormatException => Left(s"'$input' is not a valid number")
}
}
// Usage
safeDivide(10, 2) match {
case Some(result) => println(s"Result: $result")
case None => println("Cannot divide by zero")
}
parseAge("25") match {
case Right(age) => println(s"Valid age: $age")
case Left(error) => println(s"Error: $error")
}
Summary
In this lesson, you've learned the fundamentals of writing functions in Scala:
✅ Function Definition: Using def
keyword with parameters and return types
✅ Type Inference: Letting Scala figure out return types when appropriate
✅ Parameter Features: Default values, named parameters, variable arguments
✅ Function Bodies: Single expressions vs multi-statement blocks
✅ Local Functions: Defining helper functions inside other functions
✅ Recursion: Writing recursive functions with proper return types
✅ Overloading: Multiple functions with the same name
✅ Best Practices: Pure functions, composition, and error handling
Functions are essential building blocks that make your code modular, reusable, and testable. The concepts you've learned here will be crucial as we move into more advanced topics like higher-order functions and functional programming patterns.
What's Next
In the next lesson, we'll dive into "A Deeper Dive into Strings: Interpolation and Multiline." You'll master string manipulation in Scala, including advanced interpolation techniques, formatting options, and working with complex string patterns.
We'll explore how Scala's string features can make your code more readable and help you handle text processing tasks elegantly.
Ready to become a string manipulation expert? Let's continue!
Comments
Be the first to comment on this lesson!