Big Data, SCALA

Advanced Functional Programming in Scala

This entry is part 7 of 9 in the series Scala Series

Advanced Functional Programming in Scala: Pattern Matching, Case Classes, and Options
Originally posted October 17, 2018 by Kinshuk Dutta


In this blog, we’ll dive into advanced functional programming principles in Scala, particularly focusing on pattern matching, case classes, and functional error handling using Option and Try. To demonstrate these concepts, we’ll walk through a sample project: E-commerce Order Processing System, which showcases the use of these techniques in a real-world scenario.


Table of Contents

  1. Pattern Matching in Scala
  2. Case Classes and Their Role
  3. Error Handling with Option and Try
  4. Sample Project: E-commerce Order Processing System
  5. Conclusion and Next Steps

Pattern Matching in Scala

Pattern matching in Scala allows you to match data structures against defined patterns, making code more expressive and manageable. For example, if you’re dealing with different types of events in an e-commerce system, you can match each event type and perform a specific action.

scala
def handleOrderStatus(status: OrderStatus): String = status match {
case Pending(item, quantity) => s"Pending order of $quantity $item(s)"
case Shipped(trackingId) => s"Order shipped with tracking ID: $trackingId"
case Delivered(date) => s"Order delivered on $date"
case Cancelled(reason) => s"Order cancelled due to $reason"
}

Case Classes and Their Role

Case classes in Scala provide a concise way to define immutable data structures. They automatically implement methods like equals, hashCode, and toString, which makes them ideal for representing data with minimal boilerplate.

In our example, each order status—Pending, Shipped, Delivered, and Cancelled—is represented by a case class. This simplifies data handling and makes the code more readable and maintainable.

scala
sealed trait OrderStatus
case class Pending(item: String, quantity: Int) extends OrderStatus
case class Shipped(trackingId: String) extends OrderStatus
case class Delivered(date: String) extends OrderStatus
case class Cancelled(reason: String) extends OrderStatus

Error Handling with Option and Try

Scala’s Option and Try types are functional tools for handling errors and potentially missing values without relying on nulls or exceptions.

  • Option represents a value that might be present (Some) or absent (None).
  • Try represents an operation that might succeed (Success) or fail (Failure), making it useful for handling exceptions functionally.

For instance, in our e-commerce project, Try can help manage order updates where transitions might fail if invalid data is provided.

scala
import scala.util.{Try, Success, Failure}

def safeUpdateOrder(order: Order, newStatus: OrderStatus): Try[Order] = Try {
require(order.status != newStatus, "Order is already in this status.")
order.copy(status = newStatus)
}

Sample Project: E-commerce Order Processing System

Our E-commerce Order Processing System will leverage pattern matching, case classes, and functional error handling to create and update orders. This project simulates an order processing pipeline for an e-commerce platform, with modules for order creation, status updates, and error handling.

Project Structure

plaintext
ecommerce-order-processing

├── src
│ ├── main
│ │ ├── scala
│ │ │ ├── ecommerce
│ │ │ │ ├── models
│ │ │ │ │ ├── OrderStatus.scala
│ │ │ │ │ ├── Order.scala
│ │ │ │ ├── services
│ │ │ │ │ ├── OrderService.scala
│ │ │ │ │ ├── OrderErrorHandler.scala
│ │ │ │ ├── Main.scala

├── test
│ ├── scala
│ │ ├── ecommerce
│ │ │ ├── OrderServiceTest.scala
│ │ │ ├── OrderErrorHandlerTest.scala
└── build.sbt

Implementation Guide

Step 1: Define Models with Case Classes

Define each order status as a case class within the models directory.

File: OrderStatus.scala

scala
package ecommerce.models

sealed trait OrderStatus
case class Pending(item: String, quantity: Int) extends OrderStatus
case class Shipped(trackingId: String) extends OrderStatus
case class Delivered(date: String) extends OrderStatus
case class Cancelled(reason: String) extends OrderStatus

File: Order.scala

scala
package ecommerce.models

case class Order(id: String, status: OrderStatus)

Step 2: Create Services Using Pattern Matching

In the services directory, implement order status updates and descriptions.

File: OrderService.scala

scala
package ecommerce.services

import ecommerce.models._

object OrderService {

def updateOrderStatus(order: Order, newStatus: OrderStatus): Order = {
newStatus match {
case Pending(_, _) => order.copy(status = newStatus)
case Shipped(_) => order.copy(status = newStatus)
case Delivered(_) => order.copy(status = newStatus)
case Cancelled(_) => order.copy(status = newStatus)
}
}

def getOrderStatusDescription(order: Order): String = {
order.status match {
case Pending(item, quantity) => s"Pending order of $quantity $item(s)"
case Shipped(trackingId) => s"Order shipped with tracking ID: $trackingId"
case Delivered(date) => s"Order delivered on $date"
case Cancelled(reason) => s"Order cancelled due to $reason"
}
}
}

Step 3: Add Error Handling for Invalid Status Transitions

Use Try and Option in OrderErrorHandler.scala to handle errors gracefully.

File: OrderErrorHandler.scala

scala
package ecommerce.services

import ecommerce.models._
import scala.util.{Try, Success, Failure}

object OrderErrorHandler {

def safeUpdateOrder(order: Order, newStatus: OrderStatus): Try[Order] = Try {
require(order.status != newStatus, "Order is already in this status.")
OrderService.updateOrderStatus(order, newStatus)
}

def validateStatus(status: Option[OrderStatus]): String = {
status match {
case Some(value) => s"Valid status: ${value}"
case None => "Invalid status provided."
}
}
}

Step 4: Main Application Logic

File: Main.scala

scala
package ecommerce

import ecommerce.models._
import ecommerce.services._

object Main extends App {
val initialOrder = Order("123", Pending("Laptop", 1))
println(OrderService.getOrderStatusDescription(initialOrder))

// Try updating the order
val shippedOrder = OrderErrorHandler.safeUpdateOrder(initialOrder, Shipped("XYZ123")) match {
case Success(order) => order
case Failure(exception) =>
println(s"Error updating order: ${exception.getMessage}")
initialOrder
}

println(OrderService.getOrderStatusDescription(shippedOrder))
}

Testing the Project

Step 1: Add ScalaTest Dependency

Add the following to your build.sbt file to enable testing with ScalaTest:

scala
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % Test
Step 2: Write Tests for OrderService and ErrorHandler

Create test files to validate the project’s core functions and error handling.

File: OrderServiceTest.scala

scala
package ecommerce

import org.scalatest.flatspec.AnyFlatSpec
import ecommerce.models._
import ecommerce.services.OrderService

class OrderServiceTest extends AnyFlatSpec {

"OrderService" should "update the order status" in {
val order = Order("001", Pending("Laptop", 1))
val updatedOrder = OrderService.updateOrderStatus(order, Shipped("ABC123"))
assert(updatedOrder.status.isInstanceOf[Shipped])
}

it should "return correct order status description" in {
val order = Order("001", Shipped("ABC123"))
val description = OrderService.getOrderStatusDescription(order)
assert(description == "Order shipped with tracking ID: ABC123")
}
}

Step 3: Run Tests

Use sbt test to run the tests.

bash
sbt test

Conclusion and Next Steps

This project demonstrated advanced Scala concepts in a practical e-commerce context, helping to clarify key functional programming principles. In the next blogs, we’ll dive into concurrency with Futures and Promises, type classes, and monads to further expand our functional programming toolkit in Scala.

Series Navigation<< Concurrency in ScalaFunctional Programming in Scala >>