Skip to main content
Scala: Functional Programming
  1. Posts/

Scala: Functional Programming

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

This week I am taking a look at Functional Programming in Scala Essentials | Rock the JVM.

Functional Programming in Scala
#

What’s a Function
#

  • First-class functions: Functions can be assigned to variables, passed as arguments, and returned from other functions.
  • Function types: Scala uses FunctionN traits (e.g., Function1, Function2) and concise syntax like (A, B) => C.
  • Custom function traits: You can define your own function-like traits with apply.
  • Function values vs methods: Function values are objects; methods are class members.
  • Currying: Functions can return other functions (curried form).
  • All function values: Are instances of FunctionN traits with apply methods.
// FP: functions are "first-class" citizens, i.e., work with functions like any other values  
// JVM was built for Java, an OO language: objects (instances of classes) are "first-class" citizens  
  
// Scala's trick solution: traits with apply methods  
trait MyFunction[A, B] {  
  def apply(arg: A): B  
}  
  
val doubler = new MyFunction[Int, Int] { // : MyFunction[Int, Int]  
  override def apply(arg: Int) = arg * 2  
}  
  
val meaningOfLife = 42  
val meaningDoubled = doubler(meaningOfLife) // doubler.apply(meaningOfLife)  
  
// built-in function types  
// Function1[ArgType, ResultType]  
val doublerStandard = new Function1[Int, Int] {  
  override def apply(arg: Int) = arg * 2  
}  
val meaningDoubled_v2 = doublerStandard(meaningOfLife)  
  
// Function2[Arg1Type, Arg2Type, ResultType]  
val adder = new Function2[Int, Int, Int] {  
  override def apply(a: Int, b: Int) = a + b  
}  
val anAddition = adder(2, 67)  
  
// larger example  
// (Int, String, Double, Boolean) => Int, aka Function4[Int, String, Double, Boolean, Int]  
val athreeArgFunction = new ((Int, String, Double, Boolean) => Int) {  
  override def apply(v1: Int, v2: String, v3: Double, v4: Boolean): Int = ???  
}  
  
// all function values in Scala are instances of FunctionN traits with apply methods  
  
/**  
 * Exercises 
 * 1. A function which takes 2 strings and concatenates them 
 * 2. Define a function which takes an int as argument and returns ANOTHER FUNCTION as a result. 
 */
   
// 1  
// if the value is given a type, we can use single abstraction function syntax, // by removing 'new Function2[String, String, String]'  
val concatenator: (String, String) => String = (a: String, b: String) => a + b  

// 2
val superAdder = new Function1[Int, Function1[Int, Int]] {  
  override def apply(x: Int) = new Function1[Int, Int] {  
    override def apply(y: Int) = x + y  
  }  
}  
  
val adder2 = superAdder(2)  
val anAddition_v2 = adder2(67) // 69  
// currying  
val anAddiotion_v3 = superAdder(2)(67)  
  
// function values = instances of FunctionN  
// methods = invokable members of classes (concept from OOP)  
// function values != methods

Anonymous Functions
#

  • Anonymous functions (lambdas): Concise syntax for function values, e.g., (x: Int) => x * 2 or _ * 2.
  • Type inference: Parameter types can often be omitted.
  • Zero-argument functions: Use () => value.
  • Block syntax: { (x: Int) => ... } is also valid.
// instances of FunctionN  
val doubler: Int => Int = new Function1[Int, Int] {  
  override def apply(x: Int) = x * 2  
}  
  
// lambdas = anonymous function instances  
val doubler_v2: Int => Int = (x: Int) => x * 2 // identical  
val adder: (Int, Int) => Int = (x: Int, y: Int) => x + y // new Function2[Int, Int, Int] { override def apply... }  
  
  
// zero-arg functions  
val justDoSomething: () => Int = () => 45  
val anInvocation = justDoSomething()  
  
