diff --git a/_overviews/scala3-book/fp-functional-error-handling.md b/_overviews/scala3-book/fp-functional-error-handling.md index 8fa2b92bd7..edc5f8a5ed 100644 --- a/_overviews/scala3-book/fp-functional-error-handling.md +++ b/_overviews/scala3-book/fp-functional-error-handling.md @@ -30,6 +30,21 @@ While this first example doesn’t deal with null values, it’s a good way to i Imagine that you want to write a method that makes it easy to convert strings to integer values, and you want an elegant way to handle the exception that’s thrown when your method gets a string like `"Hello"` instead of `"1"`. A first guess at such a method might look like this: + +{% tabs fp-java-try class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +def makeInt(s: String): Int = + try { + Integer.parseInt(s.trim) + } catch { + case e: Exception => 0 + } +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala def makeInt(s: String): Int = try @@ -37,6 +52,9 @@ def makeInt(s: String): Int = catch case e: Exception => 0 ``` +{% endtab %} + +{% endtabs %} If the conversion works, this method returns the correct `Int` value, but if it fails, the method returns `0`. This might be okay for some purposes, but it’s not really accurate. @@ -57,6 +75,21 @@ The `Some` and `None` classes are subclasses of `Option`, so the solution works Here’s the revised version of `makeInt`: + +{% tabs fp--try-option class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +def makeInt(s: String): Option[Int] = + try { + Some(Integer.parseInt(s.trim)) + } catch { + case e: Exception => None + } +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala def makeInt(s: String): Option[Int] = try @@ -64,16 +97,25 @@ def makeInt(s: String): Option[Int] = catch case e: Exception => None ``` +{% endtab %} + +{% endtabs %} This code can be read as, “When the given string converts to an integer, return the `Int` wrapped inside a `Some`, such as `Some(1)`. When the string can’t be converted to an integer, an exception is thrown and caught, and the method returns a `None` value.” These examples show how `makeInt` works: +{% tabs fp-try-option-example %} + +{% tab 'Scala 2 and 3' %} ```scala val a = makeInt("1") // Some(1) val b = makeInt("one") // None ``` +{% endtab %} + +{% endtabs %} As shown, the string `"1"` results in a `Some(1)`, and the string `"one"` results in a `None`. This is the essence of the `Option` approach to error handling. @@ -101,11 +143,26 @@ There are two common answers, depending on your needs: One possible solution is to use a `match` expression: +{% tabs fp-option-match class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +makeInt(x) match { + case Some(i) => println(i) + case None => println("That didn’t work.") +} +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala makeInt(x) match case Some(i) => println(i) case None => println("That didn’t work.") ``` +{% endtab %} + +{% endtabs %} In this example, if `x` can be converted to an `Int`, the expression on the right-hand side of the first `case` clause is evaluated; if `x` can’t be converted to an `Int`, the expression on the right-hand side of the second `case` clause is evaluated. @@ -117,6 +174,22 @@ Another common solution is to use a `for` expression---i.e., the `for`/`yield` c For instance, imagine that you want to convert three strings to integer values, and then add them together. This is how you do that with a `for` expression and `makeInt`: + +{% tabs fp-for-comprehension class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +val y = for { + a <- makeInt(stringA) + b <- makeInt(stringB) + c <- makeInt(stringC) +} yield { + a + b + c +} +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala val y = for a <- makeInt(stringA) @@ -125,6 +198,9 @@ val y = for yield a + b + c ``` +{% endtab %} + +{% endtabs %} After that expression runs, `y` will be one of two things: @@ -133,27 +209,55 @@ After that expression runs, `y` will be one of two things: You can test this for yourself: +{% tabs fp-for-comprehension-evaluation class=tabs-scala-version %} + +{% tab 'Scala 2' %} ```scala val stringA = "1" val stringB = "2" val stringC = "3" -val y = for +val y = for { + a <- makeInt(stringA) + b <- makeInt(stringB) + c <- makeInt(stringC) +} yield { + a + b + c +} +``` +{% endtab %} + +{% tab 'Scala 3' %} +```scala +val stringA = "1" +val stringB = "2" +val stringC = "3" + +val y = for a <- makeInt(stringA) b <- makeInt(stringB) c <- makeInt(stringC) yield a + b + c ``` +{% endtab %} + +{% endtabs %} With that sample data, the variable `y` will have the value `Some(6)`. To see the failure case, change any of those strings to something that won’t convert to an integer. When you do that, you’ll see that `y` is a `None`: +{% tabs fp-for-comprehension-failure-result %} + +{% tab 'Scala 2 and 3' %} ```scala y: Option[Int] = None ``` +{% endtab %} + +{% endtabs %} ## Thinking of Option as a container @@ -179,10 +283,16 @@ They have many of the methods you’d expect from a collection class, including This raises an interesting question: What will these two values print, if anything? +{% tabs fp-option-methods-evaluation %} + +{% tab 'Scala 2 and 3' %} ```scala makeInt("1").foreach(println) makeInt("x").foreach(println) ``` +{% endtab %} + +{% endtabs %} Answer: The first example prints the number `1`, and the second example doesn’t print anything. The first example prints `1` because: @@ -205,12 +315,33 @@ Somewhere in Scala’s history, someone noted that the first example (the `Some` *But* despite having two different possible outcomes, the great thing with `Option` is that there’s really just one path: The code you write to handle the `Some` and `None` possibilities is the same in both cases. The `foreach` examples look like this: +{% tabs fp-another-option-method-example %} + +{% tab 'Scala 2 and 3' %} ```scala makeInt(aString).foreach(println) ``` +{% endtab %} + +{% endtabs %} And the `for` expression looks like this: +{% tabs fp-another-for-comprehension-example class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +val y = for { + a <- makeInt(stringA) + b <- makeInt(stringB) + c <- makeInt(stringC) +} yield { + a + b + c +} +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala val y = for a <- makeInt(stringA) @@ -219,16 +350,34 @@ val y = for yield a + b + c ``` +{% endtab %} + +{% endtabs %} With exceptions you have to worry about handling branching logic, but because `makeInt` returns a value, you only have to write one piece of code to handle both the Happy and Unhappy Paths, and that simplifies your code. Indeed, the only time you have to think about whether the `Option` is a `Some` or a `None` is when you handle the result value, such as in a `match` expression: +{% tabs fp-option-match-handle class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +makeInt(x) match { + case Some(i) => println(i) + case None => println("That didn't work.") +} +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala makeInt(x) match case Some(i) => println(i) case None => println("That didn't work.") ``` +{% endtab %} + +{% endtabs %} > There are several other ways to handle `Option` values. > See the reference documentation for more details. @@ -240,6 +389,9 @@ makeInt(x) match Getting back to `null` values, a place where a `null` value can silently creep into your code is with a class like this: +{% tabs fp=case-class-nulls %} + +{% tab 'Scala 2 and 3' %} ```scala class Address( var street1: String, @@ -249,10 +401,29 @@ class Address( var zip: String ) ``` +{% endtab %} + +{% endtabs %} While every address on Earth has a `street1` value, the `street2` value is optional. As a result, the `street2` field can be assigned a `null` value: + +{% tabs fp-case-class-nulls-example class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +val santa = new Address( + "1 Main Street", + null, // <-- D’oh! A null value! + "North Pole", + "Alaska", + "99705" +) +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala val santa = Address( "1 Main Street", @@ -262,10 +433,17 @@ val santa = Address( "99705" ) ``` +{% endtab %} + +{% endtabs %} Historically, developers have used blank strings and null values in this situation, both of which are hacks to work around the root problem: `street2` is an *optional* field. In Scala---and other modern languages---the correct solution is to declare up front that `street2` is optional: + +{% tabs fp-case-class-with-options %} + +{% tab 'Scala 2 and 3' %} ```scala class Address( var street1: String, @@ -275,9 +453,27 @@ class Address( var zip: String ) ``` +{% endtab %} + +{% endtabs %} Now developers can write more accurate code like this: +{% tabs fp-case-class-with-options-example-none class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +val santa = new Address( + "1 Main Street", + None, // 'street2' has no value + "North Pole", + "Alaska", + "99705" +) +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala val santa = Address( "1 Main Street", @@ -287,9 +483,27 @@ val santa = Address( "99705" ) ``` +{% endtab %} + +{% endtabs %} or this: +{% tabs fp-case-class-with-options-example-some class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +val santa = new Address( + "123 Main Street", + Some("Apt. 2B"), + "Talkeetna", + "Alaska", + "99676" +) +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala val santa = Address( "123 Main Street", @@ -299,6 +513,9 @@ val santa = Address( "99676" ) ``` +{% endtab %} + +{% endtabs %} diff --git a/_overviews/scala3-book/fp-functions-are-values.md b/_overviews/scala3-book/fp-functions-are-values.md index 8560710c34..b4df001cce 100644 --- a/_overviews/scala3-book/fp-functions-are-values.md +++ b/_overviews/scala3-book/fp-functions-are-values.md @@ -14,12 +14,18 @@ While every programming language ever created probably lets you write pure funct This feature has many benefits, the most common of which are (a) you can define methods to accept function parameters, and (b) you can pass functions as parameters into methods. You’ve seen this in multiple places in this book, whenever methods like `map` and `filter` are demonstrated: +{% tabs fp-function-as-values-anonymous %} + +{% tab 'Scala 2 and 3' %} ```scala val nums = (1 to 10).toList val doubles = nums.map(_ * 2) // double each value val lessThanFive = nums.filter(_ < 5) // List(1,2,3,4) ``` +{% endtab %} + +{% endtabs %} In those examples, anonymous functions are passed into `map` and `filter`. @@ -27,6 +33,9 @@ In those examples, anonymous functions are passed into `map` and `filter`. In addition to passing anonymous functions into `filter` and `map`, you can also supply them with *methods*: +{% tabs fp-function-as-values-defined %} + +{% tab 'Scala 2 and 3' %} ```scala // two methods def double(i: Int): Int = i * 2 @@ -35,6 +44,9 @@ def underFive(i: Int): Boolean = i < 5 // pass those methods into filter and map val doubles = nums.filter(underFive).map(double) ``` +{% endtab %} + +{% endtabs %} This ability to treat methods and functions as values is a powerful feature that functional programming languages provide. @@ -47,29 +59,53 @@ This ability to treat methods and functions as values is a powerful feature that As you saw in those examples, this is an anonymous function: +{% tabs fp-anonymous-function-short %} + +{% tab 'Scala 2 and 3' %} ```scala _ * 2 ``` +{% endtab %} + +{% endtabs %} As shown in the [higher-order functions][hofs] discussion, that’s a shorthand version of this syntax: +{% tabs fp-anonymous-function-full %} + +{% tab 'Scala 2 and 3' %} ```scala (i: Int) => i * 2 ``` +{% endtab %} + +{% endtabs %} Functions like these are called “anonymous” because they don’t have names. If you want to give one a name, just assign it to a variable: +{% tabs fp-function-assignement %} + +{% tab 'Scala 2 and 3' %} ```scala val double = (i: Int) => i * 2 ``` +{% endtab %} + +{% endtabs %} Now you have a named function, one that’s assigned to a variable. You can use this function just like you use a method: +{% tabs fp-function-used-like-method %} + +{% tab 'Scala 2 and 3' %} ```scala double(2) // 4 ``` +{% endtab %} + +{% endtabs %} In most scenarios it doesn’t matter if `double` is a function or a method; Scala lets you treat them the same way. Behind the scenes, the Scala technology that lets you treat methods just like functions is known as [Eta Expansion][eta]. @@ -79,6 +115,9 @@ And as you’ve seen in the `map` and `filter` examples throughout this book, th If you’re not comfortable with the process of passing functions as parameters into other functions, here are a few more examples you can experiment with: +{% tabs fp-function-as-values-example %} + +{% tab 'Scala 2 and 3' %} ```scala List("bob", "joe").map(_.toUpperCase) // List(BOB, JOE) List("bob", "joe").map(_.capitalize) // List(Bob, Joe) @@ -97,6 +136,9 @@ nums.sortWith(_ > _) // List(11, 7, 5, 3, 1) nums.takeWhile(_ < 6).sortWith(_ < _) // List(1, 3, 5) ``` +{% endtab %} + +{% endtabs %} [hofs]: {% link _overviews/scala3-book/fun-hofs.md %} diff --git a/_overviews/scala3-book/fp-immutable-values.md b/_overviews/scala3-book/fp-immutable-values.md index c0e6209df5..6202943906 100644 --- a/_overviews/scala3-book/fp-immutable-values.md +++ b/_overviews/scala3-book/fp-immutable-values.md @@ -23,11 +23,17 @@ This is where higher-order functions like `map` and `filter` come in. For example, imagine that you have a list of names---a `List[String]`---that are all in lowercase, and you want to find all the names that begin with the letter `"j"`, and then you want to capitalize those names. In FP you write this code: +{% tabs fp-list %} + +{% tab 'Scala 2 and 3' %} ```scala val a = List("jane", "jon", "mary", "joe") val b = a.filter(_.startsWith("j")) .map(_.capitalize) ``` +{% endtab %} + +{% endtabs %} As shown, you don’t mutate the original list `a`. Instead, you apply filtering and transformation functions to `a` to create a new collection, and assign that result to the new immutable variable `b`. @@ -35,32 +41,57 @@ Instead, you apply filtering and transformation functions to `a` to create a new Similarly, in FP you don’t create classes with mutable `var` constructor parameters. That is, you don’t write this: +{% tabs fp--class-variables %} + +{% tab 'Scala 2 and 3' %} ```scala // don’t do this in FP class Person(var firstName: String, var lastName: String) --- --- ``` +{% endtab %} + +{% endtabs %} Instead, you typically create `case` classes, whose constructor parameters are `val` by default: +{% tabs fp-immutable-case-class %} + +{% tab 'Scala 2 and 3' %} ```scala case class Person(firstName: String, lastName: String) ``` +{% endtab %} + +{% endtabs %} Now you create a `Person` instance as a `val` field: +{% tabs fp-case-class-creation %} + +{% tab 'Scala 2 and 3' %} ```scala val reginald = Person("Reginald", "Dwight") ``` +{% endtab %} + +{% endtabs %} Then, when you need to make a change to the data, you use the `copy` method that comes with a `case` class to “update the data as you make a copy,” like this: + +{% tabs fp-case-class-copy %} + +{% tab 'Scala 2 and 3' %} ```scala val elton = reginald.copy( firstName = "Elton", // update the first name lastName = "John" // update the last name ) ``` +{% endtab %} + +{% endtabs %} There are other techniques for working with immutable collections and variables, but hopefully these examples give you a taste of the techniques. diff --git a/_overviews/scala3-book/fp-pure-functions.md b/_overviews/scala3-book/fp-pure-functions.md index e979d6a446..fd7c356421 100644 --- a/_overviews/scala3-book/fp-pure-functions.md +++ b/_overviews/scala3-book/fp-pure-functions.md @@ -87,17 +87,39 @@ These topics are beyond the scope of this document, so to keep things simple it To write pure functions in Scala, just write them using Scala’s method syntax (though you can also use Scala’s function syntax, as well). For instance, here’s a pure function that doubles the input value it’s given: + +{% tabs fp-pure-function %} + +{% tab 'Scala 2 and 3' %} ```scala def double(i: Int): Int = i * 2 ``` +{% endtab %} + +{% endtabs %} If you’re comfortable with recursion, here’s a pure function that calculates the sum of a list of integers: +{% tabs fp-pure-recursive-function class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +def sum(xs: List[Int]): Int = xs match { + case Nil => 0 + case head :: tail => head + sum(tail) +} +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala def sum(xs: List[Int]): Int = xs match case Nil => 0 case head :: tail => head + sum(tail) ``` +{% endtab %} + +{% endtabs %} If you understand that code, you’ll see that it meets the pure function definition.