Skip to main content
Scala: SBT, Currying and Collections
  1. Posts/

Scala: SBT, Currying and Collections

·2251 words·11 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

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 shell
  • run - Compile and run your project
  • compile - Compile your source code
  • clean - Delete all generated files (target directory)
  • test - Run all tests in the current module (project)
  • testOnly - Run a specific test
    • testOnly 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 subprojects
  • project <name> - Switch to a specific subproject
    • Use project root to switch back to the root project

🔄 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 files
  • scalafmtAll - 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 sources
  • scalafixAll - Run scalafix on all sources

Rule Management
#

  • scalafix --rules=RuleName - Run specific rule
  • scalafix --rules=OrganizeImports - Organize and clean up imports

Popular Built-in Rules:

Build-in Rules

  • OrganizeImports - Sort and organize import statements
  • RemoveUnused - Remove unused imports and variables
  • DisableSyntax - 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) // 8

Using 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) // 8

Partial 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)    // 24

Practical 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 started

Higher-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 = 45

Best 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)   // true

Vectors
#

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 elements

Mutable 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 map

Performance Characteristics
#

For detailed performance information, see the official Scala documentation.