// alt syntax with curly braces  
val stringToInt = { (str: String) =>  
  // implementation: code block  
  str.toInt  
}  
  
// type inference  
val doubler_v3: Int => Int = x => x * 2 // type inferred by compiler  
val adder_v2: (Int, Int) => Int = (x, y) => x + y  
  
// shortest lambdas  
val doubler_v4: Int => Int = _ * 2 // x => x * 2  
val adder_v3: (Int, Int) => Int = _ + _ // (x, y) => x + y  
// each underscore is a different argument, you can't reuse them  
  
/**  
 * Exercises 
 * 1. Rewrite the "special" adder from WhatsAFunction using lambdas. 
 */
 
 val superAdder = new Function1[Int, Function1[Int, Int]] {  
  override def apply(x: Int) = new Function1[Int, Int] {  
    override def apply(y: Int) = x + y  
  }  
}  
  
val superAdder_v2 = (x: Int) => (y: Int) => x + y  
val adding2 = superAdder_v2(2) // (y: Int) => 2 + y  
val addingInvocation = adding2(43) // 45  
val addingInvocation_v2 = superAdder_v2(2)(43) // same

Higher-Order Functions and Currying
#

  • Higher-order functions (HOFs): Take functions as arguments or return functions as results.
  • Examples: map, flatMap, filter, and custom HOFs.
  • Currying: Transform a multi-argument function into a chain of single-argument functions.
  • Curried methods: Methods with multiple argument lists.
  • Function composition: Use compose and andThen to chain functions.
// higher order functions (HOFs)  
// functions which take other functions as arguments or return functions as results  
val aHof: (Int, (Int => Int)) => Int = (x, func) => x + 1  
// Takes an Int and a function from Int to Int, returns an Int  
val anotherHof: Int => (Int => Int) = x => (y => y + 2 * x)  
// Takes an Int, returns a function from Int to Int  
  
// quick exercise  
val superfunction: (Int, (String, (Int => Boolean)) => Int) => (Int => Int) = (x, func) => (y => x + y)  
// Takes an Int and a function from (String, Int => Boolean) to Int, returns a function from Int to Int  
  
// examples: map, flatMap, filter  
  
// more examples  
// f(f(f(...(f(x)))  
@tailrec  
def nTimes(f: Int => Int, n: Int, x: Int): Int =  
  if (n <= 0) x  
  else nTimes(f, n-1, f(x))  
  
val plusOne = (x: Int) => x + 1  
val tenThousand = nTimes(plusOne, 10000, 0) // 10000  
  
/*  
  ntv2(po, 3) =  (x: Int) => ntv2(po, 2)(po(x)) = po(po(po(x)))  
  ntv2(po, 2) =  (x: Int) => ntv2(po, 1)(po(x)) = po(po(x))  
  ntv2(po, 1) =>  (x: Int) => ntv2(po, 0)(po(x)) = po(x)  
  ntv2(po, 0) = (x: Int) => x 
  */
def nTimes_v2(f: Int => Int, n: Int): Int => Int =  
  if (n <= 0) (x: Int) => x  
  // base case: return a function that returns its argument  
  else (x: Int) => nTimes_v2(f, n-1)(f(x))  
// recursive case: return a function that applies f to x and calls itself with n-1  
  
val plusOneHundred = nTimes_v2(plusOne, 100) // po(po(po(po... risks SO if the arg is too big  
// That's because the function is not tail recursive, it builds up a stack of calls  
val oneHundred = plusOneHundred(0) // 100  
  
// Can we use tail recursion here?  
  
// currying = HOFs returning function instances  
val superAdder: Int => Int => Int = (x: Int) => (y: Int) => x + y  
val add3: Int => Int = superAdder(3)  
val invokeSuperAdder = superAdder(3)(100) // 103  
  
// curried methods = methods with a multiple arg list  
def curriedFormatter(fmt: String)(x: Double): String = fmt.format(x)  
  
