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
FunctionNtraits (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
FunctionNtraits withapplymethods.
// 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 != methodsAnonymous Functions #
- Anonymous functions (lambdas): Concise syntax for function values, e.g.,
(x: Int) => x * 2or_ * 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) // sameHigher-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
composeandandThento 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, andfilter. - 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 listLinear 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 -> bis 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 withcontains, provide defaults withwithDefaultValue. - Conversions: Lists of tuples can be converted to maps with
toMap; maps can be converted to sequences withtoList, etc. - Transformations: Maps support
map,flatMap, andfilter; with convenience methods likefilterKeysandmapValues.
// 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)orNone; eliminates null checks. - Creating options: Use
Option(value),Some(value), orNone. - Safe extraction: Use
getOrElse,orElse, or pattern matching to safely extract values. - Composition: Options support
map,flatMap, andfilterfor 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), orFailure(exception). - Core API: Check status with
isSuccess/isFailure, chain withorElse, extract withgetOrElse. - Functional composition: Supports
map,flatMap, andfilterfor error-resilient transformations. - Pattern matching: Can be deconstructed with
matchto 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)
}