© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
D. Pollak et al.Beginning Scala 3https://doi.org/10.1007/978-1-4842-7422-4_7

7. Traits and Enums

David Pollak1  , Vishal Layka2 and Andres Sacco3
(1)
San Francisco, CA, USA
(2)
Bruxelles, Belgium
(3)
Ciudad Autonoma de Buenos Aires, Argentina
 

In this chapter, you will learn two of the most useful features of Scala. These features exist in other languages that use the JVM, like Java or Kotlin—not with the same name but with the same concept behind the scenes.

One of them is traits. Traits function as interfaces in the Java universe and help you construct reusable parts of a program and deal with the tribulations of multiple inheritances, sidestepping the disadvantages of single inheritance by means of mixing compositions.

The second concept is enumerations (enums), which help you encapsulate in one place all possible values of a specific type. For example, a level of logging could have five possible values: INFO, WARN, DEBUG, ERROR, and FATAL. Also, enums provide a way to create methods with the idea of interacting with the values.

One thing to mention: These features are not entirely new in the latest version of Scala. Both exist in the previous version with a lot of differences. The most relevant case is enums, which is like a new way to do things with the idea of simplifying the use for the developers.

Traits

A trait provides code reusability in Scala by encapsulating the method and state, and then offering the possibility of mixing them into classes, thus allowing code reuse. In this way, a class can be mixed in with a myriad of traits, unlike inheritance where each class is allowed to inherit from just one superclass. Moreover, other than using the keyword trait , a trait definition resembles a class definition, as shown here:
scala> trait Gliding:
     |     def java () =
     |         println("gliding")
// defined trait Gliding

This trait named Gliding does not declare a superclass, so like a class, it has the default superclass of AnyRef. The Gliding trait is a simple example but adequate way to show how traits work.

The following are some of the things you can do with a trait:
  • Have concrete abstract fields or methods

  • Create a constructor that receives certain parameters

  • Combine or extend different traits

  • Restrict or limit which classes can use it

You can think of a trait as similar to an interface in Java where you can define a certain default behavior. In Java 8 or previous versions, this is not possible to do, and one class can extend from multiple interfaces to have multiple inheritances.

Table 7-1 shows the same block of code in the different languages that use the JVM.
Table 7-1

Different Ways to Express the Idea of a Trait

Language

Example

Java

interface Gliding {    default void gliding() {        System.out.println("gliding");    }}

Kotlin

internal interface Gliding {   fun gliding() {       println("gliding")   }}

Scala

trait Gliding:    def gliding () =        println("gliding")

Everything looks fine but how can you use this trait in a class and invoke one of the methods? This is how:

scala> class ConcreteClass extends Gliding
// defined class ConcreteClass
scala> val instance = ConcreteClass()
val instance: ConcreteClass = ConcreteClass@78e043e4
scala> instance.gliding()
gliding

Using Traits as Mixins

With single inheritance, a class can inherit methods and fields from only one class. Multiple inheritances enable the class to inherit methods and fields from more than one class. However, multiple inheritances can be problematic as the order of inheriting classes may affect the behavior of the subclass inadvertently.

The mixin composition is a better approach for solving the problems of multiple inheritances, sidestepping the drawbacks of single inheritance. In Java, a class can implement any arbitrary number of interfaces toward multiple abstractions. Unlike Java, Scala provides a mechanism for defining and using reusable code in interfaces, which is valid for all classes implementing the interface. You have abstract classes for defining and using such reusable code, but a class can extend only one abstract class, eliminating the possibility of multiple inheritances.

The term mixin is used for such reusable code that could be independently maintained. You can use traits in a way similar to the way in which Java interfaces are used. When you add implementation to traits, they become mixins. You can create a trait that inherits from a class, as well as a class that extends a trait. Once a trait is defined, it can be mixed into a class using the extends keyword . This code shows a class that mixes in the Gliding trait using extends:

scala> class Glider extends Gliding:
|     override def toString = "glider"
// defined class Glider
You can use the extends keyword to mix in a trait; in that case, you implicitly inherit the trait’s superclass. In the following code, the class Glider mixes in Gliding. You can use the methods inherited from a trait as follows:
scala> val glider = Glider()
val glider: Glider = glider
scala> glider.gliding()
gliding
A trait also defines a type. Here’s an example in which Gliding is used as a type:
scala> val g: Glider = glider
g: Glider = glider
scala> g.gliding()
gliding