val standardFormat: (Double => String) = curriedFormatter("%4.2f") 
// (x: Double) => "%4.2f".format(x)  
val preciseFormat: (Double => String) = curriedFormatter("%10.8f") 
// (x: Double) => "%10.8f".format(x)  
  
/**  
 *  1. toCurry(f: (Int, Int) => Int): Int => Int => Int 
 *     fromCurry(f: (Int => Int => Int)): (Int, Int) => Int 
 * 
 *  2. compose(f,g) => x => f(g(x)) 
 *     andThen(f,g) => x => g(f(x)) 
 */
 * 
// 1
def toCurry[A, B, C](f: (A, B) => C): A => B => C =  
  x => y => f(x, y)  
  
val superAdder_v2 = toCurry[Int, Int, Int](_ + _) // same as superAdder  
  
def fromCurry[A, B, C](f: A => B => C): (A, B) => C =  
  (x, y) => f(x)(y)  
  
val simpleAdder = fromCurry(superAdder)  
  
// 2
def compose[A, B, C](f: B => C, g: A => B): A => C =  
  x => f(g(x))  
  
def andThen[A, B, C](f: A => B, g: B => C): A => C =  
  x => g(f(x))  
  
val incrementer = (x: Int) => x + 1  
val doubler = (x: Int) => 2 * x  
val composedApplication = compose(incrementer, doubler)  
val aSequencedApplication = andThen(incrementer, doubler)

Map, FlatMap, Filter and For Comprehension
#

  • map: Transforms each element of a collection using a function.
  • filter: Selects elements that satisfy a predicate.
  • flatMap: Maps each element to a collection and flattens the result.
  • for-comprehension: Syntactic sugar for chains of map, flatMap, and filter.
  • foreach: For side effects (e.g., printing), not for producing new collections.
  // standard list
  val aList = List(1,2,3)
  // [1] -> [2] -> [3] -> Nil // [1,2,3]
  val firstElement = aList.head
  // 1
  val restOfElements = aList.tail
  // [2,3]

  // map
  val anIncrementedList = aList.map(_ + 1)
  // [2,3,4]

  // filter
  val onlyOddNumbers = aList.filter(_ % 2 != 0)
  // [1,3]

  // flatMap
  val toPair = (x: Int) => List(x, x + 1)
  // (1) => [1,2], (2) => [2,3], (3) => [3,4]
  val aFlatMappedList = aList.flatMap(toPair)
  // [1,2,2,3,3,4]

  // All the possible combinations of all the elements of those lists, in the format "1a - black"
  // Filter the numbers to only even ones
  val numbers = List(1, 2, 3, 4)
  val chars = List('a', 'b', 'c', 'd')
  val colors = List("black", "white", "red")

  /*
    lambda = num => chars.map(char => s"$num$char")
    [1,2,3,4].flatMap(lambda) = ["1a", "1b", "1c", "1d", "2a", "2b", "2c", "2d", ...]
    lambda(1) = chars.map(char => s"1$char") = ["1a", "1b", "1c", "1d"]
    lambda(2) = .. = ["2a", "2b", "2c", "2d"]
    lambda(3) = ..
    lambda(4) = ..
   */
  val combinations = numbers
    .withFilter(_ % 2 == 0)
    .flatMap(number =>
      chars.flatMap(char =>
        colors.map(color =>
          s"$number$char - $color"
        )
      )
    )

  // for-comprehension = IDENTICAL to flatMap + map chains
  val combinationsFor = for {
    number <- numbers if number % 2 == 0 // generator
    char <- chars
    color <- colors
  } yield s"$number$char - $color" // an EXPRESSION

  // for-comprehensions with Unit
  // if foreach
  for {
    num <- numbers
  } println(num) // prints each number in the list

