diff --git a/_overviews/scala3-book/collections-classes.md b/_overviews/scala3-book/collections-classes.md index 70edce73f5..b9ba20afc8 100644 --- a/_overviews/scala3-book/collections-classes.md +++ b/_overviews/scala3-book/collections-classes.md @@ -127,6 +127,9 @@ Any time you want to add or remove `List` elements, you create a new `List` from This is how you create an initial `List`: +{% tabs list-creation %} + +{% tab 'Scala 2 and 3' %} ```scala val ints = List(1, 2, 3) val names = List("Joel", "Chris", "Ed") @@ -134,19 +137,43 @@ val names = List("Joel", "Chris", "Ed") // another way to construct a List val namesAgain = "Joel" :: "Chris" :: "Ed" :: Nil ``` +{% endtab %} + +{% endtabs %} + You can also declare the `List`’s type, if you prefer, though it generally isn’t necessary: +{% tabs list-type %} + +{% tab 'Scala 2 and 3' %} ```scala val ints: List[Int] = List(1, 2, 3) val names: List[String] = List("Joel", "Chris", "Ed") ``` +{% endtab %} + +{% endtabs %} + One exception is when you have mixed types in a collection; in that case you may want to explicitly specify its type: +{% tabs list-mixed-types class=tabs-scala-version %} + +{% tab 'Scala 2' %} ```scala val things: List[Any] = List(1, "two", 3.0) ``` +{% endtab %} + +{% tab 'Scala 3' %} +```scala +val things: List[String | Int | Double] = List(1, "two", 3.0) // with union types +val thingsAny: List[Any] = List(1, "two", 3.0) // with any +``` +{% endtab %} + +{% endtabs %} ### Adding elements to a List @@ -154,16 +181,28 @@ Because `List` is immutable, you can’t add new elements to it. Instead, you create a new list by prepending or appending elements to an existing `List`. For instance, given this `List`: +{% tabs adding-elements-init %} + +{% tab 'Scala 2 and 3' %} ```scala val a = List(1, 2, 3) ``` +{% endtab %} + +{% endtabs %} When working with a `List`, _prepend_ one element with `::`, and prepend another `List` with `:::`, as shown here: +{% tabs adding-elements-example %} + +{% tab 'Scala 2 and 3' %} ```scala val b = 0 :: a // List(0, 1, 2, 3) val c = List(-1, 0) ::: a // List(-1, 0, 1, 2, 3) ``` +{% endtab %} + +{% endtabs %} You can also _append_ elements to a `List`, but because `List` is a singly-linked list, you should generally only prepend elements to it; appending elements to it is a relatively slow operation, especially when you work with large sequences. @@ -178,15 +217,27 @@ If you have a large collection and want to access elements by their index, use a These days IDEs help us out tremendously, but one way to remember those method names is to think that the `:` character represents the side that the sequence is on, so when you use `+:` you know that the list needs to be on the right, like this: +{% tabs list-prepending %} + +{% tab 'Scala 2 and 3' %} ```scala 0 +: a ``` +{% endtab %} + +{% endtabs %} Similarly, when you use `:+` you know the list needs to be on the left: +{% tabs list-appending %} + +{% tab 'Scala 2 and 3' %} ```scala a :+ 4 ``` +{% endtab %} + +{% endtabs %} There are more technical ways to think about this, but this can be a helpful way to remember the method names. @@ -202,24 +253,58 @@ You can also use non-symbolic method names to append and prepend elements, if yo Given a `List` of names: +{% tabs list-loop-init %} + +{% tab 'Scala 2 and 3' %} ```scala val names = List("Joel", "Chris", "Ed") ``` +{% endtab %} + +{% endtabs %} you can print each string like this: +{% tabs list-loop-example class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +for (name <- names) println(name) +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala for name <- names do println(name) ``` +{% endtab %} + +{% endtabs %} This is what it looks like in the REPL: +{% tabs list-loop-repl class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +scala> for (name <- names) println(name) +Joel +Chris +Ed +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala scala> for name <- names do println(name) Joel Chris Ed ``` +{% endtab %} + +{% endtabs %} + A great thing about using `for` loops with collections is that Scala is consistent, and the same approach works with all sequences, including `Array`, `ArrayBuffer`, `List`, `Seq`, `Vector`, `Map`, `Set`, etc. @@ -228,22 +313,40 @@ A great thing about using `for` loops with collections is that Scala is consiste For those interested in a little bit of history, the Scala `List` is similar to the `List` from [the Lisp programming language](https://en.wikipedia.org/wiki/Lisp_(programming_language)), which was originally specified in 1958. Indeed, in addition to creating a `List` like this: +{% tabs list-history-init %} + +{% tab 'Scala 2 and 3' %} ```scala val ints = List(1, 2, 3) ``` +{% endtab %} + +{% endtabs %} you can also create the exact same list this way: +{% tabs list-history-init2 %} + +{% tab 'Scala 2 and 3' %} ```scala val list = 1 :: 2 :: 3 :: Nil ``` +{% endtab %} + +{% endtabs %} The REPL shows how this works: +{% tabs list-history-repl %} + +{% tab 'Scala 2 and 3' %} ```scala scala> val list = 1 :: 2 :: 3 :: Nil list: List[Int] = List(1, 2, 3) ``` +{% endtab %} + +{% endtabs %} This works because a `List` is a singly-linked list that ends with the `Nil` element, and `::` is a `List` method that works like Lisp’s “cons” operator. @@ -255,20 +358,32 @@ It’s called “lazy”---or non-strict---because it computes its elements only You can see how lazy a `LazyList` is in the REPL: +{% tabs lazylist-example %} + +{% tab 'Scala 2 and 3' %} ```scala val x = LazyList.range(1, Int.MaxValue) x.take(1) // LazyList() x.take(5) // LazyList() x.map(_ + 1) // LazyList() ``` +{% endtab %} + +{% endtabs %} In all of those examples, nothing happens. Indeed, nothing will happen until you force it to happen, such as by calling its `foreach` method: -```` +{% tabs lazylist-evaluation-example %} + +{% tab 'Scala 2 and 3' %} +```scala scala> x.take(1).foreach(println) 1 -```` +``` +{% endtab %} + +{% endtabs %} For more information on the uses, benefits, and drawbacks of strict and non-strict (lazy) collections, see the “strict” and “non-strict” discussions on the [The Architecture of Scala 2.13’s Collections][strict] page. @@ -287,6 +402,9 @@ In general, except for the difference that (a) `Vector` is indexed and `List` is Here are a few ways you can create a `Vector`: +{% tabs vector-creation %} + +{% tab 'Scala 2 and 3' %} ```scala val nums = Vector(1, 2, 3, 4, 5) @@ -299,24 +417,39 @@ val people = Vector( Person("Grover") ) ``` +{% endtab %} + +{% endtabs %} Because `Vector` is immutable, you can’t add new elements to it. Instead, you create a new sequence by appending or prepending elements to an existing `Vector`. These examples show how to _append_ elements to a `Vector`: +{% tabs vector-appending %} + +{% tab 'Scala 2 and 3' %} ```scala val a = Vector(1,2,3) // Vector(1, 2, 3) val b = a :+ 4 // Vector(1, 2, 3, 4) val c = a ++ Vector(4, 5) // Vector(1, 2, 3, 4, 5) ``` +{% endtab %} + +{% endtabs %} This is how you _prepend_ elements: +{% tabs vector-prepending %} + +{% tab 'Scala 2 and 3' %} ```scala val a = Vector(1,2,3) // Vector(1, 2, 3) val b = 0 +: a // Vector(0, 1, 2, 3) val c = Vector(-1, 0) ++: a // Vector(-1, 0, 1, 2, 3) ``` +{% endtab %} + +{% endtabs %} In addition to fast random access and updates, `Vector` provides fast append and prepend times, so you can use these features as desired. @@ -324,6 +457,21 @@ In addition to fast random access and updates, `Vector` provides fast append and Finally, you use a `Vector` in a `for` loop just like a `List`, `ArrayBuffer`, or any other sequence: +{% tabs vector-loop class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +scala> val names = Vector("Joel", "Chris", "Ed") +val names: Vector[String] = Vector(Joel, Chris, Ed) + +scala> for (name <- names) println(name) +Joel +Chris +Ed +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala scala> val names = Vector("Joel", "Chris", "Ed") val names: Vector[String] = Vector(Joel, Chris, Ed) @@ -333,7 +481,9 @@ Joel Chris Ed ``` +{% endtab %} +{% endtabs %} ## ArrayBuffer @@ -346,27 +496,48 @@ Because it’s indexed, random access of elements is fast. To use an `ArrayBuffer`, first import it: +{% tabs arraybuffer-import %} + +{% tab 'Scala 2 and 3' %} ```scala import scala.collection.mutable.ArrayBuffer ``` +{% endtab %} + +{% endtabs %} If you need to start with an empty `ArrayBuffer`, just specify its type: +{% tabs arraybuffer-creation %} + +{% tab 'Scala 2 and 3' %} ```scala var strings = ArrayBuffer[String]() var ints = ArrayBuffer[Int]() var people = ArrayBuffer[Person]() ``` +{% endtab %} + +{% endtabs %} If you know the approximate size your `ArrayBuffer` eventually needs to be, you can create it with an initial size: +{% tabs list-creation-with-size %} + +{% tab 'Scala 2 and 3' %} ```scala // ready to hold 100,000 ints val buf = new ArrayBuffer[Int](100_000) ``` +{% endtab %} + +{% endtabs %} To create a new `ArrayBuffer` with initial elements, just specify its initial elements, just like a `List` or `Vector`: +{% tabs arraybuffer-init %} + +{% tab 'Scala 2 and 3' %} ```scala val nums = ArrayBuffer(1, 2, 3) val people = ArrayBuffer( @@ -375,6 +546,9 @@ val people = ArrayBuffer( Person("Grover") ) ``` +{% endtab %} + +{% endtabs %} ### Adding elements to an ArrayBuffer @@ -383,33 +557,51 @@ Or if you prefer methods with textual names you can also use `append`, `appendAl Here are some examples of `+=` and `++=`: +{% tabs arraybuffer-add %} + +{% tab 'Scala 2 and 3' %} ```scala val nums = ArrayBuffer(1, 2, 3) // ArrayBuffer(1, 2, 3) nums += 4 // ArrayBuffer(1, 2, 3, 4) nums ++= List(5, 6) // ArrayBuffer(1, 2, 3, 4, 5, 6) ``` +{% endtab %} + +{% endtabs %} ### Removing elements from an ArrayBuffer `ArrayBuffer` is mutable, so it has methods like `-=`, `--=`, `clear`, `remove`, and more. These examples demonstrate the `-=` and `--=` methods: +{% tabs arraybuffer-remove %} + +{% tab 'Scala 2 and 3' %} ```scala val a = ArrayBuffer.range('a', 'h') // ArrayBuffer(a, b, c, d, e, f, g) a -= 'a' // ArrayBuffer(b, c, d, e, f, g) a --= Seq('b', 'c') // ArrayBuffer(d, e, f, g) a --= Set('d', 'e') // ArrayBuffer(f, g) ``` +{% endtab %} + +{% endtabs %} ### Updating ArrayBuffer elements Update elements in an `ArrayBuffer` by either reassigning the desired element, or use the `update` method: +{% tabs arraybuffer-update %} + +{% tab 'Scala 2 and 3' %} ```scala val a = ArrayBuffer.range(1,5) // ArrayBuffer(1, 2, 3, 4) a(2) = 50 // ArrayBuffer(1, 2, 50, 4) a.update(0, 10) // ArrayBuffer(10, 2, 50, 4) ``` +{% endtab %} + +{% endtabs %} @@ -422,6 +614,9 @@ Scala has both mutable and immutable `Map` types, and this section demonstrates Create an immutable `Map` like this: +{% tabs map-init %} + +{% tab 'Scala 2 and 3' %} ```scala val states = Map( "AK" -> "Alaska", @@ -429,30 +624,66 @@ val states = Map( "AZ" -> "Arizona" ) ``` +{% endtab %} + +{% endtabs %} Once you have a `Map` you can traverse its elements in a `for` loop like this: +{% tabs map-loop class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +for ((k, v) <- states) println(s"key: $k, value: $v") +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala for (k, v) <- states do println(s"key: $k, value: $v") ``` +{% endtab %} + +{% endtabs %} The REPL shows how this works: -```` +{% tabs map-repl class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +scala> for ((k, v) <- states) println(s"key: $k, value: $v") +key: AK, value: Alaska +key: AL, value: Alabama +key: AZ, value: Arizona +``` +{% endtab %} + +{% tab 'Scala 3' %} +```scala scala> for (k, v) <- states do println(s"key: $k, value: $v") key: AK, value: Alaska key: AL, value: Alabama key: AZ, value: Arizona -```` +``` +{% endtab %} + +{% endtabs %} ### Accessing Map elements Access map elements by specifying the desired key value in parentheses: +{% tabs map-access-element %} + +{% tab 'Scala 2 and 3' %} ```scala val ak = states("AK") // ak: String = Alaska val al = states("AL") // al: String = Alabama ``` +{% endtab %} + +{% endtabs %} In practice, you’ll also use methods like `keys`, `keySet`, `keysIterator`, `for` loops, and higher-order functions like `map` to work with `Map` keys and values. @@ -460,6 +691,9 @@ In practice, you’ll also use methods like `keys`, `keySet`, `keysIterator`, `f Add elements to an immutable map using `+` and `++`, remembering to assign the result to a new variable: +{% tabs map-add-element %} + +{% tab 'Scala 2 and 3' %} ```scala val a = Map(1 -> "one") // a: Map(1 -> one) val b = a + (2 -> "two") // b: Map(1 -> one, 2 -> two) @@ -469,11 +703,17 @@ val c = b ++ Seq( ) // c: Map(1 -> one, 2 -> two, 3 -> three, 4 -> four) ``` +{% endtab %} + +{% endtabs %} ### Removing elements from a Map Remove elements from an immutable map using `-` or `--` and the key values to remove, remembering to assign the result to a new variable: +{% tabs map-remove-element %} + +{% tab 'Scala 2 and 3' %} ```scala val a = Map( 1 -> "one", @@ -485,11 +725,17 @@ val a = Map( val b = a - 4 // b: Map(1 -> one, 2 -> two, 3 -> three) val c = a - 4 - 3 // c: Map(1 -> one, 2 -> two) ``` +{% endtab %} + +{% endtabs %} ### Updating Map elements To update elements in an immutable map, use the `updated` method (or the `+` operator) while assigning the result to a new variable: +{% tabs map-update-element %} + +{% tab 'Scala 2 and 3' %} ```scala val a = Map( 1 -> "one", @@ -500,11 +746,30 @@ val a = Map( val b = a.updated(3, "THREE!") // b: Map(1 -> one, 2 -> two, 3 -> THREE!) val c = a + (2 -> "TWO...") // c: Map(1 -> one, 2 -> TWO..., 3 -> three) ``` +{% endtab %} + +{% endtabs %} ### Traversing a Map As shown earlier, this is a common way to manually traverse elements in a map using a `for` loop: + +{% tabs map-traverse class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +val states = Map( + "AK" -> "Alaska", + "AL" -> "Alabama", + "AZ" -> "Arizona" +) + +for ((k, v) <- states) println(s"key: $k, value: $v") +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala val states = Map( "AK" -> "Alaska", @@ -514,6 +779,9 @@ val states = Map( for (k, v) <- states do println(s"key: $k, value: $v") ``` +{% endtab %} + +{% endtabs %} That being said, there are _many_ ways to work with the keys and values in a map. Common `Map` methods include `foreach`, `map`, `keys`, and `values`. @@ -534,28 +802,46 @@ This section demonstrates the _immutable_ `Set`. Create new empty sets like this: +{% tabs set-creation %} + +{% tab 'Scala 2 and 3' %} ```scala val nums = Set[Int]() val letters = Set[Char]() ``` +{% endtab %} + +{% endtabs %} Create sets with initial data like this: +{% tabs set-init %} + +{% tab 'Scala 2 and 3' %} ```scala val nums = Set(1, 2, 3, 3, 3) // Set(1, 2, 3) val letters = Set('a', 'b', 'c', 'c') // Set('a', 'b', 'c') ``` +{% endtab %} + +{% endtabs %} ### Adding elements to a Set Add elements to an immutable `Set` using `+` and `++`, remembering to assign the result to a new variable: +{% tabs set-add-element %} + +{% tab 'Scala 2 and 3' %} ```scala val a = Set(1, 2) // Set(1, 2) val b = a + 3 // Set(1, 2, 3) val c = b ++ Seq(4, 1, 5, 5) // HashSet(5, 1, 2, 3, 4) ``` +{% endtab %} + +{% endtabs %} Notice that when you attempt to add duplicate elements, they’re quietly dropped. @@ -566,11 +852,17 @@ Also notice that the order of iteration of the elements is arbitrary. Remove elements from an immutable set using `-` and `--`, again assigning the result to a new variable: +{% tabs set-remove-element %} + +{% tab 'Scala 2 and 3' %} ```scala val a = Set(1, 2, 3, 4, 5) // HashSet(5, 1, 2, 3, 4) val b = a - 5 // HashSet(1, 2, 3, 4) val c = b -- Seq(3, 4) // HashSet(1, 2) ``` +{% endtab %} + +{% endtabs %} @@ -583,39 +875,76 @@ These REPL examples demonstrate how to create ranges: LATER: the dotty repl currently shows results differently {% endcomment %} +{% tabs range-init %} + +{% tab 'Scala 2 and 3' %} ```scala 1 to 5 // Range(1, 2, 3, 4, 5) 1 until 5 // Range(1, 2, 3, 4) 1 to 10 by 2 // Range(1, 3, 5, 7, 9) 'a' to 'c' // NumericRange(a, b, c) ``` +{% endtab %} + +{% endtabs %} You can use ranges to populate collections: +{% tabs range-conversion %} + +{% tab 'Scala 2 and 3' %} ```scala val x = (1 to 5).toList // List(1, 2, 3, 4, 5) val x = (1 to 5).toBuffer // ArrayBuffer(1, 2, 3, 4, 5) ``` +{% endtab %} + +{% endtabs %} They’re also used in `for` loops: -```` +{% tabs range-iteration class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +scala> for (i <- 1 to 3) println(i) +1 +2 +3 +``` +{% endtab %} + +{% tab 'Scala 3' %} +```scala scala> for i <- 1 to 3 do println(i) 1 2 3 -```` +``` +{% endtab %} + +{% endtabs %} + There are also `range` methods on : +{% tabs range-methods %} + +{% tab 'Scala 2 and 3' %} ```scala Vector.range(1, 5) // Vector(1, 2, 3, 4) List.range(1, 10, 2) // List(1, 3, 5, 7, 9) Set.range(1, 10) // HashSet(5, 1, 6, 9, 2, 7, 3, 8, 4) ``` +{% endtab %} + +{% endtabs %} When you’re running tests, ranges are also useful for generating test collections: +{% tabs range-tests %} + +{% tab 'Scala 2 and 3' %} ```scala val evens = (0 to 10 by 2).toList // List(0, 2, 4, 6, 8, 10) val odds = (1 to 10 by 2).toList // List(1, 3, 5, 7, 9) @@ -625,6 +954,9 @@ val doubles = (1 to 5).map(_ * 2.0) // Vector(2.0, 4.0, 6.0, 8.0, 10.0) val map = (1 to 3).map(e => (e,s"$e")).toMap // map: Map[Int, String] = Map(1 -> "1", 2 -> "2", 3 -> "3") ``` +{% endtab %} + +{% endtabs %} ## More details diff --git a/_overviews/scala3-book/collections-methods.md b/_overviews/scala3-book/collections-methods.md index a8892744ae..35fcb72b70 100644 --- a/_overviews/scala3-book/collections-methods.md +++ b/_overviews/scala3-book/collections-methods.md @@ -37,6 +37,9 @@ The following methods work on all of the sequence types, including `List`, `Vect To give you an overview of what you’ll see in the following sections, these examples show some of the most commonly used collections methods. First, here are some methods that don’t use lambdas: +{% tabs common-method-examples %} + +{% tab 'Scala 2 and 3' %} ```scala val a = List(10, 20, 30, 40, 10) // List(10, 20, 30, 40, 10) @@ -54,6 +57,9 @@ a.tail // List(20, 30, 40, 10) a.take(3) // List(10, 20, 30) a.takeRight(2) // List(40, 10) ``` +{% endtab %} + +{% endtabs %} ### Higher-order functions and lambdas @@ -61,6 +67,9 @@ a.takeRight(2) // List(40, 10) Next, we’ll show some commonly used higher-order functions (HOFs) that accept lambdas (anonymous functions). To get started, here are several variations of the lambda syntax, starting with the longest form, working in steps towards the most concise form: +{% tabs higher-order-functions-example %} + +{% tab 'Scala 2 and 3' %} ```scala // these functions are all equivalent and return // the same data: List(10, 20, 10) @@ -70,6 +79,9 @@ a.filter((i) => i < 25) // 2. `Int` is not required a.filter(i => i < 25) // 3. the parens are not required a.filter(_ < 25) // 4. `i` is not required ``` +{% endtab %} + +{% endtabs %} In those numbered examples: @@ -83,6 +95,9 @@ The [Anonymous Function][lambdas] provides more details and examples of the rule Now that you’ve seen the concise form, here are examples of other HOFs that use the short-form lambda syntax: +{% tabs anonymous-functions-example %} + +{% tab 'Scala 2 and 3' %} ```scala a.dropWhile(_ < 25) // List(30, 40, 10) a.filter(_ > 100) // List() @@ -90,11 +105,17 @@ a.filterNot(_ < 25) // List(30, 40) a.find(_ > 20) // Some(30) a.takeWhile(_ < 30) // List(10, 20) ``` +{% endtab %} + +{% endtabs %} It’s important to note that HOFs also accept methods and functions as parameters---not just lambda expressions. Here are some examples of the `map` HOF that uses a method named `double`. Several variations of the lambda syntax are shown again: +{% tabs method-as-parameter-example %} + +{% tab 'Scala 2 and 3' %} ```scala def double(i: Int) = i * 2 @@ -103,17 +124,26 @@ a.map(i => double(i)) a.map(double(_)) a.map(double) ``` +{% endtab %} + +{% endtabs %} In the last example, when an anonymous function consists of one function call that takes a single argument, you don’t have to name the argument, so even `_` isn’t required. Finally, you can combine HOFs as desired to solve problems: +{% tabs higher-order-functions-combination-example %} + +{% tab 'Scala 2 and 3' %} ```scala // yields `List(100, 200)` a.filter(_ < 40) .takeWhile(_ < 30) .map(_ * 10) ``` +{% endtab %} + +{% endtabs %} @@ -121,10 +151,16 @@ a.filter(_ < 40) The examples in the following sections use these lists: +{% tabs sample-data %} + +{% tab 'Scala 2 and 3' %} ```scala val oneToTen = (1 to 10).toList val names = List("adam", "brandy", "chris", "david") ``` +{% endtab %} + +{% endtabs %} @@ -135,22 +171,37 @@ it then returns a new list with all of the modified elements. Here’s an example of the `map` method being applied to the `oneToTen` list: +{% tabs map-example %} + +{% tab 'Scala 2 and 3' %} ```scala scala> val doubles = oneToTen.map(_ * 2) doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20) ``` +{% endtab %} + +{% endtabs %} You can also write anonymous functions using a long form, like this: +{% tabs map-example-anonymous %} + +{% tab 'Scala 2 and 3' %} ```scala scala> val doubles = oneToTen.map(i => i * 2) doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20) ``` +{% endtab %} + +{% endtabs %} However, in this lesson we’ll always use the first, shorter form. Here are a few more examples of the `map` method being applied to the `oneToTen` and `names` lists: +{% tabs few-more-examples %} + +{% tab 'Scala 2 and 3' %} ```scala scala> val capNames = names.map(_.capitalize) capNames: List[String] = List(Adam, Brandy, Chris, David) @@ -161,6 +212,9 @@ nameLengthsMap: Map[String, Int] = Map(adam -> 4, brandy -> 6, chris -> 5, david scala> val isLessThanFive = oneToTen.map(_ < 5) isLessThanFive: List[Boolean] = List(true, true, true, true, false, false, false, false, false, false) ``` +{% endtab %} + +{% endtabs %} As shown in the last two examples, it’s perfectly legal (and common) to use `map` to return a collection that has a different type than the original type. @@ -172,6 +226,9 @@ The `filter` method creates a new list containing the element that satisfy the p A predicate, or condition, is a function that returns a `Boolean` (`true` or `false`). Here are a few examples: +{% tabs filter-example %} + +{% tab 'Scala 2 and 3' %} ```scala scala> val lessThanFive = oneToTen.filter(_ < 5) lessThanFive: List[Int] = List(1, 2, 3, 4) @@ -182,20 +239,35 @@ evens: List[Int] = List(2, 4, 6, 8, 10) scala> val shortNames = names.filter(_.length <= 4) shortNames: List[String] = List(adam) ``` +{% endtab %} + +{% endtabs %} A great thing about the functional methods on collections is that you can chain them together to solve problems. For instance, this example shows how to chain `filter` and `map`: +{% tabs filter-example-anonymous %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.filter(_ < 4).map(_ * 10) ``` +{% endtab %} + +{% endtabs %} The REPL shows the result: +{% tabs filter-example-anonymous-repl %} + +{% tab 'Scala 2 and 3' %} ```scala scala> oneToTen.filter(_ < 4).map(_ * 10) val res1: List[Int] = List(10, 20, 30) ``` +{% endtab %} + +{% endtabs %} @@ -205,6 +277,9 @@ The `foreach` method is used to loop over all elements in a collection. Note that `foreach` is used for side-effects, such as printing information. Here’s an example with the `names` list: +{% tabs foreach-example %} + +{% tab 'Scala 2 and 3' %} ```scala scala> names.foreach(println) adam @@ -212,6 +287,9 @@ brandy chris david ``` +{% endtab %} + +{% endtabs %} @@ -220,31 +298,55 @@ david The `head` method comes from Lisp and other earlier functional programming languages. It’s used to access the first element (the head element) of a list: +{% tabs head-example %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.head // 1 names.head // adam ``` +{% endtab %} + +{% endtabs %} Because a `String` can be seen as a sequence of characters, you can also treat it like a list. This is how `head` works on these strings: +{% tabs string-head-example %} + +{% tab 'Scala 2 and 3' %} ```scala "foo".head // 'f' "bar".head // 'b' ``` +{% endtab %} + +{% endtabs %} `head` is a great method to work with, but as a word of caution it can also throw an exception when called on an empty collection: +{% tabs head-error-example %} + +{% tab 'Scala 2 and 3' %} ```scala val emptyList = List[Int]() // emptyList: List[Int] = List() emptyList.head // java.util.NoSuchElementException: head of empty list ``` +{% endtab %} + +{% endtabs %} Because of this you may want to use `headOption` instead of `head`, especially when programming in a functional style: +{% tabs head-option-example %} + +{% tab 'Scala 2 and 3' %} ```scala emptyList.headOption // None ``` +{% endtab %} + +{% endtabs %} As shown, it doesn't throw an exception, it simply returns the type `Option` that has the value `None`. You can learn more about this programming style in the [Functional Programming][fp-intro] chapter. @@ -256,6 +358,9 @@ You can learn more about this programming style in the [Functional Programming][ The `tail` method also comes from Lisp, and it’s used to print every element in a list after the head element. A few examples demonstrate this: +{% tabs tail-example %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.head // 1 oneToTen.tail // List(2, 3, 4, 5, 6, 7, 8, 9, 10) @@ -263,37 +368,73 @@ oneToTen.tail // List(2, 3, 4, 5, 6, 7, 8, 9, 10) names.head // adam names.tail // List(brandy, chris, david) ``` +{% endtab %} + +{% endtabs %} Just like `head`, `tail` also works on strings: +{% tabs string-tail-example %} + +{% tab 'Scala 2 and 3' %} ```scala "foo".tail // "oo" "bar".tail // "ar" ``` +{% endtab %} + +{% endtabs %} `tail` throws a _java.lang.UnsupportedOperationException_ if the list is empty, so just like `head` and `headOption`, there’s also a `tailOption` method, which is preferred in functional programming. A list can also be matched, so you can write expressions like this: +{% tabs tail-match-example %} + +{% tab 'Scala 2 and 3' %} ```scala val x :: xs = names ``` +{% endtab %} + +{% endtabs %} Putting that code in the REPL shows that `x` is assigned to the head of the list, and `xs` is assigned to the tail: +{% tabs tail-match-example-repl %} + +{% tab 'Scala 2 and 3' %} ```scala scala> val x :: xs = names val x: String = adam val xs: List[String] = List(brandy, chris, david) ``` +{% endtab %} + +{% endtabs %} Pattern matching like this is useful in many situations, such as writing a `sum` method using recursion: +{% tabs tail-match-sum-example class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +def sum(list: List[Int]): Int = list match { + case Nil => 0 + case x :: xs => x + sum(xs) +} +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala def sum(list: List[Int]): Int = list match case Nil => 0 case x :: xs => x + sum(xs) ``` +{% endtab %} + +{% endtabs %} @@ -302,6 +443,9 @@ def sum(list: List[Int]): Int = list match The `take`, `takeRight`, and `takeWhile` methods give you a nice way of “taking” the elements from a list that you want to use to create a new list. This is `take` and `takeRight`: +{% tabs take-example %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.take(1) // List(1) oneToTen.take(2) // List(1, 2) @@ -309,22 +453,37 @@ oneToTen.take(2) // List(1, 2) oneToTen.takeRight(1) // List(10) oneToTen.takeRight(2) // List(9, 10) ``` +{% endtab %} + +{% endtabs %} Notice how these methods work with “edge” cases, where we ask for more elements than are in the sequence, or ask for zero elements: +{% tabs take-edge-cases-example %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.take(Int.MaxValue) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) oneToTen.takeRight(Int.MaxValue) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) oneToTen.take(0) // List() oneToTen.takeRight(0) // List() ``` +{% endtab %} + +{% endtabs %} And this is `takeWhile`, which works with a predicate function: +{% tabs take-while-example %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.takeWhile(_ < 5) // List(1, 2, 3, 4) names.takeWhile(_.length < 5) // List(adam) ``` +{% endtab %} + +{% endtabs %} ## `drop`, `dropRight`, `dropWhile` @@ -332,6 +491,9 @@ names.takeWhile(_.length < 5) // List(adam) `drop`, `dropRight`, and `dropWhile` are essentially the opposite of their “take” counterparts, dropping elements from a list. Here are some examples: +{% tabs drop-example %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.drop(1) // List(2, 3, 4, 5, 6, 7, 8, 9, 10) oneToTen.drop(5) // List(6, 7, 8, 9, 10) @@ -339,22 +501,37 @@ oneToTen.drop(5) // List(6, 7, 8, 9, 10) oneToTen.dropRight(8) // List(1, 2) oneToTen.dropRight(7) // List(1, 2, 3) ``` +{% endtab %} + +{% endtabs %} Again notice how these methods work with edge cases: +{% tabs drop-edge-cases-example %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.drop(Int.MaxValue) // List() oneToTen.dropRight(Int.MaxValue) // List() oneToTen.drop(0) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) oneToTen.dropRight(0) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) ``` +{% endtab %} + +{% endtabs %} And this is `dropWhile`, which works with a predicate function: +{% tabs drop-while-example %} + +{% tab 'Scala 2 and 3' %} ```scala oneToTen.dropWhile(_ < 5) // List(5, 6, 7, 8, 9, 10) names.dropWhile(_ != "chris") // List(chris, david) ``` +{% endtab %} + +{% endtabs %} @@ -366,21 +543,46 @@ It takes a function (or anonymous function) and applies that function to success The best way to explain `reduce` is to create a little helper method you can pass into it. For example, this is an `add` method that adds two integers together, and also provides us some nice debug output: +{% tabs reduce-example class=tabs-scala-version %} + +{% tab 'Scala 2' %} +```scala +def add(x: Int, y: Int): Int = { + val theSum = x + y + println(s"received $x and $y, their sum is $theSum") + theSum +} +``` +{% endtab %} + +{% tab 'Scala 3' %} ```scala def add(x: Int, y: Int): Int = val theSum = x + y println(s"received $x and $y, their sum is $theSum") theSum ``` +{% endtab %} + +{% endtabs %} Given that method and this list: +{% tabs reduce-example-init %} + +{% tab 'Scala 2 and 3' %} ```scala val a = List(1,2,3,4) ``` +{% endtab %} + +{% endtabs %} this is what happens when you pass the `add` method into `reduce`: +{% tabs reduce-example-evaluation %} + +{% tab 'Scala 2 and 3' %} ```scala scala> a.reduce(add) received 1 and 2, their sum is 3 @@ -388,22 +590,37 @@ received 3 and 3, their sum is 6 received 6 and 4, their sum is 10 res0: Int = 10 ``` +{% endtab %} + +{% endtabs %} As that result shows, `reduce` uses `add` to reduce the list `a` into a single value, in this case, the sum of the integers in the list. Once you get used to `reduce`, you’ll write a “sum” algorithm like this: +{% tabs reduce-example-sum %} + +{% tab 'Scala 2 and 3' %} ```scala scala> a.reduce(_ + _) res0: Int = 10 ``` +{% endtab %} + +{% endtabs %} Similarly, a “product” algorithm looks like this: +{% tabs reduce-example-multiply %} + +{% tab 'Scala 2 and 3' %} ```scala scala> a.reduce(_ * _) res1: Int = 24 ``` +{% endtab %} + +{% endtabs %} > An important concept to know about `reduce` is that---as its name implies---it’s used to _reduce_ a collection down to a single value.