FP Design Patterns
Functional programming emphasizes the use of functions, immutability, and expressions over statements. Many traditional design patterns can be adapted to a functional style in Scala. Here are examples of functional programming design patterns in Scala:
1. Function Composition
Function composition is a fundamental concept in functional programming, where multiple functions are combined to produce a new function.
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
val addOneAndDouble: Int => Int = addOne.andThen(double)
// Usage
println(addOneAndDouble(3)) // 8 (3 + 1, then 4 * 2)
2. Higher-Order Functions
Higher-order functions are functions that take other functions as parameters or return functions as results.
def applyOperation(x: Int, y: Int, operation: (Int, Int) => Int): Int = operation(x, y)
val add: (Int, Int) => Int = _ + _
val multiply: (Int, Int) => Int = _ * _
// Usage
println(applyOperation(3, 4, add)) // 7
println(applyOperation(3, 4, multiply)) // 12
3. Currying
Currying transforms a function with multiple parameters into a series of functions, each with a single parameter.
def add(x: Int)(y: Int): Int = x + y
val addThree: Int => Int = add(3)
// Usage
println(addThree(4)) // 7
println(add(2)(3)) // 5
4. Partial Function Application
Partial function application involves fixing a few arguments of a function, producing another function of smaller arity.
def multiply(x: Int, y: Int, z: Int): Int = x * y * z
val multiplyBy2And3: Int => Int = multiply(2, 3, _)
// Usage
println(multiplyBy2And3(4)) // 24 (2 * 3 * 4)
5. Memoization
Memoization is an optimization technique to cache the results of expensive function calls and return the cached result when the same inputs occur again.
def memoize[I, O](f: I => O): I => O = {
val cache = scala.collection.mutable.Map[I, O]()
(input: I) =>
cache.getOrElseUpdate(input, f(input))
}
val fib: Int => Int = {
def fibHelper(n: Int): Int = n match {
case 0 => 0
case 1 => 1
case _ => fib(n - 1) + fib(n - 2)
}
memoize(fibHelper)
}
// Usage
println(fib(10)) // 55
6. Functor
A functor is a structure that can be mapped over. In Scala, the map method on collections is an example.
val list = List(1, 2, 3)
val doubledList = list.map(_ * 2)
// Usage
println(doubledList) // List(2, 4, 6)
7. Monad
A monad is a design pattern used to handle program-wide concerns in a functional way. The flatMap method is used to chain operations.
val list = List(1, 2, 3)
val result = list.flatMap(x => List(x, x * 2))
// Usage
println(result) // List(1, 2, 2, 4, 3, 6)
8. Option Monad
The Option monad is used to represent optional values, providing a way to handle the absence of a value.
val someValue: Option[Int] = Some(5)
val noValue: Option[Int] = None
val result = someValue.map(_ * 2).getOrElse(0)
val result2 = noValue.map(_ * 2).getOrElse(0)
// Usage
println(result) // 10
println(result2) // 0
9. Either Monad
The Either monad is used to represent a value of one of two possible types (a disjoint union). It is often used for error handling.
def divide(x: Int, y: Int): Either[String, Int] = {
if (y == 0) Left("Division by zero")
else Right(x / y)
}
val result = divide(10, 2) match {
case Right(value) => s"Result: $value"
case Left(error) => s"Error: $error"
}
val result2 = divide(10, 0) match {
case Right(value) => s"Result: $value"
case Left(error) => s"Error: $error"
}
// Usage
println(result) // Result: 5
println(result2) // Error: Division by zero
10. Lazy Evaluation
Lazy evaluation is a technique where expressions are not evaluated until their results are needed.
lazy val expensiveComputation: Int = {
println("Computing...")
42
}
// Usage
println("Before accessing lazy value")
println(expensiveComputation) // "Computing..." followed by 42
println(expensiveComputation) // 42 (without recomputing)
11. Type Classes
Type classes enable ad-hoc polymorphism in a functional way.
trait Show[A] {
def show(a: A): String
}
object ShowInstances {
implicit val intShow: Show[Int] = (a: Int) => a.toString
implicit val stringShow: Show[String] = (a: String) => a
}
object Show {
def apply[A](implicit instance: Show[A]): Show[A] = instance
def show[A: Show](a: A): String = Show[A].show(a)
}
// Usage
import ShowInstances._
println(Show.show(123)) // "123"
println(Show.show("Hello")) // "Hello"