Linear Collections: Seq, List, Vector, Array, Set, Range
#

  • Seq: Ordered collection with indexing; supports map, flatMap, filter, and many utility methods.
  • List: Immutable linked list; fast prepend, slower random access.
  • Vector: Immutable, fast random access and updates; good for large data.
  • Array: Mutable, fixed-size, interoperates with Java arrays; not a Seq but can be converted.
  • Set: Unordered collection of unique elements; supports union, intersection, difference.
  • Range: Represents a sequence of evenly spaced integers; useful for iteration.
  • Benchmarking: Vectors are much faster than lists for random updates in large collections.

  // Seq = well-defined ordering + indexing
  def testSeq(): Unit = {
    val aSequence = Seq(4,2,3,1)
    // main API: index an element
    val thirdElement = aSequence.apply(2) // element 3
    // map/flatMap/filter/for
    val anIncrementedSequence = aSequence.map(_ + 1) // [5,3,4,2]
    val aFlatMappedSequence = aSequence.flatMap(x => Seq(x, x + 1)) // [4,5,2,3,3,4,1,2]
    val aFilteredSequence = aSequence.filter(_ % 2 == 0) // [4,2]
    // other methods
    val reversed = aSequence.reverse
    val concatenation = aSequence ++ Seq(5,6,7)
    val sortedSequence = aSequence.sorted // [1,2,3,4]
    val sum = aSequence.foldLeft(0)(_ + _) // 10 - equivalent to a sum
    val stringRep = aSequence.mkString("[", ",", "]")

    println(aSequence)
    println(concatenation)
    println(sortedSequence)
  }

  // lists - particular Seq implementation
  def testLists(): Unit = {
    val aList = List(1,2,3)
    // same API as Seq
    val firstElement = aList.head
    val rest = aList.tail
    // appending and prepending
    val aBiggerList = 0 +: aList :+ 4
    val prepending = 0 :: aList // :: is a case class that prepends an element to a list
    // utility methods
    val scalax5 = List.fill(5)("Scala")
  }

  // ranges
  def testRanges(): Unit = {
    val aRange = 1 to 10 // 1 to 10 inclusive
    val aNonInclusiveRange = 1 until 10 // 10 not included
    // same Seq API
    (1 to 10).foreach(_ => println("Scala"))
  }

  // arrays = mutable
  def testArrays(): Unit = {
    val anArray = Array(1,2,3,4,5,6) // int[] on the JVM
    // most Seq APIs
    // arrays are not Seqs
    val aSequence: Seq[Int] = anArray.toIndexedSeq
    // arrays are mutable
    anArray.update(2, 30) // no new array is allocated
  }

  // vectors = fast Seqs for a large amount of data
  def testVectors(): Unit = {
    val aVector: Vector[Int] = Vector(1,2,3,4,5,6)
    // the same Seq API
  }

  def smallBenchmark(): Unit = {
    val maxRuns = 1000
    val maxCapacity = 1000000

    def getWriteTime(collection: Seq[Int]): Double = {
      val random = new Random()
      val times = for {
        i <- 1 to maxRuns
      } yield {
        val index = random.nextInt(maxCapacity)
        val element = random.nextInt()

        val currentTime = System.nanoTime()
        val updatedCollection = collection.updated(index, element)
        System.nanoTime() - currentTime
      }

      // compute average
      times.sum * 1.0 / maxRuns
    }

    val numbersList = (1 to maxCapacity).toList
    val numbersVector = (1 to maxCapacity).toVector

    println(getWriteTime(numbersList)) // Average: 5 ms
    println(getWriteTime(numbersVector)) // Average: 0.002 ms
  }

  // sets
  def testSets(): Unit = {
    val aSet = Set(1,2,3,4,5,4) // no ordering guaranteed
    // equals + hashCode = hash set
    // main API: test if in the set
    val contains3 = aSet.contains(3) // true
    val contains3_v2 = aSet(3) // same: true
    // adding/removing
    val aBiggerSet = aSet + 4 // [1,2,3,4,5]
    val aSmallerSet = aSet - 4 // [1,2,3,5]
    // concatenation == union
    val anotherSet = Set(4,5,6,7,8)
    val muchBiggerSet = aSet.union(anotherSet)
    val muchBiggerSet_v2 = aSet ++ anotherSet // same
    val muchBiggerSet_v3 = aSet | anotherSet // same
    // difference
    val aDiffSet = aSet.diff(anotherSet)
    val aDiffSet_v2 = aSet -- anotherSet // same
    // intersection
    val anIntersection = aSet.intersect(anotherSet) // [4,5]
    val anIntersection_v2 = aSet & anotherSet // same
  }

