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
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.
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.
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:
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:
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.
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.
If a class extends more than one trait, use extends for the first trait and with to mix in the other traits.
If a class extends a class and a trait, always use extends before the class name, and use with before the trait’s name.
Here ClassA is not declared abstract and therefore it implements all abstract methods of trait TraitA.
A trait can be comprised of both abstract and concrete methods.
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.
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.
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.
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.
Let’s define a couple of different Birds:
All mammals have a bodyTemperature.
Some cats and children are known as mammals that know their names but sometimes ignore their names.
Now you can define a Cat class that has legs, knows its name, and ignores its name except at dinner time.
A Person is a Mammal with legs and knows its name:
A Biker is a Person but may only be added to an Athlete:
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
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
This type of limitation appears in the examples in the section “Using Traits as Mixins.”
Limiting Access by Method
Type Parameters or Type Members
Passing Parameters on Traits
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.
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.
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.
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
Union and Intersection Types
These are new features that appear in Scala 3 to combine different things.
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.
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.”
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.