In the example shown earlier, the type of g is a Glider trait , and so g could be initialized with any object whose class mixes in Glider.

In the following code, class Glider inherits an implementation of gliding from trait Glider. The class Glider could override gliding as illustrated above.

scala> class Glider extends Gliding:
     |     override def toString = "glider"
     |     override def gliding() = println("race for now " + toString)
// defined class Glider
Because Glider overrides Gliding’s implementation of gliding, you get a new behavior when you call it.
scala> glider.gliding()
race for now glider
Fundamentally, traits are akin to Java interfaces. As with interfaces, you can just declare the methods in your trait that you want your extending classes to implement, as shown here:
trait TraitA:
    def methodA(): Unit
    def methodAWithParam(param :String): Unit
    def methodWithReturnType: String

This code shows a trait that declares methods that don’t take any argument. The methods without an argument can be declared with a def keyword followed by the method name, as illustrated in the first method, def methodA. If a method requires parameters, you can list them as usual.

One trait can extend another trait, as shown here:
trait TraitB extends TraitA:
    def methodB(): Unit
When a class extends a trait, use the extends and with keywords based on whether the class extends one trait or several traits. When a class extends one trait, use the extends keyword .
class ClassA extends TraitA:
    // code

If a class extends more than one trait, use extends for the first trait and with to mix in the other traits.

class ClassA extends TraitA with TraitB:
    // code

If a class extends a class and a trait, always use extends before the class name, and use with before the trait’s name.

class ClassA extends ClassB with TraitA with TraitB:
    // code
A class extending the trait must implement all the abstract methods of the trait, unless the class extending the trait is itself abstract.
class ClassA extends TraitA:
    def methodA(): Unit = print("methodA")
    def methodAWithParam(param :String): Unit = print(param)
    def methodWithReturnType: String = "something"

Here ClassA is not declared abstract and therefore it implements all abstract methods of trait TraitA.

Note

A trait can be comprised of both abstract and concrete methods.

