From dca9917f0b1c6924d3319bb22de28de3a9af3f79 Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Fri, 7 Oct 2022 00:30:23 +0800 Subject: [PATCH 01/10] add code tabs in num20. --- .../scala3-book/domain-modeling-tools.md | 508 +++++++++++++++++- 1 file changed, 480 insertions(+), 28 deletions(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 52ff30139e..378ccfe909 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -8,6 +8,7 @@ previous-page: domain-modeling-intro next-page: domain-modeling-oop --- + Scala 3 provides many different constructs so we can model the world around us: - Classes @@ -21,17 +22,20 @@ Scala 3 provides many different constructs so we can model the world around us: This section briefly introduces each of these language features. - ## Classes As with other languages, a _class_ in Scala is a template for the creation of object instances. Here are some examples of classes: +{% tabs class_1 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=class_1 %} ```scala class Person(var name: String, var vocation: String) class Book(var title: String, var author: String, var year: Int) class Movie(var name: String, var director: String, var year: Int) ``` +{% endtab %} +{% endtabs %} These examples show that Scala has a very lightweight way to declare classes. @@ -40,30 +44,46 @@ If you want them to be immutable---read only---create them as `val` fields inste Prior to Scala 3, you used the `new` keyword to create a new instance of a class: +{% tabs class_2 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=class_2 %} ```scala val p = new Person("Robert Allen Zimmerman", "Harmonica Player") // --- ``` +{% endtab %} +{% endtabs %} However, with [creator applications][creator] this isn’t required in Scala 3: +{% tabs class_3 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=class_3 %} ```scala val p = Person("Robert Allen Zimmerman", "Harmonica Player") ``` +{% endtab %} +{% endtabs %} Once you have an instance of a class such as `p`, you can access its fields, which in this example are all constructor parameters: +{% tabs class_4 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=class_4 %} ```scala p.name // "Robert Allen Zimmerman" p.vocation // "Harmonica Player" ``` +{% endtab %} +{% endtabs %} As mentioned, all of these parameters were created as `var` fields, so you can also mutate them: +{% tabs class_5 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=class_5 %} ```scala p.name = "Bob Dylan" p.vocation = "Musician" ``` +{% endtab %} +{% endtabs %} ### Fields and methods @@ -71,6 +91,26 @@ Classes can also have methods and additional fields that are not part of constru They are defined in the body of the class. The body is initialized as part of the default constructor: +{% tabs method class=tabs-scala-version %} +{% tab 'Scala 2' for=method %} +```scala +class Person(var firstName: String, var lastName: String) { + + println("initialization begins") + val fullName = firstName + " " + lastName + + // a class method + def printFullName: Unit = + // access the `fullName` field, which is created above + println(fullName) + + printFullName + println("initialization ends") +} +``` +{% endtab %} + +{% tab 'Scala 3' for=method %} ```scala class Person(var firstName: String, var lastName: String): @@ -85,6 +125,8 @@ class Person(var firstName: String, var lastName: String): printFullName println("initialization ends") ``` +{% endtab %} +{% endtabs %} The following REPL session shows how to create a new `Person` instance with this class: @@ -105,13 +147,19 @@ Classes can also extend traits and abstract classes, which we cover in dedicated As a quick look at a few other features, class constructor parameters can also have default values: +{% tabs default-values_1 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=default-values_1 %} ```scala class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): override def toString = s"timeout: $timeout, linger: $linger" ``` +{% endtab %} +{% endtabs %} A great thing about this feature is that it lets consumers of your code create classes in a variety of different ways, as though the class had alternate constructors: +{% tabs default-values_2 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=default-values_2 %} ```scala val s = Socket() // timeout: 5000, linger: 5000 val s = Socket(2_500) // timeout: 2500, linger: 5000 @@ -119,10 +167,14 @@ val s = Socket(10_000, 10_000) // timeout: 10000, linger: 10000 val s = Socket(timeout = 10_000) // timeout: 10000, linger: 5000 val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000 ``` +{% endtab %} +{% endtabs %} When creating a new instance of a class, you can also use named parameters. This is particularly helpful when many of the parameters have the same type, as shown in this comparison: +{% tabs default-values_3 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=default-values_3 %} ```scala // option 1 val s = Socket(10_000, 10_000) @@ -133,6 +185,8 @@ val s = Socket( linger = 10_000 ) ``` +{% endtab %} +{% endtabs %} ### Auxiliary constructors @@ -146,6 +200,45 @@ While analyzing the requirements you’ve seen that you need to be able to const One way to handle this situation in an OOP style is with this code: +{% tabs structor_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=structor_1 %} +```scala +import java.time._ + +// [1] the primary constructor +class Student( + var name: String, + var govtId: String +) { + private var _applicationDate: Option[LocalDate] = None + private var _studentId: Int = 0 + + // [2] a constructor for when the student has completed + // their application + def this( + name: String, + govtId: String, + applicationDate: LocalDate + ) = { + this(name, govtId) + _applicationDate = Some(applicationDate) + } + + // [3] a constructor for when the student is approved + // and now has a student id + def this( + name: String, + govtId: String, + studentId: Int + ) = { + this(name, govtId) + _studentId = studentId + } +} +``` +{% endtab %} + +{% tab 'Scala 3' for=structor_1 %} ```scala import java.time.* @@ -177,6 +270,8 @@ class Student( this(name, govtId) _studentId = studentId ``` +{% endtab %} +{% endtabs %} {% comment %} // for testing that code @@ -196,17 +291,19 @@ The class has three constructors, given by the numbered comments in the code: Those constructors can be called like this: +{% tabs structor_2 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=structor_2 %} ```scala val s1 = Student("Mary", "123") val s2 = Student("Mary", "123", LocalDate.now) val s3 = Student("Mary", "123", 456) ``` +{% endtab %} +{% endtabs %} While this technique can be used, bear in mind that constructor parameters can also have default values, which make it seem that a class has multiple constructors. This is shown in the previous `Socket` example. - - ## Objects An object is a class that has exactly one instance. @@ -216,47 +313,96 @@ Objects in Scala allow grouping methods and fields under one namespace, similar Declaring an `object` is similar to declaring a `class`. Here’s an example of a “string utilities” object that contains a set of methods for working with strings: +{% tabs object_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=object_1 %} +```scala +object StringUtils { + def truncate(s: String, length: Int): String = s.take(length) + def containsWhitespace(s: String): Boolean = s.object_1es(".*\\s.*") + def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty +} +``` +{% endtab %} + +{% tab 'Scala 3' for=object_1 %} ```scala object StringUtils: def truncate(s: String, length: Int): String = s.take(length) - def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*") + def containsWhitespace(s: String): Boolean = s.object_1es(".*\\s.*") def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty ``` +{% endtab %} +{% endtabs %} We can use the object as follows: + +{% tabs object_2 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=object_2 %} ```scala StringUtils.truncate("Chuck Bartowski", 5) // "Chuck" ``` +{% endtab %} +{% endtabs %} Importing in Scala is very flexible, and allows us to import _all_ members of an object: +{% tabs object_3 class=tabs-scala-version %} +{% tab 'Scala 2' for=object_3 %} +```scala +import StringUtils._ +truncate("Chuck Bartowski", 5) // "Chuck" +containsWhitespace("Sarah Walker") // true +isNullOrEmpty("John Casey") // false +``` +{% endtab %} + +{% tab 'Scala 3' for=object_3 %} ```scala import StringUtils.* truncate("Chuck Bartowski", 5) // "Chuck" containsWhitespace("Sarah Walker") // true isNullOrEmpty("John Casey") // false ``` +{% endtab %} +{% endtabs %} or just _some_ members: +{% tabs object_4 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=object_4 %} ```scala import StringUtils.{truncate, containsWhitespace} truncate("Charles Carmichael", 7) // "Charles" containsWhitespace("Captain Awesome") // true isNullOrEmpty("Morgan Grimes") // Not found: isNullOrEmpty (error) ``` +{% endtab %} +{% endtabs %} Objects can also contain fields, which are also accessed like static members: +{% tabs object_5 class=tabs-scala-version %} +{% tab 'Scala 2' for=object_5 %} ```scala -object MathConstants: +object MathConstants { val PI = 3.14159 val E = 2.71828 +} println(MathConstants.PI) // 3.14159 ``` +{% endtab %} +{% tab 'Scala 3' for=object_5 %} +```scala +object MathConstants: + val PI = 3.14159 + val E = 2.71828 +println(MathConstants.PI) // 3.14159 +``` +{% endtab %} +{% endtabs %} ## Companion objects @@ -267,6 +413,25 @@ A companion class or object can access the private members of its companion. Companion objects are used for methods and values that are not specific to instances of the companion class. For instance, in the following example the class `Circle` has a member named `area` which is specific to each instance, and its companion object has a method named `calculateArea` that’s (a) not specific to an instance, and (b) is available to every instance: +{% tabs companion class=tabs-scala-version %} +{% tab 'Scala 2' for=companion %} +```scala +import scala.math._ + +case class Circle(radius: Double) { + def area: Double = Circle.calculateArea(radius) +} + +object Circle { + private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0) +} + +val circle1 = Circle(5.0) +circle1.area +``` +{% endtab %} + +{% tab 'Scala 3' for=companion %} ```scala import scala.math.* @@ -279,6 +444,8 @@ object Circle: val circle1 = Circle(5.0) circle1.area ``` +{% endtab %} +{% endtabs %} In this example the `area` method that’s available to each instance uses the `calculateArea` method that’s defined in the companion object. Once again, `calculateArea` is similar to a static method in Java. @@ -296,6 +463,41 @@ Companion objects can be used for several purposes: Here’s a quick look at how `apply` methods can be used as factory methods to create new objects: +{% tabs companion-use class=tabs-scala-version %} +{% tab 'Scala 2' for=companion-use %} +```scala +class Person { + var name = "" + var age = 0 + override def toString = s"$name is $age years old" +} + +object Person { + // a one-arg factory method + def apply(name: String): Person = { + var p = new Person + p.name = name + p + } + + // a two-arg factory method + def apply(name: String, age: Int): Person = { + var p = new Person + p.name = name + p.age = age + p + } +} + +val joe = Person("Joe") +val fred = Person("Fred", 29) + +//val joe: Person = Joe is 0 years old +//val fred: Person = Fred is 29 years old +``` +{% endtab %} + +{% tab 'Scala 3' for=companion-use %} ```scala class Person: var name = "" @@ -325,11 +527,11 @@ val fred = Person("Fred", 29) //val joe: Person = Joe is 0 years old //val fred: Person = Fred is 29 years old ``` +{% endtab %} +{% endtabs %} The `unapply` method isn’t covered here, but it’s covered in the [Reference documentation][unapply]. - - ## Traits If you’re familiar with Java, a Scala trait is similar to an interface in Java 8+. Traits can contain: @@ -339,6 +541,18 @@ If you’re familiar with Java, a Scala trait is similar to an interface in Java In a basic use, a trait can be used as an interface, defining only abstract members that will be implemented by other classes: +{% tabs traits_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=traits_1 %} +```scala +trait Employee { + def id: Int + def firstName: String + def lastName: String +} +``` +{% endtab %} + +{% tab 'Scala 3' for=traits_1 %} ```scala trait Employee: def id: Int @@ -348,27 +562,68 @@ trait Employee: However, traits can also contain concrete members. For instance, the following trait defines two abstract members---`numLegs` and `walk()`---and also has a concrete implementation of a `stop()` method: +{% tabs traits_2 class=tabs-scala-version %} +{% tab 'Scala 2' for=traits_2 %} +```scala +trait HasLegs { + def numLegs: Int + def walk(): Unit + def stop() = println("Stopped walking") +} +``` +{% endtab %} + +{% tab 'Scala 3' for=traits_2 %} ```scala trait HasLegs: def numLegs: Int def walk(): Unit def stop() = println("Stopped walking") ``` +{% endtab %} +{% endtabs %} Here’s another trait with an abstract member and two concrete implementations: +{% tabs traits_3 class=tabs-scala-version %} +{% tab 'Scala 2' for=traits_3 %} +```scala +trait HasTail { + def tailColor: String + def wagTail() = println("Tail is wagging") + def stopTail() = println("Tail is stopped") +} +``` +{% endtab %} + +{% tab 'Scala 3' for=traits_3 %} ```scala trait HasTail: def tailColor: String def wagTail() = println("Tail is wagging") def stopTail() = println("Tail is stopped") ``` +{% endtab %} +{% endtabs %} Notice how each trait only handles very specific attributes and behaviors: `HasLegs` deals only with legs, and `HasTail` deals only with tail-related functionality. Traits let you build small modules like this. Later in your code, classes can mix multiple traits to build larger components: +{% tabs traits_4 class=tabs-scala-version %} +{% tab 'Scala 2' for=traits_4 %} +```scala +class IrishSetter(name: String) extends HasLegs, HasTail { + val numLegs = 4 + val tailColor = "Red" + def walk() = println("I’m walking") + override def toString = s"$name is a Dog" +} +``` +{% endtab %} + +{% tab 'Scala 3' for=traits_4 %} ```scala class IrishSetter(name: String) extends HasLegs, HasTail: val numLegs = 4 @@ -376,19 +631,23 @@ class IrishSetter(name: String) extends HasLegs, HasTail: def walk() = println("I’m walking") override def toString = s"$name is a Dog" ``` +{% endtab %} +{% endtabs %} Notice that the `IrishSetter` class implements the abstract members that are defined in `HasLegs` and `HasTail`. Now you can create new `IrishSetter` instances: +{% tabs traits_5 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=traits_5 %} ```scala val d = IrishSetter("Big Red") // "Big Red is a Dog" ``` +{% endtab %} +{% endtabs %} This is just a taste of what you can accomplish with traits. For more details, see the remainder of these modeling lessons. - - ## Abstract classes {% comment %} @@ -410,6 +669,24 @@ In most situations you’ll use traits, but historically there have been two sit Prior to Scala 3, when a base class needed to take constructor arguments, you’d declare it as an `abstract class`: +{% tabs abstract_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=abstract_1 %} +```scala +abstract class Pet(name: String) { + def greeting: String + def age: Int + override def toString = s"My name is $name, I say $greeting, and I’m $age" +} + +class Dog(name: String, var age: Int) extends Pet(name) { + val greeting = "Woof" +} + +val d = Dog("Fido", 1) +``` +{% endtab %} + +{% tab 'Scala 3' for=abstract_1 %} ```scala abstract class Pet(name: String): def greeting: String @@ -421,9 +698,29 @@ class Dog(name: String, var age: Int) extends Pet(name): val d = Dog("Fido", 1) ``` +{% endtab %} +{% endtabs %} However, with Scala 3, traits can now have [parameters][trait-params], so you can now use traits in the same situation: +{% tabs abstract_2 class=tabs-scala-version %} +{% tab 'Scala 2' for=abstract_2 %} +```scala +trait Pet(name: String) { + def greeting: String + def age: Int + override def toString = s"My name is $name, I say $greeting, and I’m $age" +} + +class Dog(name: String, var age: Int) extends Pet(name) { + val greeting = "Woof" +} + +val d = Dog("ssssssss +``` +{% endtab %} + +{% tab 'Scala 3' for=abstract_2 %} ```scala trait Pet(name: String): def greeting: String @@ -438,15 +735,15 @@ val d = Dog("Fido", 1) Traits are more flexible to compose---you can mix in multiple traits, but only extend one class---and should be preferred to classes and abstract classes most of the time. The rule of thumb is to use classes whenever you want to create instances of a particular type, and traits when you want to decompose and reuse behaviour. - ## Enums An enumeration can be used to define a type that consists of a finite set of named values (in the section on [FP modeling][fp-modeling], we will see that enums are much more flexible than this). Basic enumerations are used to define sets of constants, like the months in a year, the days in a week, directions like north/south/east/west, and more. - As an example, these enumerations define sets of attributes related to pizzas: +{% tabs enum_1 class=tabs-scala-version %} +{% tab 'Scala 3 only' for=enum_1 %} ```scala enum CrustSize: case Small, Medium, Large @@ -457,41 +754,57 @@ enum CrustType: enum Topping: case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions ``` +{% endtab %} +{% endtabs %} To use them in other code, first import them, and then use them: +{% tabs enum_2 class=tabs-scala-version %} +{% tab 'Scala 3 only' for=enum_2 %} ```scala import CrustSize.* val currentCrustSize = Small ``` +{% endtab %} +{% endtabs %} -Enum values can be compared using equals (`==`), and also matched on: +Enum values can be compared using equals (`==`), and also enum_1ed on: +{% tabs enum_3 class=tabs-scala-version %} +{% tab 'Scala 3 only' for=enum_3 %} ```scala // if/then if (currentCrustSize == Large) println("You get a prize!") -// match -currentCrustSize match +// enum_1 +currentCrustSize enum_1 case Small => println("small") case Medium => println("medium") case Large => println("large") ``` +{% endtab %} +{% endtabs %} ### Additional Enum Features Enumerations can also be parameterized: +{% tabs enum_4 class=tabs-scala-version %} +{% tab 'Scala 3 only' for=enum_4 %} ```scala enum Color(val rgb: Int): case Red extends Color(0xFF0000) case Green extends Color(0x00FF00) case Blue extends Color(0x0000FF) ``` +{% endtab %} +{% endtabs %} And they can also have members (like fields and methods): +{% tabs enum_5 class=tabs-scala-version %} +{% tab 'Scala 3 only' for=enum_5 %} ```scala enum Planet(mass: Double, radius: Double): private final val G = 6.67300E-11 @@ -503,14 +816,20 @@ enum Planet(mass: Double, radius: Double): case Earth extends Planet(5.976e+24, 6.37814e6) // more planets here ... ``` +{% endtab %} +{% endtabs %} ### Compatibility with Java Enums If you want to use Scala-defined enums as Java enums, you can do so by extending the class `java.lang.Enum` (which is imported by default) as follows: +{% tabs enum_6 class=tabs-scala-version %} +{% tab 'Scala 3 only' for=enum_6 %} ```scala enum Color extends Enum[Color] { case Red, Green, Blue } ``` +{% endtab %} +{% endtabs %} The type parameter comes from the Java `enum` definition, and should be the same as the type of the enum. There’s no need to provide constructor arguments (as defined in the Java API docs) to `java.lang.Enum` when extending it---the compiler generates them automatically. @@ -524,34 +843,55 @@ val res0: Int = -1 The section on [algebraic datatypes][adts] and the [reference documentation][ref-enums] cover enumerations in more detail. - ## Case classes Case classes are used to model immutable data structures. Take the following example: -```scala + +{% tabs case-classes_1 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=case-classes_1 %} +```scala: case class Person(name: String, relation: String) ``` +{% endtab %} +{% endtabs %} + Since we declare `Person` as a case class, the fields `name` and `relation` are public and immutable by default. We can create instances of case classes as follows: + +{% tabs case-classes_2 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=case-classes_2 %} ```scala val christina = Person("Christina", "niece") ``` +{% endtab %} +{% endtabs %} + Note that the fields can’t be mutated: + +{% tabs case-classes_3 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=case-classes_3 %} ```scala christina.name = "Fred" // error: reassignment to val ``` +{% endtab %} +{% endtabs %} + Since the fields of a case class are assumed to be immutable, the Scala compiler can generate many helpful methods for you: -* An `unapply` method is generated, which allows you to perform pattern matching on a case class (that is, `case Person(n, r) => ...`). +* An `unapply` method is generated, which allows you to perform pattern case-classes_1ing on a case class (that is, `case Person(n, r) => ...`). * A `copy` method is generated in the class, which is very useful to create modified copies of an instance. * `equals` and `hashCode` methods using structural equality are generated, allowing you to use instances of case classes in `Map`s. * A default `toString` method is generated, which is helpful for debugging. These additional features are demonstrated in the below example: + +{% tabs case-classes_4 class=tabs-scala-version %} +{% tab 'Scala 2' for=case-classes_4 %} ```scala // Case classes can be used as patterns -christina match +christina match { case Person(n, r) => println("name is " + n) +} // `equals` and `hashCode` methods generated for you val hannah = Person("Hannah", "niece") @@ -566,8 +906,32 @@ val cubs1908 = BaseballTeam("Chicago Cubs", 1908) val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016) // result: // cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016) + ``` +{% endtab %} +{% tab 'Scala 3' for=case-classes_4 %} +```scala +// Case classes can be used as patterns +christina match: + case Person(n, r) => println("name is " + n) + +// `equals` and `hashCode` methods generated for you +val hannah = Person("Hannah", "niece") +christina == hannah // false + +// `toString` method +println(christina) // Person(Christina,niece) + +// built-in `copy` method +case class BaseballTeam(name: String, lastWorldSeriesWin: Int) +val cubs1908 = BaseballTeam("Chicago Cubs", 1908) +val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016) +// result: +// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016) +``` +{% endtab %} +{% endtabs %} ### Support for functional programming @@ -578,8 +942,7 @@ As mentioned, case classes support functional programming (FP): Since instances of case classes can’t be changed, they can easily be shared without fearing mutation or race conditions. - Instead of mutating an instance, you can use the `copy` method as a template to create a new (potentially changed) instance. This process can be referred to as “update as you copy.” -- Having an `unapply` method auto-generated for you also lets case classes be used in advanced ways with pattern matching. - +- Having an `unapply` method auto-generated for you also lets case classes be used in advanced ways with pattern case-classes_1ing. {% comment %} NOTE: We can use this following text, if desired. If it’s used, it needs to be updated a little bit. @@ -590,20 +953,49 @@ A great thing about a case class is that it automatically generates an `unapply` To demonstrate this, imagine that you have this trait: +{% tabs case-classes_5 class=tabs-scala-version %} +{% tab 'Scala 2' for=case-classes_5 %} +```scala +trait Person { + def name: String +} +``` +{% endtab %} + +{% tab 'Scala 3' for=case-classes_5 %} ```scala trait Person: def name: String ``` +{% endtab %} +{% endtabs %} Then, create these case classes to extend that trait: +{% tabs case-classes_6 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=case-classes_6 %} ```scala case class Student(name: String, year: Int) extends Person case class Teacher(name: String, specialty: String) extends Person ``` +{% endtab %} +{% endtabs %} + +Because those are defined as case classes---and they have built-in `unapply` methods---you can write a case-classes_1 expression like this: -Because those are defined as case classes---and they have built-in `unapply` methods---you can write a match expression like this: +{% tabs case-classes_7 class=tabs-scala-version %} +{% tab 'Scala 2' for=case-classes_7 %} +```scala +def getPrintableString(p: Person): String = p match { + case Student(name, year) => + s"$name is a student in Year $year." + case Teacher(name, whatTheyTeach) => + s"$name teaches $whatTheyTeach." +} +``` +{% endtab %} +{% tab 'Scala 3' for=case-classes_7 %} ```scala def getPrintableString(p: Person): String = p match case Student(name, year) => @@ -611,29 +1003,41 @@ def getPrintableString(p: Person): String = p match case Teacher(name, whatTheyTeach) => s"$name teaches $whatTheyTeach." ``` +{% endtab %} +{% endtabs %} Notice these two patterns in the `case` statements: +{% tabs case-classes_8 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=case-classes_8 %} ```scala case Student(name, year) => case Teacher(name, whatTheyTeach) => ``` +{% endtab %} +{% endtabs %} Those patterns work because `Student` and `Teacher` are defined as case classes that have `unapply` methods whose type signature conforms to a certain standard. -Technically, the specific type of pattern matching shown in these examples is known as a _constructor pattern_. +Technically, the specific type of pattern case-classes_1ing shown in these examples is known as a _constructor pattern_. > The Scala standard is that an `unapply` method returns the case class constructor fields in a tuple that’s wrapped in an `Option`. > The “tuple” part of the solution was shown in the previous lesson. To show how that code works, create an instance of `Student` and `Teacher`: +{% tabs case-classes_9 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=case-classes_9 %} ```scala val s = Student("Al", 1) val t = Teacher("Bob Donnan", "Mathematics") ``` +{% endtab %} +{% endtabs %} Next, this is what the output looks like in the REPL when you call `getPrintableString` with those two instances: +{% tabs case-classes_10 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=case-classes_10 %} ```scala scala> getPrintableString(s) res0: String = Al is a student in Year 1. @@ -641,34 +1045,62 @@ res0: String = Al is a student in Year 1. scala> getPrintableString(t) res1: String = Bob Donnan teaches Mathematics. ``` +{% endtab %} +{% endtabs %} > All of this content on `unapply` methods and extractors is a little advanced for an introductory book like this, but because case classes are an important FP topic, it seems better to cover them, rather than skipping over them. -#### Add pattern matching to any type with unapply +#### Add pattern case-classes_1ing to any type with unapply -A great Scala feature is that you can add pattern matching to any type by writing your own `unapply` method. +A great Scala feature is that you can add pattern case-classes_1ing to any type by writing your own `unapply` method. As an example, this class defines an `unapply` method in its companion object: +{% tabs case-classes_11 class=tabs-scala-version %} +{% tab 'Scala 2' for=case-classes_11 %} +```scala +class Person(var name: String, var age: Int) +object Person { + def unapply(p: Person): Tuple2[String, Int] = (p.name, p.age) +} +``` +{% endtab %} + +{% tab 'Scala 3' for=case-classes_11 %} ```scala class Person(var name: String, var age: Int) object Person: def unapply(p: Person): Tuple2[String, Int] = (p.name, p.age) ``` +{% endtab %} +{% endtabs %} -Because it defines an `unapply` method, and because that method returns a tuple, you can now use `Person` with a `match` expression: +Because it defines an `unapply` method, and because that method returns a tuple, you can now use `Person` with a `case-classes_1` expression: +{% tabs case-classes_12 class=tabs-scala-version %} +{% tab 'Scala 2' for=case-classes_12 %} ```scala val p = Person("Astrid", 33) -p match +p match { case Person(n,a) => println(s"name: $n, age: $a") case null => println("No match") +} // that code prints: "name: Astrid, age: 33" ``` -{% endcomment %} +{% endtab %} + +{% tab 'Scala 3' for=case-classes_12 %} +```scala +val p = Person("Astrid", 33) +p match + case Person(n,a) => println(s"name: $n, age: $a") + case null => println("No match") +// that code prints: "name: Astrid, age: 33" +``` +{% endcomment %} ## Case objects @@ -678,6 +1110,8 @@ They’re particularly useful whenever you need a singleton object that needs a Case objects are useful when you need to pass immutable messages around. For instance, if you’re working on a music player project, you’ll create a set of commands or messages like this: +{% tabs case-objects_1 class=tabs-scala-version %} +{% tab 'Scala 2 and 3' for=case-objects_1 %} ```scala sealed trait Message case class PlaySong(name: String) extends Message @@ -685,9 +1119,24 @@ case class IncreaseVolume(amount: Int) extends Message case class DecreaseVolume(amount: Int) extends Message case object StopPlaying extends Message ``` +{% endtab %} +{% endtabs %} -Then in other parts of your code, you can write methods like this, which use pattern matching to handle the incoming message (assuming the methods `playSong`, `changeVolume`, and `stopPlayingSong` are defined somewhere else): +Then in other parts of your code, you can write methods like this, which use pattern case-objects_1ing to handle the incoming message (assuming the methods `playSong`, `changeVolume`, and `stopPlayingSong` are defined somewhere else): +{% tabs case-objects_2 class=tabs-scala-version %} +{% tab 'Scala 2' for=case-objects_2 %} +```scala +def handleMessages(message: Message): Unit = message match { + case PlaySong(name) => playSong(name) + case IncreaseVolume(amount) => changeVolume(amount) + case DecreaseVolume(amount) => changeVolume(-amount) + case StopPlaying => stopPlayingSong() +} +``` +{% endtab %} + +{% tab 'Scala 3' for=case-objects_2 %} ```scala def handleMessages(message: Message): Unit = message match case PlaySong(name) => playSong(name) @@ -695,6 +1144,9 @@ def handleMessages(message: Message): Unit = message match case DecreaseVolume(amount) => changeVolume(-amount) case StopPlaying => stopPlayingSong() ``` +{% endtab %} +{% endtabs %} + [ref-enums]: {{ site.scala3ref }}/enums/enums.html [adts]: {% link _overviews/scala3-book/types-adts-gadts.md %} [fp-modeling]: {% link _overviews/scala3-book/domain-modeling-fp.md %} From 85fcaa62ca0e44396d8c9095a064d657d70dbe23 Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Fri, 7 Oct 2022 00:39:02 +0800 Subject: [PATCH 02/10] correct endcomment lab. --- _overviews/scala3-book/domain-modeling-tools.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 378ccfe909..26e5a0619d 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -1100,7 +1100,8 @@ p match // that code prints: "name: Astrid, age: 33" ``` -{% endcomment %} +{% endtab %} +{% endtabs %} ## Case objects From e80a3f93e8af484667a45bb62b8fc73963813053 Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Fri, 7 Oct 2022 00:47:53 +0800 Subject: [PATCH 03/10] correct comments again. --- _overviews/scala3-book/domain-modeling-tools.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 26e5a0619d..22facb7047 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -1103,6 +1103,8 @@ p match {% endtab %} {% endtabs %} +{% endcomment %} + ## Case objects Case objects are to objects what case classes are to classes: they provide a number of automatically-generated methods to make them more powerful. From 5a15b47773770c3e586f84176cf4ae28fe6008ac Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Fri, 7 Oct 2022 01:03:42 +0800 Subject: [PATCH 04/10] correct endtabs. --- _overviews/scala3-book/domain-modeling-tools.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 22facb7047..c91bad3b3e 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -148,7 +148,15 @@ Classes can also extend traits and abstract classes, which we cover in dedicated As a quick look at a few other features, class constructor parameters can also have default values: {% tabs default-values_1 class=tabs-scala-version %} -{% tab 'Scala 2 and 3' for=default-values_1 %} +{% tab 'Scala 2' for=default-values_1 %} +```scala +class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) { + override def toString = s"timeout: $timeout, linger: $linger" +} +``` +{% endtab %} + +{% tab 'Scala 3' for=default-values_1 %} ```scala class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): override def toString = s"timeout: $timeout, linger: $linger" @@ -559,6 +567,9 @@ trait Employee: def firstName: String def lastName: String ``` +{% endtab %} +{% endtabs %} + However, traits can also contain concrete members. For instance, the following trait defines two abstract members---`numLegs` and `walk()`---and also has a concrete implementation of a `stop()` method: From 01566256d3ff227a0a506d8c018c96c7104bb76a Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Fri, 7 Oct 2022 15:07:31 +0800 Subject: [PATCH 05/10] corrected tab errors. --- .../scala3-book/domain-modeling-tools.md | 145 +++++++++++++++++- 1 file changed, 140 insertions(+), 5 deletions(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index c91bad3b3e..97ce5776d1 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -29,11 +29,13 @@ Here are some examples of classes: {% tabs class_1 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=class_1 %} + ```scala class Person(var name: String, var vocation: String) class Book(var title: String, var author: String, var year: Int) class Movie(var name: String, var director: String, var year: Int) ``` + {% endtab %} {% endtabs %} @@ -46,10 +48,12 @@ Prior to Scala 3, you used the `new` keyword to create a new instance of a class {% tabs class_2 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=class_2 %} + ```scala val p = new Person("Robert Allen Zimmerman", "Harmonica Player") // --- ``` + {% endtab %} {% endtabs %} @@ -57,9 +61,11 @@ However, with [creator applications][creator] this isn’t required in Scala 3: {% tabs class_3 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=class_3 %} + ```scala val p = Person("Robert Allen Zimmerman", "Harmonica Player") ``` + {% endtab %} {% endtabs %} @@ -67,10 +73,12 @@ Once you have an instance of a class such as `p`, you can access its fields, whi {% tabs class_4 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=class_4 %} + ```scala p.name // "Robert Allen Zimmerman" p.vocation // "Harmonica Player" ``` + {% endtab %} {% endtabs %} @@ -78,10 +86,12 @@ As mentioned, all of these parameters were created as `var` fields, so you can a {% tabs class_5 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=class_5 %} + ```scala p.name = "Bob Dylan" p.vocation = "Musician" ``` + {% endtab %} {% endtabs %} @@ -93,6 +103,7 @@ The body is initialized as part of the default constructor: {% tabs method class=tabs-scala-version %} {% tab 'Scala 2' for=method %} + ```scala class Person(var firstName: String, var lastName: String) { @@ -108,9 +119,11 @@ class Person(var firstName: String, var lastName: String) { println("initialization ends") } ``` + {% endtab %} {% tab 'Scala 3' for=method %} + ```scala class Person(var firstName: String, var lastName: String): @@ -125,6 +138,7 @@ class Person(var firstName: String, var lastName: String): printFullName println("initialization ends") ``` + {% endtab %} {% endtabs %} @@ -149,18 +163,22 @@ As a quick look at a few other features, class constructor parameters can also h {% tabs default-values_1 class=tabs-scala-version %} {% tab 'Scala 2' for=default-values_1 %} + ```scala class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) { override def toString = s"timeout: $timeout, linger: $linger" } ``` + {% endtab %} {% tab 'Scala 3' for=default-values_1 %} + ```scala class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): override def toString = s"timeout: $timeout, linger: $linger" ``` + {% endtab %} {% endtabs %} @@ -168,6 +186,7 @@ A great thing about this feature is that it lets consumers of your code create c {% tabs default-values_2 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=default-values_2 %} + ```scala val s = Socket() // timeout: 5000, linger: 5000 val s = Socket(2_500) // timeout: 2500, linger: 5000 @@ -175,6 +194,7 @@ val s = Socket(10_000, 10_000) // timeout: 10000, linger: 10000 val s = Socket(timeout = 10_000) // timeout: 10000, linger: 5000 val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000 ``` + {% endtab %} {% endtabs %} @@ -183,6 +203,7 @@ This is particularly helpful when many of the parameters have the same type, as {% tabs default-values_3 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=default-values_3 %} + ```scala // option 1 val s = Socket(10_000, 10_000) @@ -193,6 +214,7 @@ val s = Socket( linger = 10_000 ) ``` + {% endtab %} {% endtabs %} @@ -210,6 +232,7 @@ One way to handle this situation in an OOP style is with this code: {% tabs structor_1 class=tabs-scala-version %} {% tab 'Scala 2' for=structor_1 %} + ```scala import java.time._ @@ -244,9 +267,11 @@ class Student( } } ``` + {% endtab %} {% tab 'Scala 3' for=structor_1 %} + ```scala import java.time.* @@ -278,6 +303,7 @@ class Student( this(name, govtId) _studentId = studentId ``` + {% endtab %} {% endtabs %} @@ -301,11 +327,13 @@ Those constructors can be called like this: {% tabs structor_2 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=structor_2 %} + ```scala val s1 = Student("Mary", "123") val s2 = Student("Mary", "123", LocalDate.now) val s3 = Student("Mary", "123", 456) ``` + {% endtab %} {% endtabs %} @@ -323,6 +351,7 @@ Here’s an example of a “string utilities” object that contains a set of me {% tabs object_1 class=tabs-scala-version %} {% tab 'Scala 2' for=object_1 %} + ```scala object StringUtils { def truncate(s: String, length: Int): String = s.take(length) @@ -330,15 +359,18 @@ object StringUtils { def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty } ``` + {% endtab %} {% tab 'Scala 3' for=object_1 %} + ```scala object StringUtils: def truncate(s: String, length: Int): String = s.take(length) def containsWhitespace(s: String): Boolean = s.object_1es(".*\\s.*") def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty ``` + {% endtab %} {% endtabs %} @@ -346,9 +378,11 @@ We can use the object as follows: {% tabs object_2 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=object_2 %} + ```scala StringUtils.truncate("Chuck Bartowski", 5) // "Chuck" ``` + {% endtab %} {% endtabs %} @@ -356,21 +390,25 @@ Importing in Scala is very flexible, and allows us to import _all_ members of an {% tabs object_3 class=tabs-scala-version %} {% tab 'Scala 2' for=object_3 %} + ```scala import StringUtils._ truncate("Chuck Bartowski", 5) // "Chuck" containsWhitespace("Sarah Walker") // true isNullOrEmpty("John Casey") // false ``` + {% endtab %} {% tab 'Scala 3' for=object_3 %} + ```scala import StringUtils.* truncate("Chuck Bartowski", 5) // "Chuck" containsWhitespace("Sarah Walker") // true isNullOrEmpty("John Casey") // false ``` + {% endtab %} {% endtabs %} @@ -378,12 +416,14 @@ or just _some_ members: {% tabs object_4 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=object_4 %} + ```scala import StringUtils.{truncate, containsWhitespace} truncate("Charles Carmichael", 7) // "Charles" containsWhitespace("Captain Awesome") // true isNullOrEmpty("Morgan Grimes") // Not found: isNullOrEmpty (error) ``` + {% endtab %} {% endtabs %} @@ -391,6 +431,7 @@ Objects can also contain fields, which are also accessed like static members: {% tabs object_5 class=tabs-scala-version %} {% tab 'Scala 2' for=object_5 %} + ```scala object MathConstants { val PI = 3.14159 @@ -399,9 +440,11 @@ object MathConstants { println(MathConstants.PI) // 3.14159 ``` + {% endtab %} {% tab 'Scala 3' for=object_5 %} + ```scala object MathConstants: val PI = 3.14159 @@ -409,6 +452,7 @@ object MathConstants: println(MathConstants.PI) // 3.14159 ``` + {% endtab %} {% endtabs %} @@ -423,6 +467,7 @@ For instance, in the following example the class `Circle` has a member named `ar {% tabs companion class=tabs-scala-version %} {% tab 'Scala 2' for=companion %} + ```scala import scala.math._ @@ -437,9 +482,11 @@ object Circle { val circle1 = Circle(5.0) circle1.area ``` + {% endtab %} {% tab 'Scala 3' for=companion %} + ```scala import scala.math.* @@ -452,6 +499,7 @@ object Circle: val circle1 = Circle(5.0) circle1.area ``` + {% endtab %} {% endtabs %} @@ -473,6 +521,7 @@ Here’s a quick look at how `apply` methods can be used as factory methods to c {% tabs companion-use class=tabs-scala-version %} {% tab 'Scala 2' for=companion-use %} + ```scala class Person { var name = "" @@ -503,9 +552,11 @@ val fred = Person("Fred", 29) //val joe: Person = Joe is 0 years old //val fred: Person = Fred is 29 years old ``` + {% endtab %} {% tab 'Scala 3' for=companion-use %} + ```scala class Person: var name = "" @@ -535,6 +586,7 @@ val fred = Person("Fred", 29) //val joe: Person = Joe is 0 years old //val fred: Person = Fred is 29 years old ``` + {% endtab %} {% endtabs %} @@ -551,6 +603,7 @@ In a basic use, a trait can be used as an interface, defining only abstract memb {% tabs traits_1 class=tabs-scala-version %} {% tab 'Scala 2' for=traits_1 %} + ```scala trait Employee { def id: Int @@ -558,15 +611,18 @@ trait Employee { def lastName: String } ``` + {% endtab %} {% tab 'Scala 3' for=traits_1 %} + ```scala trait Employee: def id: Int def firstName: String def lastName: String ``` + {% endtab %} {% endtabs %} @@ -575,6 +631,7 @@ For instance, the following trait defines two abstract members---`numLegs` and ` {% tabs traits_2 class=tabs-scala-version %} {% tab 'Scala 2' for=traits_2 %} + ```scala trait HasLegs { def numLegs: Int @@ -582,15 +639,18 @@ trait HasLegs { def stop() = println("Stopped walking") } ``` + {% endtab %} {% tab 'Scala 3' for=traits_2 %} + ```scala trait HasLegs: def numLegs: Int def walk(): Unit def stop() = println("Stopped walking") ``` + {% endtab %} {% endtabs %} @@ -598,6 +658,7 @@ Here’s another trait with an abstract member and two concrete implementations: {% tabs traits_3 class=tabs-scala-version %} {% tab 'Scala 2' for=traits_3 %} + ```scala trait HasTail { def tailColor: String @@ -605,15 +666,18 @@ trait HasTail { def stopTail() = println("Tail is stopped") } ``` + {% endtab %} {% tab 'Scala 3' for=traits_3 %} + ```scala trait HasTail: def tailColor: String def wagTail() = println("Tail is wagging") def stopTail() = println("Tail is stopped") ``` + {% endtab %} {% endtabs %} @@ -624,6 +688,7 @@ Later in your code, classes can mix multiple traits to build larger components: {% tabs traits_4 class=tabs-scala-version %} {% tab 'Scala 2' for=traits_4 %} + ```scala class IrishSetter(name: String) extends HasLegs, HasTail { val numLegs = 4 @@ -632,9 +697,11 @@ class IrishSetter(name: String) extends HasLegs, HasTail { override def toString = s"$name is a Dog" } ``` + {% endtab %} {% tab 'Scala 3' for=traits_4 %} + ```scala class IrishSetter(name: String) extends HasLegs, HasTail: val numLegs = 4 @@ -642,6 +709,7 @@ class IrishSetter(name: String) extends HasLegs, HasTail: def walk() = println("I’m walking") override def toString = s"$name is a Dog" ``` + {% endtab %} {% endtabs %} @@ -650,9 +718,11 @@ Now you can create new `IrishSetter` instances: {% tabs traits_5 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=traits_5 %} + ```scala val d = IrishSetter("Big Red") // "Big Red is a Dog" ``` + {% endtab %} {% endtabs %} @@ -663,6 +733,7 @@ For more details, see the remainder of these modeling lessons. {% comment %} LATER: If anyone wants to update this section, our comments about abstract classes and traits are on Slack. The biggest points seem to be: + - The `super` of a trait is dynamic - At the use site, people can mix in traits but not classes - It remains easier to extend a class than a trait from Java, if the trait has at least a field @@ -682,6 +753,7 @@ Prior to Scala 3, when a base class needed to take constructor arguments, you’ {% tabs abstract_1 class=tabs-scala-version %} {% tab 'Scala 2' for=abstract_1 %} + ```scala abstract class Pet(name: String) { def greeting: String @@ -695,9 +767,11 @@ class Dog(name: String, var age: Int) extends Pet(name) { val d = Dog("Fido", 1) ``` + {% endtab %} {% tab 'Scala 3' for=abstract_1 %} + ```scala abstract class Pet(name: String): def greeting: String @@ -709,6 +783,7 @@ class Dog(name: String, var age: Int) extends Pet(name): val d = Dog("Fido", 1) ``` + {% endtab %} {% endtabs %} @@ -716,6 +791,7 @@ However, with Scala 3, traits can now have [parameters][trait-params], so you ca {% tabs abstract_2 class=tabs-scala-version %} {% tab 'Scala 2' for=abstract_2 %} + ```scala trait Pet(name: String) { def greeting: String @@ -729,9 +805,11 @@ class Dog(name: String, var age: Int) extends Pet(name) { val d = Dog("ssssssss ``` + {% endtab %} {% tab 'Scala 3' for=abstract_2 %} + ```scala trait Pet(name: String): def greeting: String @@ -743,6 +821,10 @@ class Dog(name: String, var age: Int) extends Pet(name): val d = Dog("Fido", 1) ``` + +{% endtab %} +{% endtabs %} + Traits are more flexible to compose---you can mix in multiple traits, but only extend one class---and should be preferred to classes and abstract classes most of the time. The rule of thumb is to use classes whenever you want to create instances of a particular type, and traits when you want to decompose and reuse behaviour. @@ -755,6 +837,7 @@ As an example, these enumerations define sets of attributes related to pizzas: {% tabs enum_1 class=tabs-scala-version %} {% tab 'Scala 3 only' for=enum_1 %} + ```scala enum CrustSize: case Small, Medium, Large @@ -765,6 +848,7 @@ enum CrustType: enum Topping: case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions ``` + {% endtab %} {% endtabs %} @@ -772,10 +856,12 @@ To use them in other code, first import them, and then use them: {% tabs enum_2 class=tabs-scala-version %} {% tab 'Scala 3 only' for=enum_2 %} + ```scala import CrustSize.* val currentCrustSize = Small ``` + {% endtab %} {% endtabs %} @@ -783,6 +869,7 @@ Enum values can be compared using equals (`==`), and also enum_1ed on: {% tabs enum_3 class=tabs-scala-version %} {% tab 'Scala 3 only' for=enum_3 %} + ```scala // if/then if (currentCrustSize == Large) @@ -794,6 +881,7 @@ currentCrustSize enum_1 case Medium => println("medium") case Large => println("large") ``` + {% endtab %} {% endtabs %} @@ -803,12 +891,14 @@ Enumerations can also be parameterized: {% tabs enum_4 class=tabs-scala-version %} {% tab 'Scala 3 only' for=enum_4 %} + ```scala enum Color(val rgb: Int): case Red extends Color(0xFF0000) case Green extends Color(0x00FF00) case Blue extends Color(0x0000FF) ``` + {% endtab %} {% endtabs %} @@ -816,6 +906,7 @@ And they can also have members (like fields and methods): {% tabs enum_5 class=tabs-scala-version %} {% tab 'Scala 3 only' for=enum_5 %} + ```scala enum Planet(mass: Double, radius: Double): private final val G = 6.67300E-11 @@ -827,6 +918,7 @@ enum Planet(mass: Double, radius: Double): case Earth extends Planet(5.976e+24, 6.37814e6) // more planets here ... ``` + {% endtab %} {% endtabs %} @@ -836,9 +928,11 @@ If you want to use Scala-defined enums as Java enums, you can do so by extending {% tabs enum_6 class=tabs-scala-version %} {% tab 'Scala 3 only' for=enum_6 %} + ```scala enum Color extends Enum[Color] { case Red, Green, Blue } ``` + {% endtab %} {% endtabs %} @@ -861,9 +955,11 @@ Take the following example: {% tabs case-classes_1 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=case-classes_1 %} + ```scala: case class Person(name: String, relation: String) ``` + {% endtab %} {% endtabs %} @@ -872,9 +968,11 @@ We can create instances of case classes as follows: {% tabs case-classes_2 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=case-classes_2 %} + ```scala val christina = Person("Christina", "niece") ``` + {% endtab %} {% endtabs %} @@ -882,22 +980,26 @@ Note that the fields can’t be mutated: {% tabs case-classes_3 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=case-classes_3 %} + ```scala christina.name = "Fred" // error: reassignment to val ``` + {% endtab %} {% endtabs %} Since the fields of a case class are assumed to be immutable, the Scala compiler can generate many helpful methods for you: -* An `unapply` method is generated, which allows you to perform pattern case-classes_1ing on a case class (that is, `case Person(n, r) => ...`). -* A `copy` method is generated in the class, which is very useful to create modified copies of an instance. -* `equals` and `hashCode` methods using structural equality are generated, allowing you to use instances of case classes in `Map`s. -* A default `toString` method is generated, which is helpful for debugging. + +- An `unapply` method is generated, which allows you to perform pattern case-classes_1ing on a case class (that is, `case Person(n, r) => ...`). +- A `copy` method is generated in the class, which is very useful to create modified copies of an instance. +- `equals` and `hashCode` methods using structural equality are generated, allowing you to use instances of case classes in `Map`s. +- A default `toString` method is generated, which is helpful for debugging. These additional features are demonstrated in the below example: {% tabs case-classes_4 class=tabs-scala-version %} {% tab 'Scala 2' for=case-classes_4 %} + ```scala // Case classes can be used as patterns christina match { @@ -919,9 +1021,11 @@ val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016) // cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016) ``` + {% endtab %} {% tab 'Scala 3' for=case-classes_4 %} + ```scala // Case classes can be used as patterns christina match: @@ -941,6 +1045,7 @@ val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016) // result: // cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016) ``` + {% endtab %} {% endtabs %} @@ -966,18 +1071,22 @@ To demonstrate this, imagine that you have this trait: {% tabs case-classes_5 class=tabs-scala-version %} {% tab 'Scala 2' for=case-classes_5 %} + ```scala trait Person { def name: String } ``` + {% endtab %} {% tab 'Scala 3' for=case-classes_5 %} + ```scala trait Person: def name: String ``` + {% endtab %} {% endtabs %} @@ -985,10 +1094,12 @@ Then, create these case classes to extend that trait: {% tabs case-classes_6 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=case-classes_6 %} + ```scala case class Student(name: String, year: Int) extends Person case class Teacher(name: String, specialty: String) extends Person ``` + {% endtab %} {% endtabs %} @@ -996,6 +1107,7 @@ Because those are defined as case classes---and they have built-in `unapply` met {% tabs case-classes_7 class=tabs-scala-version %} {% tab 'Scala 2' for=case-classes_7 %} + ```scala def getPrintableString(p: Person): String = p match { case Student(name, year) => @@ -1004,9 +1116,11 @@ def getPrintableString(p: Person): String = p match { s"$name teaches $whatTheyTeach." } ``` + {% endtab %} {% tab 'Scala 3' for=case-classes_7 %} + ```scala def getPrintableString(p: Person): String = p match case Student(name, year) => @@ -1014,6 +1128,7 @@ def getPrintableString(p: Person): String = p match case Teacher(name, whatTheyTeach) => s"$name teaches $whatTheyTeach." ``` + {% endtab %} {% endtabs %} @@ -1021,10 +1136,12 @@ Notice these two patterns in the `case` statements: {% tabs case-classes_8 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=case-classes_8 %} + ```scala case Student(name, year) => case Teacher(name, whatTheyTeach) => ``` + {% endtab %} {% endtabs %} @@ -1038,10 +1155,12 @@ To show how that code works, create an instance of `Student` and `Teacher`: {% tabs case-classes_9 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=case-classes_9 %} + ```scala val s = Student("Al", 1) val t = Teacher("Bob Donnan", "Mathematics") ``` + {% endtab %} {% endtabs %} @@ -1049,6 +1168,7 @@ Next, this is what the output looks like in the REPL when you call `getPrintable {% tabs case-classes_10 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=case-classes_10 %} + ```scala scala> getPrintableString(s) res0: String = Al is a student in Year 1. @@ -1056,6 +1176,7 @@ res0: String = Al is a student in Year 1. scala> getPrintableString(t) res1: String = Bob Donnan teaches Mathematics. ``` + {% endtab %} {% endtabs %} @@ -1068,20 +1189,24 @@ As an example, this class defines an `unapply` method in its companion object: {% tabs case-classes_11 class=tabs-scala-version %} {% tab 'Scala 2' for=case-classes_11 %} + ```scala class Person(var name: String, var age: Int) object Person { def unapply(p: Person): Tuple2[String, Int] = (p.name, p.age) } ``` + {% endtab %} {% tab 'Scala 3' for=case-classes_11 %} + ```scala class Person(var name: String, var age: Int) object Person: def unapply(p: Person): Tuple2[String, Int] = (p.name, p.age) ``` + {% endtab %} {% endtabs %} @@ -1089,6 +1214,7 @@ Because it defines an `unapply` method, and because that method returns a tuple, {% tabs case-classes_12 class=tabs-scala-version %} {% tab 'Scala 2' for=case-classes_12 %} + ```scala val p = Person("Astrid", 33) @@ -1099,9 +1225,11 @@ p match { // that code prints: "name: Astrid, age: 33" ``` + {% endtab %} {% tab 'Scala 3' for=case-classes_12 %} + ```scala val p = Person("Astrid", 33) @@ -1111,6 +1239,7 @@ p match // that code prints: "name: Astrid, age: 33" ``` + {% endtab %} {% endtabs %} @@ -1126,6 +1255,7 @@ For instance, if you’re working on a music player project, you’ll create a s {% tabs case-objects_1 class=tabs-scala-version %} {% tab 'Scala 2 and 3' for=case-objects_1 %} + ```scala sealed trait Message case class PlaySong(name: String) extends Message @@ -1133,6 +1263,7 @@ case class IncreaseVolume(amount: Int) extends Message case class DecreaseVolume(amount: Int) extends Message case object StopPlaying extends Message ``` + {% endtab %} {% endtabs %} @@ -1140,6 +1271,7 @@ Then in other parts of your code, you can write methods like this, which use pat {% tabs case-objects_2 class=tabs-scala-version %} {% tab 'Scala 2' for=case-objects_2 %} + ```scala def handleMessages(message: Message): Unit = message match { case PlaySong(name) => playSong(name) @@ -1148,9 +1280,11 @@ def handleMessages(message: Message): Unit = message match { case StopPlaying => stopPlayingSong() } ``` + {% endtab %} {% tab 'Scala 3' for=case-objects_2 %} + ```scala def handleMessages(message: Message): Unit = message match case PlaySong(name) => playSong(name) @@ -1158,6 +1292,7 @@ def handleMessages(message: Message): Unit = message match case DecreaseVolume(amount) => changeVolume(-amount) case StopPlaying => stopPlayingSong() ``` + {% endtab %} {% endtabs %} @@ -1166,4 +1301,4 @@ def handleMessages(message: Message): Unit = message match [fp-modeling]: {% link _overviews/scala3-book/domain-modeling-fp.md %} [creator]: {{ site.scala3ref }}/other-new-features/creator-applications.html [unapply]: {{ site.scala3ref }}/changed-features/pattern-matching.html -[trait-params]: {{ site.scala3ref }}/other-new-features/trait-parameters.html +[trait-params]: {{ site.scala3ref }}/other-new-features/trait-parameters.html \ No newline at end of file From 27087517f00bd62d7eadefa53dc2a7ba5380d62c Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Fri, 7 Oct 2022 23:00:54 +0800 Subject: [PATCH 06/10] Update _overviews/scala3-book/domain-modeling-tools.md Co-authored-by: Jamie Thompson --- _overviews/scala3-book/domain-modeling-tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 97ce5776d1..f6438e5665 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -27,7 +27,7 @@ This section briefly introduces each of these language features. As with other languages, a _class_ in Scala is a template for the creation of object instances. Here are some examples of classes: -{% tabs class_1 class=tabs-scala-version %} +{% tabs class_1 %} {% tab 'Scala 2 and 3' for=class_1 %} ```scala From 110900f3c21a85b1346a3cef8a40d4afaf3c7d2f Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Fri, 7 Oct 2022 23:17:05 +0800 Subject: [PATCH 07/10] correct 2&3 and 3 only. --- .../scala3-book/domain-modeling-tools.md | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index f6438e5665..3b86f1a47f 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -46,7 +46,7 @@ If you want them to be immutable---read only---create them as `val` fields inste Prior to Scala 3, you used the `new` keyword to create a new instance of a class: -{% tabs class_2 class=tabs-scala-version %} +{% tabs class_2 %} {% tab 'Scala 2 and 3' for=class_2 %} ```scala @@ -59,7 +59,7 @@ val p = new Person("Robert Allen Zimmerman", "Harmonica Player") However, with [creator applications][creator] this isn’t required in Scala 3: -{% tabs class_3 class=tabs-scala-version %} +{% tabs class_3 %} {% tab 'Scala 2 and 3' for=class_3 %} ```scala @@ -71,7 +71,7 @@ val p = Person("Robert Allen Zimmerman", "Harmonica Player") Once you have an instance of a class such as `p`, you can access its fields, which in this example are all constructor parameters: -{% tabs class_4 class=tabs-scala-version %} +{% tabs class_4 %} {% tab 'Scala 2 and 3' for=class_4 %} ```scala @@ -84,7 +84,7 @@ p.vocation // "Harmonica Player" As mentioned, all of these parameters were created as `var` fields, so you can also mutate them: -{% tabs class_5 class=tabs-scala-version %} +{% tabs class_5 %} {% tab 'Scala 2 and 3' for=class_5 %} ```scala @@ -184,7 +184,7 @@ class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): A great thing about this feature is that it lets consumers of your code create classes in a variety of different ways, as though the class had alternate constructors: -{% tabs default-values_2 class=tabs-scala-version %} +{% tabs default-values_2 %} {% tab 'Scala 2 and 3' for=default-values_2 %} ```scala @@ -201,7 +201,7 @@ val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000 When creating a new instance of a class, you can also use named parameters. This is particularly helpful when many of the parameters have the same type, as shown in this comparison: -{% tabs default-values_3 class=tabs-scala-version %} +{% tabs default-values_3 %} {% tab 'Scala 2 and 3' for=default-values_3 %} ```scala @@ -325,7 +325,7 @@ The class has three constructors, given by the numbered comments in the code: Those constructors can be called like this: -{% tabs structor_2 class=tabs-scala-version %} +{% tabs structor_2 %} {% tab 'Scala 2 and 3' for=structor_2 %} ```scala @@ -376,7 +376,7 @@ object StringUtils: We can use the object as follows: -{% tabs object_2 class=tabs-scala-version %} +{% tabs object_2 %} {% tab 'Scala 2 and 3' for=object_2 %} ```scala @@ -414,7 +414,7 @@ isNullOrEmpty("John Casey") // false or just _some_ members: -{% tabs object_4 class=tabs-scala-version %} +{% tabs object_4 %} {% tab 'Scala 2 and 3' for=object_4 %} ```scala @@ -716,7 +716,7 @@ class IrishSetter(name: String) extends HasLegs, HasTail: Notice that the `IrishSetter` class implements the abstract members that are defined in `HasLegs` and `HasTail`. Now you can create new `IrishSetter` instances: -{% tabs traits_5 class=tabs-scala-version %} +{% tabs traits_5 %} {% tab 'Scala 2 and 3' for=traits_5 %} ```scala @@ -835,7 +835,7 @@ Basic enumerations are used to define sets of constants, like the months in a ye As an example, these enumerations define sets of attributes related to pizzas: -{% tabs enum_1 class=tabs-scala-version %} +{% tabs enum_1 %} {% tab 'Scala 3 only' for=enum_1 %} ```scala @@ -854,7 +854,7 @@ enum Topping: To use them in other code, first import them, and then use them: -{% tabs enum_2 class=tabs-scala-version %} +{% tabs enum_2 %} {% tab 'Scala 3 only' for=enum_2 %} ```scala @@ -867,7 +867,7 @@ val currentCrustSize = Small Enum values can be compared using equals (`==`), and also enum_1ed on: -{% tabs enum_3 class=tabs-scala-version %} +{% tabs enum_3 %} {% tab 'Scala 3 only' for=enum_3 %} ```scala @@ -889,7 +889,7 @@ currentCrustSize enum_1 Enumerations can also be parameterized: -{% tabs enum_4 class=tabs-scala-version %} +{% tabs enum_4 %} {% tab 'Scala 3 only' for=enum_4 %} ```scala @@ -904,7 +904,7 @@ enum Color(val rgb: Int): And they can also have members (like fields and methods): -{% tabs enum_5 class=tabs-scala-version %} +{% tabs enum_5 %} {% tab 'Scala 3 only' for=enum_5 %} ```scala @@ -926,7 +926,7 @@ enum Planet(mass: Double, radius: Double): If you want to use Scala-defined enums as Java enums, you can do so by extending the class `java.lang.Enum` (which is imported by default) as follows: -{% tabs enum_6 class=tabs-scala-version %} +{% tabs enum_6 %} {% tab 'Scala 3 only' for=enum_6 %} ```scala @@ -953,7 +953,7 @@ The section on [algebraic datatypes][adts] and the [reference documentation][ref Case classes are used to model immutable data structures. Take the following example: -{% tabs case-classes_1 class=tabs-scala-version %} +{% tabs case-classes_1 %} {% tab 'Scala 2 and 3' for=case-classes_1 %} ```scala: @@ -966,7 +966,7 @@ case class Person(name: String, relation: String) Since we declare `Person` as a case class, the fields `name` and `relation` are public and immutable by default. We can create instances of case classes as follows: -{% tabs case-classes_2 class=tabs-scala-version %} +{% tabs case-classes_2 %} {% tab 'Scala 2 and 3' for=case-classes_2 %} ```scala @@ -978,7 +978,7 @@ val christina = Person("Christina", "niece") Note that the fields can’t be mutated: -{% tabs case-classes_3 class=tabs-scala-version %} +{% tabs case-classes_3 %} {% tab 'Scala 2 and 3' for=case-classes_3 %} ```scala @@ -1092,7 +1092,7 @@ trait Person: Then, create these case classes to extend that trait: -{% tabs case-classes_6 class=tabs-scala-version %} +{% tabs case-classes_6 %} {% tab 'Scala 2 and 3' for=case-classes_6 %} ```scala @@ -1134,7 +1134,7 @@ def getPrintableString(p: Person): String = p match Notice these two patterns in the `case` statements: -{% tabs case-classes_8 class=tabs-scala-version %} +{% tabs case-classes_8 %} {% tab 'Scala 2 and 3' for=case-classes_8 %} ```scala @@ -1153,7 +1153,7 @@ Technically, the specific type of pattern case-classes_1ing shown in these examp To show how that code works, create an instance of `Student` and `Teacher`: -{% tabs case-classes_9 class=tabs-scala-version %} +{% tabs case-classes_9 %} {% tab 'Scala 2 and 3' for=case-classes_9 %} ```scala @@ -1166,7 +1166,7 @@ val t = Teacher("Bob Donnan", "Mathematics") Next, this is what the output looks like in the REPL when you call `getPrintableString` with those two instances: -{% tabs case-classes_10 class=tabs-scala-version %} +{% tabs case-classes_10 %} {% tab 'Scala 2 and 3' for=case-classes_10 %} ```scala @@ -1253,7 +1253,7 @@ They’re particularly useful whenever you need a singleton object that needs a Case objects are useful when you need to pass immutable messages around. For instance, if you’re working on a music player project, you’ll create a set of commands or messages like this: -{% tabs case-objects_1 class=tabs-scala-version %} +{% tabs case-objects_1 %} {% tab 'Scala 2 and 3' for=case-objects_1 %} ```scala @@ -1301,4 +1301,4 @@ def handleMessages(message: Message): Unit = message match [fp-modeling]: {% link _overviews/scala3-book/domain-modeling-fp.md %} [creator]: {{ site.scala3ref }}/other-new-features/creator-applications.html [unapply]: {{ site.scala3ref }}/changed-features/pattern-matching.html -[trait-params]: {{ site.scala3ref }}/other-new-features/trait-parameters.html \ No newline at end of file +[trait-params]: {{ site.scala3ref }}/other-new-features/trait-parameters.html From fa03a8189ea8c646a230c3928bbf01258d0b72dd Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Sat, 15 Oct 2022 02:07:34 +0800 Subject: [PATCH 08/10] Update _overviews/scala3-book/domain-modeling-tools.md Co-authored-by: Jamie Thompson --- _overviews/scala3-book/domain-modeling-tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 3b86f1a47f..68094e942d 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -60,7 +60,7 @@ val p = new Person("Robert Allen Zimmerman", "Harmonica Player") However, with [creator applications][creator] this isn’t required in Scala 3: {% tabs class_3 %} -{% tab 'Scala 2 and 3' for=class_3 %} +{% tab 'Scala 3 Only' for=class_3 %} ```scala val p = Person("Robert Allen Zimmerman", "Harmonica Player") From 684dcb90d0342faf79712b6f2fff20a93e00274b Mon Sep 17 00:00:00 2001 From: Ben Luo Date: Sat, 15 Oct 2022 02:30:50 +0800 Subject: [PATCH 09/10] correct scala2 code. change to Scala 3 "Only". --- .../scala3-book/domain-modeling-tools.md | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 68094e942d..61d442e283 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -325,8 +325,18 @@ The class has three constructors, given by the numbered comments in the code: Those constructors can be called like this: -{% tabs structor_2 %} -{% tab 'Scala 2 and 3' for=structor_2 %} +{% tabs structor_2 class=tabs-scala-version %} +{% tab 'Scala 2' for=structor_2 %} + +```scala +val s1 = new Student("Mary", "123") +val s2 = new Student("Mary", "123", LocalDate.now) +val s3 = new Student("Mary", "123", 456) +``` + +{% endtab %} + +{% tab 'Scala 3' for=structor_2 %} ```scala val s1 = Student("Mary", "123") @@ -836,7 +846,7 @@ Basic enumerations are used to define sets of constants, like the months in a ye As an example, these enumerations define sets of attributes related to pizzas: {% tabs enum_1 %} -{% tab 'Scala 3 only' for=enum_1 %} +{% tab 'Scala 3 Only' for=enum_1 %} ```scala enum CrustSize: @@ -855,7 +865,7 @@ enum Topping: To use them in other code, first import them, and then use them: {% tabs enum_2 %} -{% tab 'Scala 3 only' for=enum_2 %} +{% tab 'Scala 3 Only' for=enum_2 %} ```scala import CrustSize.* @@ -868,7 +878,7 @@ val currentCrustSize = Small Enum values can be compared using equals (`==`), and also enum_1ed on: {% tabs enum_3 %} -{% tab 'Scala 3 only' for=enum_3 %} +{% tab 'Scala 3 Only' for=enum_3 %} ```scala // if/then @@ -890,7 +900,7 @@ currentCrustSize enum_1 Enumerations can also be parameterized: {% tabs enum_4 %} -{% tab 'Scala 3 only' for=enum_4 %} +{% tab 'Scala 3 Only' for=enum_4 %} ```scala enum Color(val rgb: Int): @@ -905,7 +915,7 @@ enum Color(val rgb: Int): And they can also have members (like fields and methods): {% tabs enum_5 %} -{% tab 'Scala 3 only' for=enum_5 %} +{% tab 'Scala 3 Only' for=enum_5 %} ```scala enum Planet(mass: Double, radius: Double): @@ -927,7 +937,7 @@ enum Planet(mass: Double, radius: Double): If you want to use Scala-defined enums as Java enums, you can do so by extending the class `java.lang.Enum` (which is imported by default) as follows: {% tabs enum_6 %} -{% tab 'Scala 3 only' for=enum_6 %} +{% tab 'Scala 3 Only' for=enum_6 %} ```scala enum Color extends Enum[Color] { case Red, Green, Blue } From 1b215deb142e8e639e63efdb2d2aba4112ec472c Mon Sep 17 00:00:00 2001 From: Jamie Thompson Date: Tue, 18 Oct 2022 17:52:55 +0200 Subject: [PATCH 10/10] adjust formatting --- .../scala3-book/domain-modeling-tools.md | 259 ++++++++++-------- 1 file changed, 148 insertions(+), 111 deletions(-) diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 61d442e283..2c485e3a5f 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -9,7 +9,7 @@ next-page: domain-modeling-oop --- -Scala 3 provides many different constructs so we can model the world around us: +Scala provides many different constructs so we can model the world around us: - Classes - Objects @@ -17,6 +17,7 @@ Scala 3 provides many different constructs so we can model the world around us: - Traits - Abstract classes - Enums +Scala 3 only - Case classes - Case objects @@ -28,7 +29,7 @@ As with other languages, a _class_ in Scala is a template for the creation of ob Here are some examples of classes: {% tabs class_1 %} -{% tab 'Scala 2 and 3' for=class_1 %} +{% tab 'Scala 2 and 3' %} ```scala class Person(var name: String, var vocation: String) @@ -47,7 +48,7 @@ If you want them to be immutable---read only---create them as `val` fields inste Prior to Scala 3, you used the `new` keyword to create a new instance of a class: {% tabs class_2 %} -{% tab 'Scala 2 and 3' for=class_2 %} +{% tab 'Scala 2 and 3' %} ```scala val p = new Person("Robert Allen Zimmerman", "Harmonica Player") @@ -57,10 +58,11 @@ val p = new Person("Robert Allen Zimmerman", "Harmonica Player") {% endtab %} {% endtabs %} -However, with [creator applications][creator] this isn’t required in Scala 3: +However, with [universal apply methods][creator] this isn’t required in Scala 3: +Scala 3 only {% tabs class_3 %} -{% tab 'Scala 3 Only' for=class_3 %} +{% tab 'Scala 3 Only' %} ```scala val p = Person("Robert Allen Zimmerman", "Harmonica Player") @@ -72,7 +74,7 @@ val p = Person("Robert Allen Zimmerman", "Harmonica Player") Once you have an instance of a class such as `p`, you can access its fields, which in this example are all constructor parameters: {% tabs class_4 %} -{% tab 'Scala 2 and 3' for=class_4 %} +{% tab 'Scala 2 and 3' %} ```scala p.name // "Robert Allen Zimmerman" @@ -85,7 +87,7 @@ p.vocation // "Harmonica Player" As mentioned, all of these parameters were created as `var` fields, so you can also mutate them: {% tabs class_5 %} -{% tab 'Scala 2 and 3' for=class_5 %} +{% tab 'Scala 2 and 3' %} ```scala p.name = "Bob Dylan" @@ -102,7 +104,7 @@ They are defined in the body of the class. The body is initialized as part of the default constructor: {% tabs method class=tabs-scala-version %} -{% tab 'Scala 2' for=method %} +{% tab 'Scala 2' %} ```scala class Person(var firstName: String, var lastName: String) { @@ -122,7 +124,7 @@ class Person(var firstName: String, var lastName: String) { {% endtab %} -{% tab 'Scala 3' for=method %} +{% tab 'Scala 3' %} ```scala class Person(var firstName: String, var lastName: String): @@ -144,7 +146,21 @@ class Person(var firstName: String, var lastName: String): The following REPL session shows how to create a new `Person` instance with this class: +{% tabs demo-person class=tabs-scala-version %} +{% tab 'Scala 2' %} +````scala +scala> val john = new Person("John", "Doe") +initialization begins +John Doe +initialization ends +val john: Person = Person@55d8f6bb + +scala> john.printFullName +John Doe ```` +{% endtab %} +{% tab 'Scala 3' %} +````scala scala> val john = Person("John", "Doe") initialization begins John Doe @@ -154,6 +170,8 @@ val john: Person = Person@55d8f6bb scala> john.printFullName John Doe ```` +{% endtab %} +{% endtabs %} Classes can also extend traits and abstract classes, which we cover in dedicated sections below. @@ -162,7 +180,7 @@ Classes can also extend traits and abstract classes, which we cover in dedicated As a quick look at a few other features, class constructor parameters can also have default values: {% tabs default-values_1 class=tabs-scala-version %} -{% tab 'Scala 2' for=default-values_1 %} +{% tab 'Scala 2' %} ```scala class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) { @@ -172,7 +190,7 @@ class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) { {% endtab %} -{% tab 'Scala 3' for=default-values_1 %} +{% tab 'Scala 3' %} ```scala class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): @@ -184,8 +202,19 @@ class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): A great thing about this feature is that it lets consumers of your code create classes in a variety of different ways, as though the class had alternate constructors: -{% tabs default-values_2 %} -{% tab 'Scala 2 and 3' for=default-values_2 %} +{% tabs default-values_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val s = new Socket() // timeout: 5000, linger: 5000 +val s = new Socket(2_500) // timeout: 2500, linger: 5000 +val s = new Socket(10_000, 10_000) // timeout: 10000, linger: 10000 +val s = new Socket(timeout = 10_000) // timeout: 10000, linger: 5000 +val s = new Socket(linger = 10_000) // timeout: 5000, linger: 10000 +``` + +{% endtab %} +{% tab 'Scala 3' %} ```scala val s = Socket() // timeout: 5000, linger: 5000 @@ -201,8 +230,22 @@ val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000 When creating a new instance of a class, you can also use named parameters. This is particularly helpful when many of the parameters have the same type, as shown in this comparison: -{% tabs default-values_3 %} -{% tab 'Scala 2 and 3' for=default-values_3 %} +{% tabs default-values_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +// option 1 +val s = new Socket(10_000, 10_000) + +// option 2 +val s = new Socket( + timeout = 10_000, + linger = 10_000 +) +``` + +{% endtab %} +{% tab 'Scala 3' %} ```scala // option 1 @@ -231,7 +274,7 @@ While analyzing the requirements you’ve seen that you need to be able to const One way to handle this situation in an OOP style is with this code: {% tabs structor_1 class=tabs-scala-version %} -{% tab 'Scala 2' for=structor_1 %} +{% tab 'Scala 2' %} ```scala import java.time._ @@ -254,7 +297,7 @@ class Student( this(name, govtId) _applicationDate = Some(applicationDate) } - + // [3] a constructor for when the student is approved // and now has a student id def this( @@ -270,7 +313,7 @@ class Student( {% endtab %} -{% tab 'Scala 3' for=structor_1 %} +{% tab 'Scala 3' %} ```scala import java.time.* @@ -326,7 +369,7 @@ The class has three constructors, given by the numbered comments in the code: Those constructors can be called like this: {% tabs structor_2 class=tabs-scala-version %} -{% tab 'Scala 2' for=structor_2 %} +{% tab 'Scala 2' %} ```scala val s1 = new Student("Mary", "123") @@ -336,7 +379,7 @@ val s3 = new Student("Mary", "123", 456) {% endtab %} -{% tab 'Scala 3' for=structor_2 %} +{% tab 'Scala 3' %} ```scala val s1 = Student("Mary", "123") @@ -360,7 +403,7 @@ Declaring an `object` is similar to declaring a `class`. Here’s an example of a “string utilities” object that contains a set of methods for working with strings: {% tabs object_1 class=tabs-scala-version %} -{% tab 'Scala 2' for=object_1 %} +{% tab 'Scala 2' %} ```scala object StringUtils { @@ -372,7 +415,7 @@ object StringUtils { {% endtab %} -{% tab 'Scala 3' for=object_1 %} +{% tab 'Scala 3' %} ```scala object StringUtils: @@ -387,7 +430,7 @@ object StringUtils: We can use the object as follows: {% tabs object_2 %} -{% tab 'Scala 2 and 3' for=object_2 %} +{% tab 'Scala 2 and 3' %} ```scala StringUtils.truncate("Chuck Bartowski", 5) // "Chuck" @@ -399,7 +442,7 @@ StringUtils.truncate("Chuck Bartowski", 5) // "Chuck" Importing in Scala is very flexible, and allows us to import _all_ members of an object: {% tabs object_3 class=tabs-scala-version %} -{% tab 'Scala 2' for=object_3 %} +{% tab 'Scala 2' %} ```scala import StringUtils._ @@ -410,7 +453,7 @@ isNullOrEmpty("John Casey") // false {% endtab %} -{% tab 'Scala 3' for=object_3 %} +{% tab 'Scala 3' %} ```scala import StringUtils.* @@ -425,7 +468,7 @@ isNullOrEmpty("John Casey") // false or just _some_ members: {% tabs object_4 %} -{% tab 'Scala 2 and 3' for=object_4 %} +{% tab 'Scala 2 and 3' %} ```scala import StringUtils.{truncate, containsWhitespace} @@ -440,7 +483,7 @@ isNullOrEmpty("Morgan Grimes") // Not found: isNullOrEmpty (error) Objects can also contain fields, which are also accessed like static members: {% tabs object_5 class=tabs-scala-version %} -{% tab 'Scala 2' for=object_5 %} +{% tab 'Scala 2' %} ```scala object MathConstants { @@ -453,7 +496,7 @@ println(MathConstants.PI) // 3.14159 {% endtab %} -{% tab 'Scala 3' for=object_5 %} +{% tab 'Scala 3' %} ```scala object MathConstants: @@ -476,12 +519,12 @@ Companion objects are used for methods and values that are not specific to insta For instance, in the following example the class `Circle` has a member named `area` which is specific to each instance, and its companion object has a method named `calculateArea` that’s (a) not specific to an instance, and (b) is available to every instance: {% tabs companion class=tabs-scala-version %} -{% tab 'Scala 2' for=companion %} +{% tab 'Scala 2' %} ```scala import scala.math._ -case class Circle(radius: Double) { +class Circle(val radius: Double) { def area: Double = Circle.calculateArea(radius) } @@ -489,18 +532,18 @@ object Circle { private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0) } -val circle1 = Circle(5.0) +val circle1 = new Circle(5.0) circle1.area ``` {% endtab %} -{% tab 'Scala 3' for=companion %} +{% tab 'Scala 3' %} ```scala import scala.math.* -case class Circle(radius: Double): +class Circle(val radius: Double): def area: Double = Circle.calculateArea(radius) object Circle: @@ -530,7 +573,7 @@ Companion objects can be used for several purposes: Here’s a quick look at how `apply` methods can be used as factory methods to create new objects: {% tabs companion-use class=tabs-scala-version %} -{% tab 'Scala 2' for=companion-use %} +{% tab 'Scala 2' %} ```scala class Person { @@ -546,7 +589,7 @@ object Person { p.name = name p } - + // a two-arg factory method def apply(name: String, age: Int): Person = { var p = new Person @@ -563,9 +606,11 @@ val fred = Person("Fred", 29) //val fred: Person = Fred is 29 years old ``` +The `unapply` method isn’t covered here, but it’s covered in the [Language Specification](https://scala-lang.org/files/archive/spec/2.13/08-pattern-matching.html#extractor-patterns). + {% endtab %} -{% tab 'Scala 3' for=companion-use %} +{% tab 'Scala 3' %} ```scala class Person: @@ -597,11 +642,11 @@ val fred = Person("Fred", 29) //val fred: Person = Fred is 29 years old ``` +The `unapply` method isn’t covered here, but it’s covered in the [Reference documentation]({{ site.scala3ref }}/changed-features/pattern-matching.html). + {% endtab %} {% endtabs %} -The `unapply` method isn’t covered here, but it’s covered in the [Reference documentation][unapply]. - ## Traits If you’re familiar with Java, a Scala trait is similar to an interface in Java 8+. Traits can contain: @@ -612,7 +657,7 @@ If you’re familiar with Java, a Scala trait is similar to an interface in Java In a basic use, a trait can be used as an interface, defining only abstract members that will be implemented by other classes: {% tabs traits_1 class=tabs-scala-version %} -{% tab 'Scala 2' for=traits_1 %} +{% tab 'Scala 2' %} ```scala trait Employee { @@ -624,7 +669,7 @@ trait Employee { {% endtab %} -{% tab 'Scala 3' for=traits_1 %} +{% tab 'Scala 3' %} ```scala trait Employee: @@ -640,7 +685,7 @@ However, traits can also contain concrete members. For instance, the following trait defines two abstract members---`numLegs` and `walk()`---and also has a concrete implementation of a `stop()` method: {% tabs traits_2 class=tabs-scala-version %} -{% tab 'Scala 2' for=traits_2 %} +{% tab 'Scala 2' %} ```scala trait HasLegs { @@ -652,7 +697,7 @@ trait HasLegs { {% endtab %} -{% tab 'Scala 3' for=traits_2 %} +{% tab 'Scala 3' %} ```scala trait HasLegs: @@ -667,7 +712,7 @@ trait HasLegs: Here’s another trait with an abstract member and two concrete implementations: {% tabs traits_3 class=tabs-scala-version %} -{% tab 'Scala 2' for=traits_3 %} +{% tab 'Scala 2' %} ```scala trait HasTail { @@ -679,7 +724,7 @@ trait HasTail { {% endtab %} -{% tab 'Scala 3' for=traits_3 %} +{% tab 'Scala 3' %} ```scala trait HasTail: @@ -697,10 +742,10 @@ Traits let you build small modules like this. Later in your code, classes can mix multiple traits to build larger components: {% tabs traits_4 class=tabs-scala-version %} -{% tab 'Scala 2' for=traits_4 %} +{% tab 'Scala 2' %} ```scala -class IrishSetter(name: String) extends HasLegs, HasTail { +class IrishSetter(name: String) extends HasLegs with HasTail { val numLegs = 4 val tailColor = "Red" def walk() = println("I’m walking") @@ -710,7 +755,7 @@ class IrishSetter(name: String) extends HasLegs, HasTail { {% endtab %} -{% tab 'Scala 3' for=traits_4 %} +{% tab 'Scala 3' %} ```scala class IrishSetter(name: String) extends HasLegs, HasTail: @@ -726,8 +771,15 @@ class IrishSetter(name: String) extends HasLegs, HasTail: Notice that the `IrishSetter` class implements the abstract members that are defined in `HasLegs` and `HasTail`. Now you can create new `IrishSetter` instances: -{% tabs traits_5 %} -{% tab 'Scala 2 and 3' for=traits_5 %} +{% tabs traits_5 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val d = new IrishSetter("Big Red") // "Big Red is a Dog" +``` + +{% endtab %} +{% tab 'Scala 3' %} ```scala val d = IrishSetter("Big Red") // "Big Red is a Dog" @@ -762,7 +814,7 @@ In most situations you’ll use traits, but historically there have been two sit Prior to Scala 3, when a base class needed to take constructor arguments, you’d declare it as an `abstract class`: {% tabs abstract_1 class=tabs-scala-version %} -{% tab 'Scala 2' for=abstract_1 %} +{% tab 'Scala 2' %} ```scala abstract class Pet(name: String) { @@ -775,12 +827,12 @@ class Dog(name: String, var age: Int) extends Pet(name) { val greeting = "Woof" } -val d = Dog("Fido", 1) +val d = new Dog("Fido", 1) ``` {% endtab %} -{% tab 'Scala 3' for=abstract_1 %} +{% tab 'Scala 3' %} ```scala abstract class Pet(name: String): @@ -797,28 +849,13 @@ val d = Dog("Fido", 1) {% endtab %} {% endtabs %} -However, with Scala 3, traits can now have [parameters][trait-params], so you can now use traits in the same situation: - -{% tabs abstract_2 class=tabs-scala-version %} -{% tab 'Scala 2' for=abstract_2 %} +

Trait Parameters Scala 3 only

-```scala -trait Pet(name: String) { - def greeting: String - def age: Int - override def toString = s"My name is $name, I say $greeting, and I’m $age" -} - -class Dog(name: String, var age: Int) extends Pet(name) { - val greeting = "Woof" -} - -val d = Dog("ssssssss -``` +However, with Scala 3, traits can now have [parameters][trait-params], so you can now use traits in the same situation: -{% endtab %} +{% tabs abstract_2 %} -{% tab 'Scala 3' for=abstract_2 %} +{% tab 'Scala 3 Only' %} ```scala trait Pet(name: String): @@ -838,7 +875,7 @@ val d = Dog("Fido", 1) Traits are more flexible to compose---you can mix in multiple traits, but only extend one class---and should be preferred to classes and abstract classes most of the time. The rule of thumb is to use classes whenever you want to create instances of a particular type, and traits when you want to decompose and reuse behaviour. -## Enums +

Enums Scala 3 only

An enumeration can be used to define a type that consists of a finite set of named values (in the section on [FP modeling][fp-modeling], we will see that enums are much more flexible than this). Basic enumerations are used to define sets of constants, like the months in a year, the days in a week, directions like north/south/east/west, and more. @@ -846,7 +883,7 @@ Basic enumerations are used to define sets of constants, like the months in a ye As an example, these enumerations define sets of attributes related to pizzas: {% tabs enum_1 %} -{% tab 'Scala 3 Only' for=enum_1 %} +{% tab 'Scala 3 Only' %} ```scala enum CrustSize: @@ -865,7 +902,7 @@ enum Topping: To use them in other code, first import them, and then use them: {% tabs enum_2 %} -{% tab 'Scala 3 Only' for=enum_2 %} +{% tab 'Scala 3 Only' %} ```scala import CrustSize.* @@ -875,18 +912,18 @@ val currentCrustSize = Small {% endtab %} {% endtabs %} -Enum values can be compared using equals (`==`), and also enum_1ed on: +Enum values can be compared using equals (`==`), and also matched on: {% tabs enum_3 %} -{% tab 'Scala 3 Only' for=enum_3 %} +{% tab 'Scala 3 Only' %} ```scala // if/then -if (currentCrustSize == Large) +if currentCrustSize == Large then println("You get a prize!") -// enum_1 -currentCrustSize enum_1 +// match +currentCrustSize match case Small => println("small") case Medium => println("medium") case Large => println("large") @@ -900,7 +937,7 @@ currentCrustSize enum_1 Enumerations can also be parameterized: {% tabs enum_4 %} -{% tab 'Scala 3 Only' for=enum_4 %} +{% tab 'Scala 3 Only' %} ```scala enum Color(val rgb: Int): @@ -915,7 +952,7 @@ enum Color(val rgb: Int): And they can also have members (like fields and methods): {% tabs enum_5 %} -{% tab 'Scala 3 Only' for=enum_5 %} +{% tab 'Scala 3 Only' %} ```scala enum Planet(mass: Double, radius: Double): @@ -937,7 +974,7 @@ enum Planet(mass: Double, radius: Double): If you want to use Scala-defined enums as Java enums, you can do so by extending the class `java.lang.Enum` (which is imported by default) as follows: {% tabs enum_6 %} -{% tab 'Scala 3 Only' for=enum_6 %} +{% tab 'Scala 3 Only' %} ```scala enum Color extends Enum[Color] { case Red, Green, Blue } @@ -964,7 +1001,7 @@ Case classes are used to model immutable data structures. Take the following example: {% tabs case-classes_1 %} -{% tab 'Scala 2 and 3' for=case-classes_1 %} +{% tab 'Scala 2 and 3' %} ```scala: case class Person(name: String, relation: String) @@ -977,7 +1014,7 @@ Since we declare `Person` as a case class, the fields `name` and `relation` are We can create instances of case classes as follows: {% tabs case-classes_2 %} -{% tab 'Scala 2 and 3' for=case-classes_2 %} +{% tab 'Scala 2 and 3' %} ```scala val christina = Person("Christina", "niece") @@ -989,7 +1026,7 @@ val christina = Person("Christina", "niece") Note that the fields can’t be mutated: {% tabs case-classes_3 %} -{% tab 'Scala 2 and 3' for=case-classes_3 %} +{% tab 'Scala 2 and 3' %} ```scala christina.name = "Fred" // error: reassignment to val @@ -1000,7 +1037,7 @@ christina.name = "Fred" // error: reassignment to val Since the fields of a case class are assumed to be immutable, the Scala compiler can generate many helpful methods for you: -- An `unapply` method is generated, which allows you to perform pattern case-classes_1ing on a case class (that is, `case Person(n, r) => ...`). +- An `unapply` method is generated, which allows you to perform pattern matching on a case class (that is, `case Person(n, r) => ...`). - A `copy` method is generated in the class, which is very useful to create modified copies of an instance. - `equals` and `hashCode` methods using structural equality are generated, allowing you to use instances of case classes in `Map`s. - A default `toString` method is generated, which is helpful for debugging. @@ -1008,7 +1045,7 @@ Since the fields of a case class are assumed to be immutable, the Scala compiler These additional features are demonstrated in the below example: {% tabs case-classes_4 class=tabs-scala-version %} -{% tab 'Scala 2' for=case-classes_4 %} +{% tab 'Scala 2' %} ```scala // Case classes can be used as patterns @@ -1034,11 +1071,11 @@ val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016) {% endtab %} -{% tab 'Scala 3' for=case-classes_4 %} +{% tab 'Scala 3' %} ```scala // Case classes can be used as patterns -christina match: +christina match case Person(n, r) => println("name is " + n) // `equals` and `hashCode` methods generated for you @@ -1068,7 +1105,7 @@ As mentioned, case classes support functional programming (FP): Since instances of case classes can’t be changed, they can easily be shared without fearing mutation or race conditions. - Instead of mutating an instance, you can use the `copy` method as a template to create a new (potentially changed) instance. This process can be referred to as “update as you copy.” -- Having an `unapply` method auto-generated for you also lets case classes be used in advanced ways with pattern case-classes_1ing. +- Having an `unapply` method auto-generated for you also lets case classes be used in advanced ways with pattern matching. {% comment %} NOTE: We can use this following text, if desired. If it’s used, it needs to be updated a little bit. @@ -1080,7 +1117,7 @@ A great thing about a case class is that it automatically generates an `unapply` To demonstrate this, imagine that you have this trait: {% tabs case-classes_5 class=tabs-scala-version %} -{% tab 'Scala 2' for=case-classes_5 %} +{% tab 'Scala 2' %} ```scala trait Person { @@ -1090,7 +1127,7 @@ trait Person { {% endtab %} -{% tab 'Scala 3' for=case-classes_5 %} +{% tab 'Scala 3' %} ```scala trait Person: @@ -1103,7 +1140,7 @@ trait Person: Then, create these case classes to extend that trait: {% tabs case-classes_6 %} -{% tab 'Scala 2 and 3' for=case-classes_6 %} +{% tab 'Scala 2 and 3' %} ```scala case class Student(name: String, year: Int) extends Person @@ -1113,10 +1150,10 @@ case class Teacher(name: String, specialty: String) extends Person {% endtab %} {% endtabs %} -Because those are defined as case classes---and they have built-in `unapply` methods---you can write a case-classes_1 expression like this: +Because those are defined as case classes---and they have built-in `unapply` methods---you can write a match expression like this: {% tabs case-classes_7 class=tabs-scala-version %} -{% tab 'Scala 2' for=case-classes_7 %} +{% tab 'Scala 2' %} ```scala def getPrintableString(p: Person): String = p match { @@ -1129,7 +1166,7 @@ def getPrintableString(p: Person): String = p match { {% endtab %} -{% tab 'Scala 3' for=case-classes_7 %} +{% tab 'Scala 3' %} ```scala def getPrintableString(p: Person): String = p match @@ -1145,7 +1182,7 @@ def getPrintableString(p: Person): String = p match Notice these two patterns in the `case` statements: {% tabs case-classes_8 %} -{% tab 'Scala 2 and 3' for=case-classes_8 %} +{% tab 'Scala 2 and 3' %} ```scala case Student(name, year) => @@ -1156,7 +1193,7 @@ case Teacher(name, whatTheyTeach) => {% endtabs %} Those patterns work because `Student` and `Teacher` are defined as case classes that have `unapply` methods whose type signature conforms to a certain standard. -Technically, the specific type of pattern case-classes_1ing shown in these examples is known as a _constructor pattern_. +Technically, the specific type of pattern matching shown in these examples is known as a _constructor pattern_. > The Scala standard is that an `unapply` method returns the case class constructor fields in a tuple that’s wrapped in an `Option`. > The “tuple” part of the solution was shown in the previous lesson. @@ -1164,7 +1201,7 @@ Technically, the specific type of pattern case-classes_1ing shown in these examp To show how that code works, create an instance of `Student` and `Teacher`: {% tabs case-classes_9 %} -{% tab 'Scala 2 and 3' for=case-classes_9 %} +{% tab 'Scala 2 and 3' %} ```scala val s = Student("Al", 1) @@ -1177,7 +1214,7 @@ val t = Teacher("Bob Donnan", "Mathematics") Next, this is what the output looks like in the REPL when you call `getPrintableString` with those two instances: {% tabs case-classes_10 %} -{% tab 'Scala 2 and 3' for=case-classes_10 %} +{% tab 'Scala 2 and 3' %} ```scala scala> getPrintableString(s) @@ -1192,13 +1229,13 @@ res1: String = Bob Donnan teaches Mathematics. > All of this content on `unapply` methods and extractors is a little advanced for an introductory book like this, but because case classes are an important FP topic, it seems better to cover them, rather than skipping over them. -#### Add pattern case-classes_1ing to any type with unapply +#### Add pattern matching to any type with unapply -A great Scala feature is that you can add pattern case-classes_1ing to any type by writing your own `unapply` method. +A great Scala feature is that you can add pattern matching to any type by writing your own `unapply` method. As an example, this class defines an `unapply` method in its companion object: {% tabs case-classes_11 class=tabs-scala-version %} -{% tab 'Scala 2' for=case-classes_11 %} +{% tab 'Scala 2' %} ```scala class Person(var name: String, var age: Int) @@ -1209,7 +1246,7 @@ object Person { {% endtab %} -{% tab 'Scala 3' for=case-classes_11 %} +{% tab 'Scala 3' %} ```scala class Person(var name: String, var age: Int) @@ -1220,10 +1257,10 @@ object Person: {% endtab %} {% endtabs %} -Because it defines an `unapply` method, and because that method returns a tuple, you can now use `Person` with a `case-classes_1` expression: +Because it defines an `unapply` method, and because that method returns a tuple, you can now use `Person` with a `match` expression: {% tabs case-classes_12 class=tabs-scala-version %} -{% tab 'Scala 2' for=case-classes_12 %} +{% tab 'Scala 2' %} ```scala val p = Person("Astrid", 33) @@ -1238,7 +1275,7 @@ p match { {% endtab %} -{% tab 'Scala 3' for=case-classes_12 %} +{% tab 'Scala 3' %} ```scala val p = Person("Astrid", 33) @@ -1264,7 +1301,7 @@ Case objects are useful when you need to pass immutable messages around. For instance, if you’re working on a music player project, you’ll create a set of commands or messages like this: {% tabs case-objects_1 %} -{% tab 'Scala 2 and 3' for=case-objects_1 %} +{% tab 'Scala 2 and 3' %} ```scala sealed trait Message @@ -1277,10 +1314,10 @@ case object StopPlaying extends Message {% endtab %} {% endtabs %} -Then in other parts of your code, you can write methods like this, which use pattern case-objects_1ing to handle the incoming message (assuming the methods `playSong`, `changeVolume`, and `stopPlayingSong` are defined somewhere else): +Then in other parts of your code, you can write methods like this, which use pattern matching to handle the incoming message (assuming the methods `playSong`, `changeVolume`, and `stopPlayingSong` are defined somewhere else): {% tabs case-objects_2 class=tabs-scala-version %} -{% tab 'Scala 2' for=case-objects_2 %} +{% tab 'Scala 2' %} ```scala def handleMessages(message: Message): Unit = message match { @@ -1293,7 +1330,7 @@ def handleMessages(message: Message): Unit = message match { {% endtab %} -{% tab 'Scala 3' for=case-objects_2 %} +{% tab 'Scala 3' %} ```scala def handleMessages(message: Message): Unit = message match