Multiple inheritance can lead to a lot of issues and one of the most known is the diamond problem where the compiler doesn't know which of inherited methods use. However in Scala we can use another structure to compose a class with several different classes, keeping us far away of the diamond problem. This structure is called mixin.
A virtual conference at the intersection of Data and AI. This is not a conference for the hype. Its real users talking about real experiences.
- 40+ speakers with the likes of Hannes from Duck DB, Sol Rashidi, Joe Reis, Sadie St. Lawrence, Ryan Wolf from nvidia, Rebecca from lidl
- 12th September 2024
- Three simultaneous tracks
- Panels, Lighting Talks, Keynotes, Booth crawls, Roundtables and Entertainment.
- Topics include (ingestion, finops for data, data for inference (feature platforms), data for ML observability
- 100% virtual and 100% free
👉 Register here
This post defines Scala's mixins. Its first section shows it from a big picture. The second part explains how mixins can be simply implemented in Scala. The next one talks about a way Scala resolves the quoted diamond problem. The last section contains a code showing a practical use case of mixins in a Scalatest.
Mixin definition
A mixin can be described in different terms. One of the most common are:
- special case of multiple "inheritance" - the inheritance is included in quote marks not by mistake. The mixins are often considered as being included to the class rather than being inherited. A consequence of that is their behavioral character of the class using the mixins. Unlike the classical inheritance, we consider mixin class as not having is a relationship with the parent classes but rather a behaves as one.
Thus the mixins can be thought as a dynamic additions of methods into an object. - not designed to exist in their own - the classes used in mixin composition aren't designed to exist in their own. Instead they're supposed to define some behavior that can be used in the composition.
- small scope - the previous point brings automatically another requirement for the classes involved in mixin composition. Since they're not supposed to be initialized and are behavior-oriented, they'd provide some units of functionalities that can be easily mixed together to create a fully featured class.
- popular - the mixins are analyzed here in the context of Scala but they exist in a lot of other languages. We retrieve them in: PHP (with the help of traits), Python (an example here in Mixins and Python) or Ruby (here an example using the keyword include and emphasizing what was told in previous points)
- mix - this word was not used by accident. Unlike extends that refers to the inheritance, It emphasizes the fact of mixing the functionalities of mixin classes in the mixed class.
- feature-oriented - the mixins are especially useful when we want to use one small and particular feature in a lot of classes or inversely, when we want to use a lot of small and decoupled features in one given class. It enhances the composability of the classes.
Mixins and Scala
Mixins in Scala are the traits that are chained together through with keyword as shown in the following snippet:
trait A trait B class Letter class ABLetter extends A with B // The mixin can also be created on the fly val abLetter = new Letter with A with B
As you can also note, the class composed of mixins (let's call it "mixed class" for simplicity) extends another class. This inheritance step is required and we can't build a class composed only of mixins.
In the situation where the mixed class includes 2 traits having the same method, the code won't compile. It's illustrated in the following example:
class X trait A { def get: String = "A" } trait B { def get: String = "B" } class C extends X with A with B { }
The compilation for such code will fail with:
Error:(88, 7) class C inherits conflicting members: method get in trait A of type => String and method get in trait B of type => String
Scala mixin and diamond problem
The diamond problem in Scala is solved pretty simply. The compiler takes the rightmost implementation of given method as the implementation used in mixed class. For instance in a class new MyClass extends ParentClass with Mixin1 with Mixin2 where Mixin1 and Mixin2 inherits from the same parent, the methods implemented in Mixin2 will be used. An exception happens however if the rightmost class doesn't provide the implementation for given method. If it's the case, the 2nd rightmost trait is used (Mixin1). If it also doesn't provide the behavior it's the 3rd mixin and so on.
Diamond problem
The diamond problem can be easily illustrated in the following image:
As you can see the class EnglishPerson extends 2 implementations of the same interface. Thus both of them implement the speak() method. If the programming language doesn't have the resolution rules for that case, the code won't work. Otherwise, as in the case of Scala, the resolution rule will be applied on compilation time to decide which of these 2 implementations should be used.
The following tests show how the diamond problem is handled by Scala:
behavior of "mixin class in diamond problem context" it should "take the behavior from the rightmost mixed trait" in { abstract class Letter { def get: String } trait A extends Letter { override def get: String = "A" } trait B extends Letter { override def get: String = "B" } class ABLetter extends Letter with A with B { override def get: String = super.get } val abLetter = new ABLetter abLetter.get shouldEqual "B" } it should "take the behavior from the 2nd rightmost mixed trait" in { // As explained in the post, it happens because trait B doesn't implement // the get method // If the implementation was missing in trait A, then the ABLetter class should provide // an implementation itself. Otherwise the code wouldn't compile. abstract class Letter { def get: String } trait A extends Letter { override def get: String = "A" } trait B extends Letter { } class ABLetter extends Letter with A with B { override def get: String = super.get } val abLetter = new ABLetter abLetter.get shouldEqual "A" }
You can also notice that the ABLetter class extends the same base class as the mixin traits. If it wasn't the case, the code wouldn't compile and the following error would be printed:
Error:(41, 36) illegal inheritance; superclass OtherLetter is not a subclass of the superclass Letter of the mixin trait A class ABLetter extends OtherLetter with A with B {
Scala mixin real world example
A good real world examples of Scala's mixin are Scalatest specifications. As shown in the post about ScalaTest and testing styles, the mixins are often used to compose reusable test base classes providing a common initialization logic. An example of such implementation is defined in the following test case:
class ScalatestExampleTest extends ExternalStorageSpec { "an example" should "show how the Scalatesdt mixin works" in { println("===Test#1===") } "another example" should "show how the Scalatest mixin works" in { println("===Test#2===") } } class ExternalStorageSpec extends FlatSpec with BeforeAndAfterAll with BeforeAndAfter { override def beforeAll(): Unit = { println("Init embedded DB") } override def afterAll(): Unit = { println("Shutdown embedded DB") } before { println("Add test data") } after { println("Remove test data") } }
Once executed we should see the following output:
Init embedded DB Add test data ===Test#1=== Remove test data Add test data ===Test#2=== Remove test data Shutdown embedded DB
This special kind of inheritance can help a lot to compose behavior-oriented classes. As told in the first section, the mixins prefer "behave as" rather than "is a" relationship and thus we can consider them as a special kind of composition. More specifically in Scala the mixins are defined thanks to with keywords followed by the traits used in the composition. Scala handles the multiple inheritance diamond problem pretty simply because it uses the rightmost defined method as the one used in the mixed class.