Tuples and Maps
#

  • Tuples: Finite ordered groups of values under the same value; accessed via ._1, ._2, etc. (1-indexed).
  • Arrow syntax: a -> b is shorthand for creating tuples of two elements (a, b).
  • Maps: Collections of key-value pairs; support lookups, additions, removals, and transformations.
  • Map operations: Add with +, remove with -, check existence with contains, provide defaults with withDefaultValue.
  • Conversions: Lists of tuples can be converted to maps with toMap; maps can be converted to sequences with toList, etc.
  • Transformations: Maps support map, flatMap, and filter; with convenience methods like filterKeys and mapValues.
// Tuples: finite ordered collections of heterogeneous elements under a single value
// Type is Tuple[A,B,...] where A,B are the element types
val aTuple = (2, "rock the jvm") 
// Creates a Tuple2[Int, String] == (Int, String)  
val firstField = aTuple._1 
// Access elements via ._N notation - returns 2  
// Important: Tuple indexing starts at 1, not 0 (unlike most collections)  
val aCopiedTuple = aTuple.copy(_1 = 54) 
// Creates a new tuple with modified first element: (54, "rock the jvm")
  
// Special syntax for tuples of 2 elements (pairs)
val aTuple_v2 = 2 -> "rock the jvm" 
// IDENTICAL to (2, "rock the jvm") - syntactic sugar for creating pairs
  
// Maps: immutable collections of key -> value associations
val aMap = Map() // Creates an empty map
  
val phonebook: Map[String, Int] = Map(  
  "Jim" -> 555,     // Each key -> value is actually a tuple under the hood
  "Daniel" -> 789,  
  "Jane" -> 123  
).withDefaultValue(-1) 
// Sets default value (-1) for non-existent keys instead of throwing exceptions
  
// Core Map APIs: lookup and existence checks
val phonebookHasDaniel = phonebook.contains("Daniel") 
// Returns true - safer than direct lookup
val marysPhoneNumber = phonebook("Mary") 
// Would normally throw NoSuchElementException, but returns -1 thanks to withDefaultValue
  
// Adding elements to maps (immutable - creates new map)
val newPair = "Mary" -> 678  
// Create a key-value pair
val newPhonebook = phonebook + newPair 
// Returns a new map with the additional entry
  
// Removing elements (immutable - creates new map) 
val phoneBookWithoutDaniel = phonebook - "Daniel" 
// Returns a new map without the "Daniel" entry
  
// Converting collections of pairs to maps
val linearPhonebook = List(  
  "Jim" -> 555,  
  "Daniel" -> 789,  
  "Jane" -> 123  
)  
val phonebook_v2 = linearPhonebook.toMap 
// Any collection of (K,V) pairs can be converted to a Map[K,V]
  
// Converting maps to other collection types  
val linearPhonebook_v2 = phonebook.toList 
// Returns List[(String, Int)] containing the key-value pairs
// Other conversions: toSeq, toVector, toArray, toSet - all return collections of key-value tuples
  
// map, flatMap, filter  
// Map("Jim" -> 123, "jiM" -> 999) => Map("JIM" -> ????)  
val aProcessedPhonebook = phonebook  
  .map(pair =>  
    (pair._1.toUpperCase(), pair._2)  // Creates new pairs with transformed keys
  ) // Returns Map("JIM" -> 555, "DANIEL" -> 789, "JANE" -> 123)
  
