I touched Scala ~ [Trait] ~

Introduction

image.png

It's a direct migration of the dwango tutorial, where you'll study, edit it, and replace it with your own terms.

Trait

Our programs often have tens of thousands of lines, often hundreds of thousands or more. It's difficult to keep track of all of them at once, so you have to divide your program into meaningful and easy-to-understand units. In addition, it would be nice if the divided parts could be assembled as flexibly as possible to create a large program.

Program partitioning (modularization) and assembly (synthesis) are important design concepts in both object-oriented and functional programming. And traits are central to the modularization concept in Scala's object-oriented programming.

In this section, let's take a look at the features of Scala's traits.

Trait definition

A Scala trait is like removing the ability to define a constructor from a class, which can be roughly defined as:


trait <Trait name> {
  (<Field definition> | <Method definition>)*
}

Field definitions and method definitions do not have to have a body. The name specified in the trait name is defined as the trait.

Trait basics

Scala traits have the following features compared to classes.

You can mix in multiple traits into one class or trait Cannot be instantiated directly Cannot take class parameters (constructor arguments) Below, we will introduce each feature.

You can mix in multiple traits into one class or trait Unlike classes, Scala traits allow you to mix multiple traits into a single class or trait.


trait TraitA

trait TraitB

class ClassA

class ClassB

//Can be compiled
class ClassC extends ClassA with TraitA with TraitB

scala> //Compile error!
     | class ClassD extends ClassA with ClassB
<console>:15: error: class ClassB needs to be a trait to be mixed in
       class ClassD extends ClassA with ClassB
                                        ^

In the above example, you can create ClassC that inherits ClassA, TraitA, and TraitB, but you cannot create ClassD that inherits ClassA and ClassB. I get the error message "class ClassB needs to be a trait to be mixed in", which means "it needs to be a trait to mix in ClassB". If you want to inherit multiple classes, make the class a trait.

Cannot be instantiated directly

Scala traits, unlike classes, cannot be instantiated directly.


scala> trait TraitA
defined trait TraitA
scala> object ObjectA {
     |   //Compile error!
     |   val a = new TraitA
     | }
<console>:15: error: trait TraitA is abstract; cannot be instantiated
         val a = new TraitA
                 ^

This is a limitation because the trait is not supposed to be used alone in the first place. When you use a trait, you usually create a class that inherits it.



trait TraitA

class ClassA extends TraitA

object ObjectA {
  //Can be instantiated by making it a class
  val a = new ClassA

}

In addition, using the notation new Trait {}, it seems that the trait can be instantiated, but since this is a syntax that creates an anonymous class that inherits Trait and creates that instance, the trait itself is used. It is not instantiated.

Cannot take class parameters (constructor arguments)

Unlike classes, Scala traits have the limitation that they cannot take parameters (constructor arguments) 1.


//Correct program
class ClassA(name: String) {
  def printName() = println(name)
}
scala> //Compile error!
     | trait TraitA(name: String)
<console>:3: error: traits or objects may not have parameters
       trait TraitA(name: String)
                   ^

This isn't too much of a problem either. You can pass a value by giving the trait an abstract member. You can pass values to the trait by letting the class inherit it as you would in a problem that cannot be instantiated, or by implementing an abstract member at the time of instantiation.


trait TraitA {
  val name: String
  def printName(): Unit = println(name)
}

//Make it a class and overwrite name
class ClassA(val name: String) extends TraitA

object ObjectA {
  val a = new ClassA("dwango")

  //You may give an implementation that overwrites name
  val a2 = new TraitA { val name = "kadokawa" }
}

As mentioned above, trait restrictions are not a problem in practice, and can be used in the same way as classes in other respects. In other words, you can do virtually the same thing as multiple inheritance. And trait mixins bring great benefits to modularity. Let's get used to it.

About the term "trait"

This section uses object-oriented terms such as traits and mixins, but be aware that they may have slightly different meanings than those used in other languages.

The trait is based on the paper "Traits: Composable Units of Behavior" adopted by Schärli et al. In the 2003 ECOOP. , The handling of state variables, etc., looks different.

However, the terms traits and mixins vary from language to language, and the official Scala documentation we refer to and Scala Scalable Programming also use the phrase "mixin traits" here. Then I would like to imitate it.

Various features of the trait

[The diamond problem]

As we've seen above, traits are useful because they have class-like functionality but can be effectively multiple inherited, but there's one thing to consider. This is the "diamond inheritance problem" that programming languages with multiple inheritance face.

Consider the following inheritance relationship. TraitA, which defines the greet method, TraitB and TraitC, which implement greet, and ClassA, which inherits both TraitB and TraitC.


trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  def greet(): Unit = println("Good morning!")
}

trait TraitC extends TraitA {
  def greet(): Unit = println("Good evening!")
}
class ClassA extends TraitB with TraitC

TraitB and TraitC implements of the greet method are in conflict. What should Class A's greet do in this case? Should TraitB's greet method be executed or TraitC's greet method should be executed? Any language that supports multiple inheritance has this ambiguity problem and needs to be addressed.

By the way, when I compile the above example with Scala, I get the following error.


scala> class ClassA extends TraitB with TraitC
<console>:14: error: class ClassA inherits conflicting members:
  method greet in trait TraitB of type ()Unit  and
  method greet in trait TraitC of type ()Unit
(Note: this can be resolved by declaring an override in class ClassA.)
       class ClassA extends TraitB with TraitC
             ^

In Scala, if override is not specified, method definition collision will result in an error.

One solution in this case is to override greet with Class A, as the compilation error says "Note: this can be resolved by declaring an override in class Class A."


class ClassA extends TraitB with TraitC {
  override def greet(): Unit = println("How are you?")
}

