Welcome back to my functional programming journey! In Week 1, I explored the foundational concepts of functional programming: pure functions, immutability, and algebraic data types. This week, I’m diving into the practical side of Scala development.
I’ve started working with an actual Scala codebase. So this week’s focus areas emerged from real development needs: wrestling with SBT, discovering curried functions for creating reusable code patterns, and getting comfortable with Scala’s collections API. Let me share what I’ve learned.
🏗️ SBT (Scala Build Tool) #
SBT is the standard build tool for Scala projects. Think of it as the Maven or Gradle of the Scala world.
Basic SBT Commands #
Core Commands:
sbt- Start the interactive SBT shellrun- Compile and run your projectcompile- Compile your source codeclean- Delete all generated files (target directory)test- Run all tests in the current module (project)testOnly- Run a specific testtestOnly io.project.monitoring.db.RepoSpec- Run a single test:
testOnly io.project.monitoring.db.RepoSpec -- -t "should filter by alertType"
Note: Don’t have to type out a full path can just use a wildcard
testOnly *RepoSpec
Project Management #
new- Create a new project (e.g.,sbt new scala/hello-world.g8)projects- List all subprojectsproject <name>- Switch to a specific subproject- Use
project rootto switch back to the root project
- Use
🔄 Continuous Actions #
Automatic recompilation on file changes:
~compile- Continuously compile (recompile when files change)~test- Continuously run tests~testOnly- Continuously run a specific test~run- Continuously compile and run
Scalafmt (Code Formatter) #
Core Commands:
scalafmt- Format main source filesscalafmtAll- Format all sources (main, test, and sbt files)
Configuration #
- Configuration is defined in
.scalafmt.conf
Scalafix (Code Linter & Migration Tool) #
Core Commands:
scalafix- Run scalafix rules on main sourcesscalafixAll- Run scalafix on all sources
Rule Management #
scalafix --rules=RuleName- Run specific rulescalafix --rules=OrganizeImports- Organize and clean up imports
Popular Built-in Rules:
OrganizeImports- Sort and organize import statementsRemoveUnused- Remove unused imports and variablesDisableSyntax- Disable deprecated Scala syntax
Setup:
- Configure rules in
.scalafix.conf
🍛 Currying Functions #
Understanding the Currying function in Scala. | by Ajaykumar Dev | Medium
What is Currying? #
Currying transforms a function that takes multiple arguments into a series of functions that each take a single argument. Instead of f(a, b, c), you get f(a)(b)(c).
Basic Syntax #
Manual Currying #
// Regular function
def add(x: Int, y: Int): Int = x + y
// Manually curried function
def addCurried(x: Int)(y: Int): Int = x + y
// Usage
val result1 = add(5, 3) // 8
val result2 = addCurried(5)(3) // 8Using the curried Method
#
// Convert existing function to curried form
val addFunction = (x: Int, y: Int) => x + y
val curriedAdd = addFunction.curried
// Usage
val result = curriedAdd(5)(3) // 8Partial Application #
Currying enables partial application, fixing some arguments and creating new functions:
def multiply(x: Int)(y: Int)(z: Int): Int = x * y * z
// Partial application
val multiplyBy2 = multiply(2) // don't need to have a trailing `_`
val multiplyBy2And3 = multiply(2)(3) // don't need to have a trailing `_`
// Usage
val result1 = multiplyBy2(5)(10) // 100
val result2 = multiplyBy2And3(4) // 24Practical Examples #
Configuration Pattern #
Perfect for creating specialised loggers or processors:
def logMessage(level: String)(timestamp: String)(message: String): Unit =
println(s"[$level] $timestamp: $message")
// Create specialized loggers
val errorLogger = logMessage("ERROR") // don't need to have a trailing `_`
val infoLogger = logMessage("INFO") // don't need to have a trailing `_`
// Usage
errorLogger("2024-01-15")("Database connection failed")
// [ERROR] 2024-01-15: Database connection failed
infoLogger("2024-01-15")("Application started")
// [INFO] 2024-01-15: Application startedHigher-Order Functions #
def processData(processor: String => String)(data: List[String]): List[String] =
data.map(processor)
// Create specialized processors
val upperCaseProcessor = processData(_.toUpperCase) // don't need to have a trailing `_`
val trimProcessor = processData(_.trim) // don't need to have a trailing `_`
// Usage
val data = List(" hello ", " world ")
val upperCased = upperCaseProcessor(data) // List(" HELLO ", " WORLD ")
val trimmed = trimProcessor(data) // List("hello", "world")Function Composition #
Curried functions compose naturally like LEGO blocks:
def add(x: Int)(y: Int): Int = x + y
def multiply(x: Int)(y: Int): Int = x * y
val addFive = add(5) // don't need to have a trailing `_`
val multiplyByThree = multiply(3) // don't need to have a trailing `_`
// Compose functions
val combined = (x: Int) => multiplyByThree(addFive(x))
val result = combined(10) // (10 + 5) * 3 = 45Best Practices #
When to Use Currying #
- When you frequently need partial application
- For creating specialised versions of generic functions
- When building function pipelines
- For dependency injection patterns
When NOT to Use Currying #
- Simple functions that don’t benefit from partial application
- When all arguments are always provided together
- For beginners learning Scala (can be confusing at first)
Common Patterns #
Builder Pattern #
case class Person(name: String, age: Int, email: String)
def createPerson(name: String)(age: Int)(email: String): Person =
Person(name, age, email)
val johnBuilder = createPerson("John")
val john25 = johnBuilder(25)
val johnComplete = john25("[email protected]")Validation Chain #
def validate(data: String)(rule: String => Boolean)(errorMsg: String): Either[String, String] =
if (rule(data)) Right(data) else Left(errorMsg)
val validateEmail = validate(_: String)(_.contains("@"))("Invalid email")
val validateLength = validate(_: String)(_.length > 5)("Too short")
// Chain validations
val email = "[email protected]"
val result = validateEmail(email).flatMap(validateLength)💡 Key takeaway: Currying is a powerful technique that makes Scala code more modular and reusable. Use it when you need flexible function composition and partial application.
📚 Scala Collections API Guide #
Overview #
Scala’s collections library is built around a unified hierarchy with consistent operations across different collection types. Collections are either mutable or immutable, with immutable being the default (and recommended for functional programming).
Core Hierarchy #
Iterable
├── Seq (ordered collections)
│ ├── List (linked list)
│ ├── Vector (indexed sequence)
│ ├── Array (mutable, efficient)
│ └── Range (lazy sequence of numbers)
├── Set (unique elements)
│ ├── HashSet (unordered)
│ └── TreeSet (sorted)
└── Map (key-value pairs)
├── HashMap (unordered)
└── TreeMap (sorted by keys)Essential Collection Types #
Lists #
Key characteristics:
- Immutable by default
- Prepend-optimised (O(1) for head operations)
- Pattern:
head :: tail
val list = List(1, 2, 3)
val newList = 0 :: list // List(0, 1, 2, 3) - prepend (fast!)
val appended = list :+ 4 // List(1, 2, 3, 4) - append (slow!)
val concatenated = list ++ List(4, 5) // List(1, 2, 3, 4, 5)
// Pattern matching
list match {
case head :: tail => println(s"Head: $head, Tail: $tail")
case Nil => println("Empty list")
}
// Basic operations
val head = list.head // 1
val tail = list.tail // List(2, 3)
val length = list.length // 3
val isEmpty = list.isEmpty // false
val contains = list.contains(2) // trueVectors #
Key characteristics:
- Balanced for random access and updates
- Good general-purpose choice when you need indexed access
- O(log₃₂n) for most operations (effectively constant time)
- More efficient than Lists for random access
val vector = Vector(1, 2, 3)
val updated = vector.updated(1, 5) // Vector(1, 5, 3)
// Random Access Operations
val data = Vector(10, 20, 30, 40, 50)
val third = data(2) // 30 - O(log₃₂n) access
val slice = data.slice(1, 4) // Vector(20, 30, 40)
// Building Collections Incrementally
// Efficient append operations
val builder = Vector.newBuilder[Int]
for (i <- 1 to 1000) {
builder += i * i
}
val squares = builder.result() // Vector(1, 4, 9, 16, ...)
// Or using foldLeft
val numbers = (1 to 100).foldLeft(Vector.empty[Int])(_ :+ _)
// Insert and Update
val timeline = Vector(
"9:00 AM - Meeting",
"10:30 AM - Code review",
"12:00 PM - Lunch",
"2:00 PM - Development",
"4:00 PM - Testing"
)
// Insert at specific position
val withBreak = timeline.patch(3, Vector("1:00 PM - Break"), 0)
// Update specific time slot
val revised = timeline.updated(1, "10:30 AM - Architecture discussion")Arrays #
Key characteristics:
- Mutable by default
- O(1) random access and updates
- Fixed size, most memory efficient
- Closest to Java arrays
val array = Array(1, 2, 3)
array(0) = 10 // Array(10, 2, 3) - mutable update
val element = array(1) // 2 - O(1) access
val length = array.length // 3
// Creating arrays
val zeros = Array.fill(5)(0) // Array(0, 0, 0, 0, 0)
val range = Array.range(1, 6) // Array(1, 2, 3, 4, 5)
val fromList = List(1, 2, 3).toArray // Array(1, 2, 3)
// Basic operations (return new arrays)
val doubled = array.map(_ * 2) // Array(20, 4, 6)
val filtered = array.filter(_ > 5) // Array(10)
val sorted = Array(3, 1, 2).sorted // Array(1, 2, 3)
// Convert to other collections
val asList = array.toList // List(10, 2, 3)
val asVector = array.toVector // Vector(10, 2, 3)⚖️ Vector vs Array: When to Use What? #
| Aspect | Vector | Array |
|---|---|---|
| Mutability | Immutable - returns new instances | Mutable - modify in place |
| Performance | O(log₃₂ n) operations (effectively constant) | O(1) random access |
| Thread Safety | Thread-safe | Not thread-safe |
| Memory | Higher overhead | More memory efficient |
| Use Case | General use, functional style | High performance, frequent mutations |
💡 Rule of thumb: Use Vector for functional programming, Array when you need maximum performance and don’t mind mutability.
Sets #
Key characteristics:
- No duplicates allowed
- Fast membership testing O(1) for HashSet
- HashSet for unordered, TreeSet for sorted
val set = Set(1, 2, 3, 2) // Set(1, 2, 3) - duplicates removed
val added = set + 4 // Set(1, 2, 3, 4)
val removed = set - 2 // Set(1, 3)
val union = set ++ Set(4, 5) // Set(1, 2, 3, 4, 5)
// Basic operations
val contains = set.contains(2) // true
val size = set.size // 3
val isEmpty = set.isEmpty // false
// Set operations (mathematical!)
val set2 = Set(2, 3, 4)
val intersection = set & set2 // Set(2, 3)
val difference = set -- set2 // Set(1)
val isSubset = Set(1, 2).subsetOf(set) // true
// Convert to other collections
val asList = set.toList // List(1, 2, 3) - order not guaranteed
val asVector = set.toVector // Vector(1, 2, 3)Maps #
Key characteristics:
- Key-value associations
- HashMap for unordered (fast), TreeMap for sorted keys
val map = Map("a" -> 1, "b" -> 2)
val added = map + ("c" -> 3) // Map("a" -> 1, "b" -> 2, "c" -> 3)
val removed = map - "a" // Map("b" -> 2)
val updated = map.updated("b", 5) // Map("a" -> 1, "b" -> 5)
// Basic operations
val value = map.get("a") // Some(1) - safe access
val valueOrDefault = map.getOrElse("c", 0) // 0 - with default
val directAccess = map("a") // 1 - throws exception if key missing!
val contains = map.contains("b") // true
val size = map.size // 2
// Working with keys and values
val keys = map.keys // Set("a", "b")
val values = map.values // Iterable(1, 2)
val pairs = map.toList // List(("a", 1), ("b", 2))
// Merging maps
val map2 = Map("b" -> 3, "d" -> 4)
val merged = map ++ map2 // Map("a" -> 1, "b" -> 3, "d" -> 4)
// Transform values
val doubled = map.view.mapValues(_ * 2).toMap // Map("a" -> 2, "b" -> 4)Core Operations #
Transformation Methods #
| Method | Purpose | Example | Output |
|---|---|---|---|
map |
Transform each element | List(1,2,3).map(_ * 2) |
List(2,4,6) |
filter |
Keep elements matching predicate | List(1,2,3,4).filter(_ > 2) |
List(3,4) |
flatMap |
Map and flatten | List(1,2).flatMap(x => List(x, x)) |
List(1,1,2,2) |
collect |
Partial function transformation | List(1,2,3).collect { case x if x > 1 => x * 2 } |
List(4,6) |
Reduction Methods #
| Method | Purpose | Example | Note |
|---|---|---|---|
fold |
Reduce with initial value | List(1,2,3).fold(0)(_ + _) |
Safe - works on empty collections |
reduce |
Reduce without initial value | List(1,2,3).reduce(_ + _) |
Throws exception on empty collections |
aggregate |
Parallel-friendly reduction | List(1,2,3).aggregate(0)(_ + _, _ + _) |
Good for parallel collections |
Grouping and Partitioning #
val numbers = List(1, 2, 3, 4, 5, 6)
// Group by condition - creates a Map
val grouped = numbers.groupBy(_ % 2)
// Result: Map(1 -> List(1,3,5), 0 -> List(2,4,6))
// Partition into two groups - creates a Tuple
val (evens, odds) = numbers.partition(_ % 2 == 0)
// Result: (List(2,4,6), List(1,3,5))For-Comprehensions #
Syntactic sugar for chaining map, flatMap, and filter:
// For-comprehension
val result = for {
x <- List(1, 2, 3)
y <- List(10, 20)
if x > 1
} yield x * y
// Equivalent to:
val result2 = List(1,2,3)
.flatMap(x =>
List(10,20)
.filter(_ => x > 1)
.map(y => x * y)
)
// Both produce: List(20, 40, 30, 60)Views and Lazy Evaluation #
Views create lazy collections that defer computation until needed:
val numbers = (1 to 1000000).view // Lazy - no computation yet!
val result = numbers
.map(_ * 2) // Still lazy
.filter(_ > 100) // Still lazy
.take(10) // Still lazy
.toList // NOW computation happens!
// Only processes the minimum needed elementsMutable vs Immutable #
🔒 Immutable (Default - Recommended) #
- Located in
scala.collection.immutable - Thread-safe by design
- Operations return new collections
- Import not needed (it’s the default)
- Better for functional programming
🔓 Mutable #
- Located in
scala.collection.mutable - Not thread-safe but more efficient for repeated updates
- Operations modify collections in-place
- Requires explicit import
import scala.collection.mutable
val mutableList = mutable.ListBuffer(1, 2, 3)
mutableList += 4 // Modifies existing collection
mutableList.append(5) // Also modifies in place
val mutableMap = mutable.Map("a" -> 1)
mutableMap("b") = 2 // Adds to existing map
mutableMap.remove("a") // Removes from existing mapPerformance Characteristics #
For detailed performance information, see the official Scala documentation.