However, if a class extends a trait but does not implement the abstract methods defined in the trait, the class extending the trait must be declared abstract.
abstract class ClassA extends TraitA:
    def methodA(): Unit { // code... }
    def methodAWithParam(param :String): Unit{ // code... }

Here ClassA does not implement the methodWithReturnType method of TraitA. The subclass of a trait can choose, if it prefers, to override the trait’s method.

The following code illustrates a Vehicle trait that provides an implementation for the drive method:
trait Vehicle:
    def drive() = println("Driving")
    def race(): String
In the following code, drive is a concrete method and race is an abstract method. The Car class here does not override the drive method of Vehicle trait.
class Car extends Vehicle:
    def race(): String = "Racing the car"
The Boat class in the following code overrides the drive method of the Vehicle trait:
class Boat extends Vehicle:
    override def drive() =  println("float")
    def race() =  "Racing boat."
Note

Although Scala has abstract classes, it’s recommended to use traits instead of abstract classes to implement base behavior because a class can extend only one abstract class, but it can implement multiple traits. If you want the base behavior to be inherited in Java code, use an abstract class.

You can also use fields in your traits. The fields of a trait can be declared as either var or val and can be concrete by defining the field with an initial value or abstract by not assigning an initial value. The following code illustrates a trait named CarTrait with an abstract field door and a concrete field seat:
trait CarTrait:
    var door: Int
    var seat = 4
The next code illustrates a Car class that extends the CarTrait. As you can see, you don’t need to use the override keyword to override var fields door and seat.
class Car extends CarTrait:
    var door = 4
    seat = 5
However, you need to use the override keyword in a subclass of a trait to override a val field.
trait CarTrait:
    val door: Int
class Car extends CarTrait:
    override val door = 5

In the class Car that extends the CarTrait trait, you need to define the values for the abstract fields. Otherwise, you need to define the class as abstract.

As you can see, traits can declare fields and maintain state. The syntaxes of the class definition and the trait definition are exactly the same, except that a class definition can have the parameters passed to the primary constructor of a class but a trait definition cannot have such parameters.
class Car(door: Int )
trait Car (door: Int) // does not compile - Car is already defined as class Car

In the next section, you will explore using traits for modeling complex class hierarchies that you cannot model in Java.

Traits and Class Hierarchies

One of the big challenges with developing a class hierarchy when you are constrained by single inheritance is figuring out what things should be base classes and where things should go in the class hierarchy. If you’re modeling living things, how do you model things with legs when that can include any animal? Should there be LeggedAnimals and LeglessAnimals? But then, how do you deal with Mammals and Reptiles? Maybe you can make HasLegs an interface, but then you can give a Plant a leg. Scala to the rescue.

You’ve already seen that traits can implement methods. Additionally, traits can have rules about what kind of classes and other traits they can be mixed into. Further, you can declare method parameters that are a consolidation of types, such as:
def foo(bar: Baz with Blarg with FruitBat)
Only instances of classes that extend Baz, Blarg, and FruitBat may be passed into this method.
abstract class LivingThing
abstract class Plant extends LivingThing
abstract class Fungus extends LivingThing
abstract class Animal extends LivingThing
Good so far. A LivingThing must be a plant, fungus, or animal. But, what about legs? Who can have legs?
trait HasLegs extends Animal:
  def walk() = println("Walking")
The HasLegs trait extends Animal. But Animal is a class, so what does it mean for a trait to extend a class? It means that the compiler will only let you mix HasLegs into something that subclasses from Animal. Thus, you’ve defined that only animals have legs, but any type of animal can have legs. It’s the same for HasWings.
trait HasWings extends Animal:
  def flap() = println("Flap Flap")
But only things with wings can fly. This is a different notation. You define the rules of the self type with this: HasWings =>. The compiler flags an error if this trait is not mixed into a class that also extends HasWings. So, you can use self types to define the rules for what classes a given trait can be mixed into.1
trait Flies:
  this: HasWings => def fly()  = println("I'm flying")
And Birds have wings and legs:
abstract class Bird extends Animal with HasWings with HasLegs
One little comment before you continue: You can use the keyword with or the comma to concatenate more than one trait. They work in the same way.
abstract class Bird extends Animal, HasWings, HasLegs

Let’s define a couple of different Birds:

class Robin extends Bird with Flies
class Ostrich extends Bird

All mammals have a bodyTemperature.

abstract class Mammal extends Animal:
  def bodyTemperature: Double
Some animals know their name, and if they do, they respond to their name.
trait KnowsName extends Animal:
  def name: String
So, in the following code, a Dog is a Mammal that has legs and knows (responds to) its name.
class Dog(val name: String) extends Mammal with HasLegs with KnowsName:
  def bodyTemperature: Double = 99.3

Some cats and children are known as mammals that know their names but sometimes ignore their names.

trait IgnoresName:
  this: KnowsName => def ignoreName(when: String): Boolean
  def currentName(when: String): Option[String] =
    if ignoreName(when) then None else Some(name)

Now you can define a Cat class that has legs, knows its name, and ignores its name except at dinner time.

class Cat(val name: String) extends Mammal with HasLegs with KnowsName with IgnoresName:
  def ignoreName(when: String) =
       when match
         case "Dinner" => false
         case _ => true
  def bodyTemperature: Double = 99.5
Some Animals can be Athletes, and Runners are Athletes with legs:
trait Athlete extends Animal
Here is a Runner trait:
trait Runner:
  this: Athlete with HasLegs => def run() = println("I'm running")

A Person is a Mammal with legs and knows its name:

class Person(val name: String) extends Mammal with HasLegs with KnowsName:
  def bodyTemperature: Double = 98.6

A Biker is a Person but may only be added to an Athlete:

trait Biker extends Person:
  this: Athlete=> def ride() = println("I'm riding my bike")
And finally, let’s define some Genders.
trait Gender
trait Male extends Gender
trait Female extends Gender
You’ve defined a complex hierarchy of classes and traits. Let’s see what you can do with these classes. First, let’s try to create a Dog that’s also a Biker:
scala> val bikerDog = new Dog("biker") with Athlete with Biker
-- Error:
1 |val bikerDog = new Dog("biker") with Athlete with Biker
  |                               ^^^^^
  |                               illegal trait inheritance: superclass Dog does not derive from trait Biker's superclass Person
Cool, the compiler enforced your rule about Bikers needing to be Persons . Let’s create some valid LivingThings. Please note that you can combine different traits as part of the object creation. So, archer is an instance of a class that is a subclass of Dog that implements Athlete, Runner, and Male. The Scala compiler automatically creates this new, anonymous class for you.
scala> val archer = new Dog("archer") with Athlete with Runner with Male
archer: Dog with Athlete with Runner with Male = $anon$1@18bbc98
scala> val dpp = new Person("David") with Athlete with Biker with Male
dpp: Person with Athlete with Biker with Male = $anon$1@7b5617
scala> val john = new Person("John") with Athlete with Runner with Male
john: Person with Athlete with Runner with Male = $anon$1@cd927d
scala> val annette = new Person("Annette") with Athlete with Runner with Female
annette: Person with Athlete with Runner with Female = $anon$1@1ec41c0
You’ve got a bunch of Animals. Let’s see what you can do with them.
scala> def goBiking(b: Biker) = println(b.name + " is biking")
goBiking: (Biker)Unit
scala> goBiking(dpp)
David is biking
What happens if you try to send Annette on a bike ride?
scala> goBiking(annette)
-- Error:
1 |goBiking(annette)
  |         ^^^^^^^
  |         Found:    (annette : Person & Athlete & (Runner & Female))
  |         Required: Biker
This makes sense. The method requires a Biker, and Annette is not a Biker. However, just as you can compose a class out of traits, you can require that a class implement more than one trait in order to be the parameter to a method.
scala> def charityRun(r: Person with Runner) = r.run()
charityRun: (Person with Runner)Unit
The charityRun method can only be called with a parameter that is a subclass of Person and also implements the Runner trait.
scala> charityRun(annette)
I'm running
What if you try to call the method with a Runner that is not a Person?
 scala> charityRun(archer)
<console> :7: error: type mismatch;
found     : Dog with Athlete with Runner with Male
required  : Person with Runner
      charityRun(archer)
You can define the parameter in terms of traits. The womensRun method may only be called with a parameter that’s both a Runner and a Female.
scala> def womensRun(r: Runner with Female) = r.run()
womensRun: (Runner with Female)Unit
scala> womensRun(annette)
I'm running
scala> val madeline = new Cat("Madeline") with Athlete with Runner with Female
madeline: Cat with Athlete with Runner with Female = $anon$1@11dde0c
scala> womensRun(madeline)
I'm running

In this way, you’ve modeled complex relationships. You’ve modeled things in a way that you cannot do with Java. Scala’s compositional rules are very powerful tools for defining complex class hierarchies and for specifying the rules for composing classes as well as the rules for passing parameters into methods. In this way, you can make sure that the charityRun method can only be called with valid parameters rather than testing for parameter correctness at runtime and throwing an exception if the parameter is not correct. This increased modeling flexibility combined with enhanced type safety gives the architect another tool to help developers write correct code.

Conflicts of Method Names

Everything looks great using traits, but what happens if you extend from two different traits that contain the same method? To see this problem with one example, let’s create two traits:
scala> trait TraitOne:
     |     def text = "One"
// defined trait TraitOne
scala> trait TraitTwo:
     |     def text = "Two"
// defined trait TraitTwo
Now, create a class that extends from both traits and sees what happens.
scala> class Concrete extends TraitOne with TraitTwo
1 |class Concrete extends TraitOne with TraitTwo
  |      ^
  |      class Concrete inherits conflicting members:
  |        method text in trait TraitOne of type => String  and
  |        method text in trait TraitTwo of type => String
  |      (Note: this can be resolved by declaring an override in class Concrete.)
The error that appears in the console tries to explain that the compiler can’t decide which implementation of the method to use. There are ways to solve this problem. One of them is to override the method and define the behavior.
scala> class Concrete extends TraitOne with TraitTwo:
     |     override def text = "Class"
     // defined class Concrete
This solution compiles, but you lose the behavior of the methods that exist in both traits. To solve this problem, you can create new methods that call the methods of the traits using super.
scala> class Concrete extends TraitOne with TraitTwo:
     |     override def text = "Class"
     |     def textOne = super[TraitOne].text
     |     def textTwo = super[TraitTwo].text
// defined class Concrete
Now see what happens when you create an instance of the Concrete class.
scala> val con = Concrete()
val con: Concrete = Concrete@269bf0ac
scala> con.text
val res0: String = Class
scala> con.textOne
val res1: String = One
scala> con.textTwo
val res2: String = Two

One last comment: This problem only occurs when the methods of both traits have the same name, parameters, and return. If some of them are different, the problem won’t appear because the compiler detects that they are different methods.

Limiting the Use of a Trait

Sometimes when you create a certain trait, you want it to only be used in a specific situation. To solve this problem, there are two different approaches: one limits access to the class/trait that extends from another one, and the other option is where the class/trait has a specific method.

Limiting Access by Class

The idea of this type of limitation is that only the class/trait that contains certain traits can use it. Let’s see this concept with a concrete example.
scala> trait Engine:
     |     val enable = true
// defined trait Engine
scala> trait Vehicle:
     |     this: Engine => def drive = println("driving")
// defined trait Vehicle
In the example, all classes/traits that extend from this trait need to extend to Engine . If not, an exception appears.
scala> class Car extends Vehicle
1 |class Car extends Vehicle
  |      ^
  |      illegal inheritance: self type Car of class Car does not conform to self type Engine
  |      of parent trait Vehicle
If you modify the previous definition of the class Car and extend from Vehicle and Engine, everything works fine.
scala> class Car extends Vehicle, Engine
// defined class Car

This type of limitation appears in the examples in the section “Using Traits as Mixins.”

Limiting Access by Method

Let’s see the way to limit the use to only classes/traits that have a particular method. First, create a trait with the definition of the method.
scala> trait Vehicle:
     |     this: {def drive(): Unit} => println("drive")
Now create a class named Car that does not contain the method drive to see what happens.
scala> class Car extends Vehicle:
     |     def notDrive(): Unit = println("drive")
1 |class Car extends Vehicle:
  |      ^
  |      illegal inheritance: self type Car of class Car does not conform to self type Object{drive(): Unit}
  |      of parent trait Vehicle
If you define the method with the same structure that appears in the trait, everything works.
scala> class Car extends Vehicle:
     |     def drive(): Unit = println("drive")
// defined class Car

Type Parameters or Type Members

Traits offers a way to write code that you can use in any generic type or with certain types that cover one condition. A practical example of this is to imagine that you want to print the status of all components. The following example shows how you can do it:
scala> class Component:
     |     override def toString = "enable"
     |
     | trait Status[A]:
     |     def status(a: A): Unit = println(a.toString)
     |
     | class Screen extends Status[Component]
In the example, the Screen class defines which types support the status to print the status. Let’s see what happens when you invoke the method:
scala> val screen = Screen()
val screen: Screen = Screen@61dd1c3d
scala> val component = Component()
val component: Component = enable
scala> screen.status(component)
enable

Passing Parameters on Traits

Scala 3 lets you pass parameters in the constructor like an abstract class, so you can pass parameters from the concrete class to a trait.
scala> trait Vehicle(val model: String):
     |      override def toString = s"model: $model"
     |
     | class Car(override val model:String) extends Vehicle(model):
     |      override def toString = "car's " + super.toString()
// defined trait Vehicle
// defined class Car
Now if you create an instance of the class Car with any value, you can use the value that saves it in the constructor of the trait from the class.
scala> val car = Car("C4")
scala> println(car.toString())
car's model: C4

Enumerations

Enumerations (enums) are not a new feature in Scala 3. In fact, they exist in Scala 2 but they are not simple to use, so many developers do not use them a lot. This was one of the main reasons why Martin Odersky and Scala’s team decided to refactor the entire feature to give it a new face.

So what is an enumeration? An enumeration is a way to group a set of contents that have the same relationship. An example of the use of enumeration is information that has certain defined values in a person like gender or marital status.
enum Genre:
    case MALE, FEMALE, NO_BINARY
enum MaritalStatus:
    case SINGLE, MARRIED, DIVORCED

The way to define an enumeration is simple. You only need to use the keyword enum. To specify the possible values, you need to use the keyword case.

Now is time to use this constant in a simple way. Let’s create a class named Person that has two parameters in the constructor.
scala> val anna = Person(Genre.FEMALE, MaritalStatus.SINGLE) ass Person(genre: Genre, maritalStatus: MaritalStatus):
    override def toString() = s"Genre: $genre - Marital status: $maritalStatus"
The next step is to create an instance of the class, passing the values, and to print the result.
scala> val anna = Person(Genre.FEMALE, MaritalStatus.SINGLE)
val anna: Person = Genre: FEMALE - Marital status: SINGLE
scala> println(anna.toString())
Genre: FEMALE - Marital status: SINGLE
Having all possible values in one place is great but what happens if you don’t want to persist the entire String in the database? To solve this problem, enums let you pass variables that represent one value. Let’s change the Gender enum to show each possible value.
enum Genre(val code: Int):
    case MALE extends Genre(1)
    case FEMALE extends Genre(2)
    case NO_BINARY extends Genre(3)
If you want to know the code of one of the values of the enum, you only need to call the field code.
scala> println(Genre.MALE.code)
1
Enums give you by default a set of methods or operations to interact with the values. They are shown in Table 7-2.
Table 7-2

Enumeration Operations

Method

Description

Example

ordinal

A unique integer associated with each possible value that appears

scala> println(Genre.MALE.ordinal)       0

values

To obtain as an Array all possible values

scala> Genre.valuesval res0: Array[Genre] = Array(MALE, FEMALE, NO_BINARY)

fromOrdinal

Obtains the value in the enum from an ordinal position

scala> Genre.fromOrdinal(2)val res1: Genre = NO_BINARY

valueOf

Obtains the value in the enum from a string

scala> Genre.valueOf("MALE")val res2: Genre = MALE

Note that enums in Scala are not the same as in Java. It is the same situation that happens with collections, so to obtain great performance, try to not use it.

The way to use an enum from Java is to extend and parametrize with the type.
scala> enum Genre extends Enum[Genre]:
     |     case MALE, FEMALE, NO_BINARY
// defined class Genre

As you can see, enumerations can help reduce duplicate code or keep all possible values of something in one place. Try to use them only when you have a finite and small number of values.

Algebraic Data Types in Enums

ADT (algebraic data types ) is a classification where you can define a set of values enumerating all the values that it contains. Enums support the use of ADTs and add specific methods. Let’s see this concept with a simple example.
scala> enum Options[+T]:
     |   case Some(x: T)
     |   case None
// defined class Options
As you can see, Options is a parametrized enum with two simple cases. The way to use it is simple, as with any common enum.
scala> Options.Some(1)
val res6: Options[Int] = Some(1)
scala> Options.None
val res7: Options[Nothing] = None
You can create a custom method to interact with the different values, as you do with a common enumeration.
scala> enum Options[+T]:
     |     case Some(x: T)
     |     case None
     |
     |     def isDefined: Boolean = this match
     |       case None => false
     |       case _    => true
     |
// defined class Options
The way to invoke the method in the enumeration is simple. You just call the method of a particular value.
scala> Options.None.isDefined
val res8: Boolean = false

Union and Intersection Types

These are new features that appear in Scala 3 to combine different things.

Union types let you receive, return, or interact in some way with different types. Let’s see this concept with a practical example. Create a method that returns two possible types depending on one parameter.
scala> class Car
     | class Boat
scala> def createVehicle(vehicleType: Int): Car | Boat =
     |     vehicleType match
     |         case 1 => Car()
     |         case _ => Boat()
def createVehicle(vehicleType: Int): Car | Boat
Note that the order of classes in the union type does not have a particular constraint. You can invert the order and the code will perform the same way.
scala> def createVehicle(vehicleType: Int): Boat | Car =
     |     vehicleType match
     |         case 1 => Car()
     |         case _ => Boat()
def createVehicle(vehicleType: Int): Boat | Car
When you invoke the method and pass the parameters, you get the concrete instance of one particular class.
scala> val vehicle = createVehicle(1)
val vehicle: Car | Boat = Car@11ce721f

Union types are ideal when you need to return or pass more than one type, but when more types are added to the code, the complexity grows.

Intersection types are not so different from union types. The idea behind the intersection type is to combine the functionality of two or more different types.
scala> trait Printable:
     |     def print() =
     |         println("print")
     |
     | trait Status:
     |     def isEnable() =
     |         println("isEnable")
     |
     | class Component extends Printable, Status
     |
     | def checkComponent(x: Printable & Status) =
     |     x.print()
     |     x.isEnable()
// defined trait Printable
// defined trait Status
// defined class Component
def checkComponent(x: Printable & Status): Unit

In the example, checkComponent needs to receive something that contains both traits to use the methods inside. This approach is a good way to not directly specify one concrete class or another trait. It says, “Send me something that contains both traits.”

Now let’s see what happens when you create an instance of the class Component.
scala> val component = Component()
scala> checkComponent(component)
print
isEnable
As you can see, everything works well, but if you send something that does not cover both traits, an exception will appear.
scala> class Button extends Printable
scala> val button = Button()
scala> checkComponent(button)
1 |checkComponent(button)
  |               ^^^^^^
  |               Found:    (button : Button)
  |               Required: Printable & Status

Summary

This chapter shows how traits work and how to use them. You saw how a trait encapsulates method and field definitions, which can then be reused by mixing them into classes. You saw that traits are similar to multiple inheritances, but they avoid some of the difficulties of multiple inheritances.

In this chapter, you explored the advantages of enumerations in Scala to reduce the complexity when you have several values connected in some way. Also, you learned different ways to combine methods or parameters in the code using union and intersection types.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.165.234