// Filtering maps by keys efficiently (using view for lazy evaluation)
val noJs = phonebook  
  .view 
  .filterKeys(!_.startsWith("J")) // Keeps only entries whose keys don't start with "J" 
  .toMap  // Materializes the view back to a concrete Map
  
// Transforming only the values in a map (keys remain unchanged)
val prefixNumbers = phonebook  
  .view  
  .mapValues(number => s"0255-$number") // Adds prefix to each number
  .toMap  
  // Returns Map("Jim" -> "0255-555", "Daniel" -> "0255-789", "Jane" -> "0255-123")
  
// Creating maps from other collections via grouping
val names = List("Bob", "James", "Angela", "Mary", "Daniel", "Jim")
val nameGroupings = names.groupBy(name => name.charAt(0)) 
// Groups names by first letter
// Returns Map('B' -> List("Bob"), 'J' -> List("James", "Jim"), 'A' -> List("Angela"), etc.)

Handling Absence: Option
#

  • Option type: A container that holds either Some(value) or None; eliminates null checks.
  • Creating options: Use Option(value), Some(value), or None.
  • Safe extraction: Use getOrElse, orElse, or pattern matching to safely extract values.
  • Composition: Options support map, flatMap, and filter for functional composition.
  • Error handling: Better than null checks for dealing with potentially missing values.
  • For-comprehensions: Options work naturally in for-comprehensions for cleaner chaining.
// Options: "collections" with at most one value (Some or None)  
val anOption: Option[Int] = Option(42)  
// Option will wrap the value if non-null
val anEmptyOption: Option[Int] = Option.empty  
// Creates a None
  
// Alternative constructors 
val aPresentValue: Option[Int] = Some(4) 
val anEmptyOption_v2: Option[Int] = None
  
// Core API methods
val isEmpty = anOption.isEmpty  
// Returns false as the option contains a value
val innerValue = anOption.getOrElse(90)  
// Safely extracts 42, or returns 90 if empty
val anotherOption = Option(46)  
// Another option with value 46
val aChainedOption = anEmptyOption.orElse(anotherOption)  
// Falls back to anotherOption if first is empty
// Above is equivalent to:  
anOption match {  
  case Some(x) => Some(2)  
  case None    => anotherOption  
}  
  
// Functional operations on Options
val anIncrementedOption = anOption.map(_ + 1) 
// Transforms Some(42) to Some(43)  
val aFilteredOption = anIncrementedOption.filter(_ % 2 == 0) 
// Becomes None as 43 is odd  
val aFlatMappedOption = anOption.flatMap(value => Option(value * 10)) 
// Some(420)  
  
// WHY options: dealing with potentially unsafe API calls
def unsafeMethod(): String = null  // Method that might return null
def fallbackMethod(): String = "some valid result"  
  
// Traditional defensive programming style (error-prone)
val stringLength = {  
  val potentialString = unsafeMethod()  
  if (potentialString == null) -1  
  else potentialString.length  
}  
  
// Option-style: elegant null handling
val stringLengthOption = Option(unsafeMethod()).map(_.length) 
// Becomes None if null  
  
// Chaining fallbacks with orElse
val someResult = Option(unsafeMethod()).orElse(Option(fallbackMethod()))  
  
// Better API design with Options in signatures
def betterUnsafeMethod(): Option[String] = None  
// Explicitly communicates possible absence
def betterFallbackMethod(): Option[String] = Some("A valid result")  
val betterChain = betterUnsafeMethod().orElse(betterFallbackMethod())  
  