At this time, you can also specify the method of TraitB or TraitC and use it by calling the method by specifying the type for super in ClassA.



class ClassB extends TraitB with TraitC {
  override def greet(): Unit = super[TraitB].greet()
}

The execution result is as follows.


scala> (new ClassA).greet()
How are you?

scala> (new ClassB).greet()
Good morning!

But what if you want to call both TraitB and TraitC methods? One way is to explicitly call both the TraitB and TraitC classes as above.


class ClassA extends TraitB with TraitC {
  override def greet(): Unit = {
    super[TraitB].greet()
    super[TraitC].greet()
  }
}

However, it is difficult to call everything explicitly when the inheritance relationship becomes complicated. There are also methods that are always called, such as constructors.

Scala traits have a feature called "linearization" to solve this problem.

Linearization

Scala's trait linearization feature treats the order in which traits are mixed in as the trait inheritance order.

Next, consider the following example. The difference from the previous example is that the treat method definitions for TraitB and TraitC have the override modifier.


trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  override def greet(): Unit = println("Good morning!")
}

trait TraitC extends TraitA {
  override def greet(): Unit = println("Good evening!")
}



class ClassA extends TraitB with TraitC

In this case, no compilation error will occur. So what exactly does you see when you call the ClassA greet method? Let's actually run it.


scala> (new ClassA).greet()
Good evening!

The call to the classA greet method executed the TraitC greet method. This is because the trait inheritance order is linearized, giving priority to the later mixed-in Trait C. In other words, if you reverse the order of trait mixins, Trait B will take precedence. Try changing the mixin order as follows.


class ClassB extends TraitC with TraitB
Then, the classB's greet method is called, and this time the TraitB's greet method is executed.

scala> (new ClassB).greet()
Good morning!

You can also use a linearized parent trait by using super

trait TraitA {
  def greet(): Unit = println("Hello!")
}

trait TraitB extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("My name is Terebi-chan.")
  }
}

trait TraitC extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("I like niconico.")
  }
}

class ClassA extends TraitB with TraitC
class ClassB extends TraitC with TraitB

The result of this greet method also changes in the order of inheritance.


scala> (new ClassA).greet()
Hello!
My name is Terebi-chan.
I like niconico.

scala> (new ClassB).greet()
Hello!
I like niconico.
My name is Terebi-chan.

The linearization feature makes it easy to recall the processing of all mixed-in traits. The process of stacking traits by such linearization is sometimes called Stackable Trait in Scala terminology.

This linearization is the solution to Scala's diamond problem.

Pit: Trait initialization order

The val initialization order of Scala traits is a big pitfall in using traits. Consider the following example. Trait A declares the variable foo, trait B uses foo to create the variable bar, class C assigns a value to foo, and then uses bar.


trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World"
}

class C extends B {
  val foo = "Hello"

  def printBar(): Unit = println(bar)
}

Let's call the class C printBar method in the REPL.


scala> (new C).printBar()
nullWorld

It is displayed as nullWorld. It seems that the value assigned to foo in class C is not reflected. The reason this happens is that Scala classes and traits are initialized in order from the superclass. In this example, class C inherits trait B and trait B inherits trait A. In other words, trait A is done first, the variable foo is declared, and nothing is assigned to the contents, so it will be null. Next, the variable bar is declared in trait B, and the string "nullWorld" is created from the null foo and the strings "World" and assigned to the variable bar. This is the character string displayed earlier.

How to avoid the initialization order of trait val

So how can this trap be avoided? In the above example, delaying the initialization of the bar so that foo is properly initialized before using it. Use lazy val or def to delay processing.

Let's look at the concrete code.


trait A {
  val foo: String
}

trait B extends A {
  lazy val bar = foo + "World" //Or def bar
}

class C extends B {
  val foo = "Hello"

  def printBar(): Unit = println(bar)
}

Unlike the previous example where nullWorld was displayed, lazy val is now used to initialize the bar. This will delay the initialization of the bar until it is actually used. In the meantime, foo is initialized in class C, so foo before initialization is not used.

This time, even if I call the printBar method of class C, Hello World is displayed properly.


scala> (new C).printBar()
HelloWorld

lazy val is a bit heavier than val and can cause deadlocks on complex calls. The problem is that if you use def instead of val, the value will be calculated every time. However, both are often not big issues, so consider using lazy val or def, especially if you want to use the value of val to create the value of val.

Another way to avoid the trait val initialization order is to use Early Definitions. Predefinition is a method of initializing fields before the superclass.


trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World" //You can leave it as val
}

class C extends {
  val foo = "Hello" //Called before superclass initialization
} with B {
  def printBar(): Unit = println(bar)
}

Calling the C printBar above will correctly display Hello World.

This pre-definition is a workaround from the user side, but in this example, there is a problem with trait B (initialization problems occur when used normally), so trait B was corrected. It may be better.

This predefined feature may not be very common in real code, as trait initialization issues often need to be resolved on the inherited trait side.

end

Next time, we will study type parameters and displacement specifications.

reference

This document is CC BY-NC-SA 3.0

image.png It is distributed under.

https://dwango.github.io/scala_text/

Recommended Posts

I touched Scala ~ [Trait] ~
I touched Scala
I touched Scala ~ [Class] ~
I touched Scala ~ [Object] ~
I touched Scala ~ [Control syntax] ~
I first touched Java ②
I first touched Java ③
I first touched Java ④
I touched Scala ~ [Type parameters and displacement specification] ~
I first touched Java
I went to Scala Fukuoka 2019!
I made an eco server with scala
Note that I touched Android's SQLiteOpenHelper lightly
I touched ODM for Developer ② Rule development