// Real-world example: Map.get returns Option instead of throwing
val phoneBook = Map(  
  "Daniel" -> 1234  
)  
val marysPhoneNumber = phoneBook.get("Mary") 
// Returns None instead of throwing exception
// No need to crash, check for nulls, or verify if Mary exists in the map
/**  
 * Exercise: 
 *  Get the host and port from the config map, 
 *    try to open a connection, 
 *    print "Conn successful" 
 *    or "Conn failed" 
 */  
 
val config: Map[String, String] = Map(  
  // Configuration that might come from external source  
  "host" -> "176.45.32.1",  
  "port" -> "8081"  
)  
  
// Connection class that represents a successful connection
class Connection {  
  def connect(): String = "Connection successful"  
}  
  
// Connection factory with Option return type to handle potential failure
object Connection {  
  val random = new Random()  
  
  // Returns Some(Connection) or None based on success/failure
  def apply(host: String, port: String): Option[Connection] =  
    if (random.nextBoolean()) Some(new Connection)  
    else None  
}  
  
// Traditional defensive style (in an imperative language like Java)
/*  
  String host = config("host")  
  String port = config("port")  
  if (host != null)    
    if (port != null)      
      Connection conn = Connection.apply(host, port)      
      if (conn != null)        return conn.connect()      
      // ... that's just the happy path, we need to add the rest of the branches 
*/

// Solution 1: Step-by-step with Options
val host = config.get("host")  // Option[String]
val port = config.get("port")  // Option[String]
val connection = host.flatMap(h => port.flatMap(p => Connection(h, p)))  
// Option[Connection]
val connStatus = connection.map(_.connect())  // Option[String]
  
// Solution 2: Compact chained approach
val connStatus_v2 =  
  config.get("host").flatMap(h =>  
    config.get("port").flatMap(p =>  
      Connection(h, p).map(_.connect())  
    )  
  )  
  
// Solution 3: Most readable using for-comprehension
val connStatus_v3 = for {  
  h <- config.get("host")     // Extract host or short-circuit
  p <- config.get("port")     // Extract port or short-circuit
  conn <- Connection(h, p)    // Create connection or short-circuit
} yield conn.connect()        // Return connection status if successful
  
// Display result with a fallback message if connection failed
def main(args: Array[String]): Unit = {  
  println(connStatus.getOrElse("Failed to establish connection"))  
}

Handling Failure: Try
#

  • Exception handling: Wraps computations that might throw exceptions in a success/failure container.
  • Type safety: Encapsulates exceptions as values rather than special control flow.
  • Constructors: Create with Try(computation), Success(value), or Failure(exception).
  • Core API: Check status with isSuccess/isFailure, chain with orElse, extract with getOrElse.
  • Functional composition: Supports map, flatMap, and filter for error-resilient transformations.
  • Pattern matching: Can be deconstructed with match to handle success and failure cases.
// Try: a container for computations that might succeed or fail with an exception
val aTry: Try[Int] = Try(42)  
// Success - wraps successful computation
val aFailedTry: Try[Int] = Try(throw new RuntimeException)  
// Failure - captures exception
  
// Alternative direct constructors 
val aTry_v2: Try[Int] = Success(42)  
// Explicitly create Success
val aFailedTry_v2: Try[Int] = Failure(new RuntimeException)  
// Explicitly create Failure
  
// Core API methods
val checkSuccess = aTry.isSuccess
val checkFailure = aTry.isFailure
val aChain = aFailedTry.orElse(aTry)  
// Fallback mechanism - returns aTry since aFailedTry failed
  
// Functional operations on Try values
val anIncrementedTry = aTry.map(_ + 1)  
// Success(43) - map transforms the value inside
val aFlatMappedTry = aTry.flatMap(mol => Try(s"My meaning of life is $mol"))  
// Success("My meaning of life is 42")
val aFilteredTry = aTry.filter(_ % 2 == 0) 
// Success(42) - 42 is even so passes the filter
  
// WHY Try: safely handle exceptions from unsafe APIs  
def unsafeMethod(): String =  
  throw new RuntimeException("No string for you, buster!")  
  // This method will always throw
  
// Traditional defensive style with try-catch blocks
val stringLengthDefensive = try {  
  val aString = unsafeMethod()  
  aString.length  // This will never execute due to exception
} catch {  
  case e: RuntimeException => -1  
  // Return fallback value on error
}  
  
// Purely functional style with Try
val stringLengthPure = Try(unsafeMethod())  // Captures exception in Failure
  .map(_.length)       // Would transform the value if it was a Success
  .getOrElse(-1)       // Extract value or use default (-1) if Failure
  
// Better API design with Try in signatures
def betterUnsafeMethod(): Try[String] = Failure(new RuntimeException("No string for you, buster!"))  
def betterBackupMethod(): Try[String] = Success("Scala")  
val stringLengthPure_v2 = betterUnsafeMethod().map(_.length)  
// Failure preserved through map
val aSafeChain = betterUnsafeMethod()
  .orElse(betterBackupMethod())  // Falls back to Success("Scala")
  .map(_.length)                 // Transforms to Success(5)
  
/**  
 * Exercise: 
 *   Obtain a connection, 
 *   then fetch the URL, 
 *   then print the resulting HTML
 *   Handle all potential failures gracefully
 */

val host = "localhost"  
val port = "8081"  
val myDesiredURL = "rockthejvm.com/home"  
  
// Connection class with methods that might fail
class Connection {  
  val random = new Random()  
  
  // Unsafe method - might throw exception
  def get(url: String): String = {  
    if (random.nextBoolean()) "<html>Success</html>"  
    else throw new RuntimeException("Cannot fetch page right now.")  
  }  
  
  // Safe wrapper using Try
  def getSafe(url: String): Try[String] =  
    Try(get(url))  // Captures potential exception in a Try
}  
  
// Service with methods that might fail
object HttpService {  
  val random = new Random()  
  
  // Unsafe method - might throw exception
  def getConnection(host: String, port: String): Connection =  
    if (random.nextBoolean()) new Connection  
    else throw new RuntimeException("Cannot access host/port combination.")  
  
  // Safe wrapper using Try
  def getConnectionSafe(host: String, port: String): Try[Connection] =  
    Try(getConnection(host, port))  // Captures potential exception in a Try
}  
  
// Solution 1: Traditional defensive style with nested try-catch (error-prone)
val finalHtml = try {  
  val conn = HttpService.getConnection(host, port)  // Might throw
  val html = try {  
    conn.get(myDesiredURL)  // Might throw
  } catch {  
    case e: RuntimeException => s"<html>${e.getMessage}</html>"  
    // Error handling for URL fetch
  }  
  html  // Return value if both operations succeed
} catch {  
  case e: RuntimeException => s"<html>${e.getMessage}</html>"  
  // Error handling for connection
}  
  
// Solution 2: Purely functional approach with explicit Try handling
val maybeConn = Try(HttpService.getConnection(host, port))  
// Wrap connection in Try
val maybeHtml = maybeConn.flatMap(conn => Try(conn.get(myDesiredURL)))  
// Chain with URL fetch
val finalResult = maybeHtml.fold(
  e => s"<html>${e.getMessage}</html>",  // Transform exception to HTML error message
  html => html                           // Keep successful HTML unchanged
)  
  
// Solution 3: Most readable using for-comprehension with safe methods
val maybeHtml_v2 = for {  
  conn <- HttpService.getConnectionSafe(host, port)  
  // Get connection or short-circuit
  html <- conn.getSafe(myDesiredURL)                 
  // Get HTML or short-circuit
} yield html  
// Returns Success with HTML or Failure with exception
val finalResult_v2 = maybeHtml.fold(
  e => s"<html>${e.getMessage}</html>",  // Handle failure
  html => html                           // Handle success
)  
  
def main(args: Array[String]): Unit = {  
  println(finalResult) 
}