Small steps to Scala (10/20) - traits

Scala
10/20

Traits

  • traits as interfaces
  • traits with concrete implementations
  • layered traits & concrete/abstract methods & concrete/abstract fields
  • a more complex example

Traits as interfaces

Traits, or the abstraction over classes, are similar to interfaces in Java, but unlike Java, in Scala your trait can have both abstract and concrete methods.

In Scala a class can implement multiple traits. Also, you can add more abstract or concrete methods to the trait without influencing the other traits or classes extending from it.

However, when you modify an existing concrete method in a trait, it will influence the behavior of that method in all of the classes that extend from it.

trait Logger {
    def log(msg: String)                         // there is no need for the abstract keyword
    def info(msg: String) {log("INFO: " + msg)}  // you can add more methods
    def warn(msg: String) {log("WARN: " + msg)}
}

class ConsoleLogger extends Logger {
    def log(msg: String): Unit = {
        println(msg)
    }
}

var error = new ConsoleLogger()
error.log("logging")
error.info("Wrong parameter")

The above code will return

logging
INFO: Wrong parameter

All Java interfaces can be used as Scala traits. If you want to implement multiple traits in a class, use the with keyword as shown below:

class ConsoleLogger extends Logger with Cloneable with Serializable {
...
}

Traits with concrete implementations

In a trait you can have concrete implementations too.

One disadvantage of having concrete methods in a trait is that when the method changes, it may change the behavior of all classes who implemented it.

Also, you don't have to implement all the methods from the trait into your class.

import java.util.Date
    
trait Visitor {
    def id: String
    def registeredAt: Date
    def age: Long = new Date().getTime - registeredAt.getTime
}

case class Anon(id: String, registeredAt: Date = new Date()) extends Visitor
case class User(id: String, email: String, registeredAt: Date = new Date()) extends Visitor

Now we can create our users inside the REPL

scala> Anon("secret user")
res9: Anon = Anon(secret user,Sun Sep 02 12:21:41 JST 2018)

scala> res9.registeredAt
res10: java.util.Date = Sun Sep 02 12:21:41 JST 2018

scala> res9.age
res11: Long = 24365

Let's write a method that takes as argument any Visitor subtype:

def compare(v1: Visitor, v2: Visitor): Boolean = v1.registeredAt.before(v2.registeredAt)

In the REPL, we'll be using the previously created Anon visitor object (res9) and a newly created object of type User:

scala> compare(User("fresh user", "fresh@test.com"), res9)
res13: Boolean = false

scala> compare(res9, User("new user", "test@test.com"))
res14: Boolean = true

Layered traits & concrete/abstract methods & concrete/abstract fields

Let's look at some more examples of trait implementations below.

class Account {
    val user:String = "Jeremy"
    var balance:Int = 100
}

trait AccountLogger extends Logger {
    override def log(msg: String): Unit = {
        println(msg)
    }
}

trait AccountTimestampLogger extends AccountLogger {
    private val s = "## "  
    override def log(msg: String): Unit = {
        super.log(s + new java.util.Date() + "\n" + msg)
    }
}

trait AccountShortLogger extends AccountLogger {
    val maxLen: Int
    override def log(msg: String): Unit = {
        super.log(if (msg.length <= maxLen) msg else msg.substring(0, maxLen) + "...")
    }
}

When we extend a trait, the order is important, since the last one listed will be called at the end.

class SavingsAccount extends Account with AccountLogger with AccountTimestampLogger  with AccountShortLogger{
    val maxLen = 50  // if the field is abstract in the trait, it needs to be set here
    
    def withdraw(amount: Int) {
        if(amount > balance) log("Dear " + user + ", you have insufficient funds") // by default the last trait is used
        else { 
            balance -= amount
            super[AccountLogger].log("Hello, " + user + "\nYour current account has a balance of " + balance +" JPY!") // use another trait not the last one
        }
    }
}

Now let's make a transaction

val transaction = new SavingsAccount
transaction.withdraw(110)

The result

## Mon Jan 30 21:14:49 JST 2017
Dear Jeremy, you have insufficient funds

A more complex example

Put the following code in a file called Color.scala and load it from the REPL with the following command:

scala> :load /path/to/file/Color.scala

To run the program below, execute in the REPL: Run.main(Array("")).
What we want to achieve is to create two traits (Color and Shape) and Draw different shapes which take different colors.

The comments in the source code should be self-explanatory.

/**
	* Implementing a Color and a Shape sealed traits
	* The sealed traits can be extended only in the same file
	* The compiler knows all existing subtypes of the trait
	* and can provide an exhaustive match of all of them.
	*/
sealed trait Color {
	def red: Double
	def green: Double
	def blue: Double

	/**
		*  deciding the nuance of the color based on the intensity of RGB colors
		*/
	def isLight: Boolean = (red + green * blue) / 3.0 > 125.0
	def isDark: Boolean = !isLight

}

/**
	* Creating some Color objects (as RGB)
	* Red, Yellow and Fuchsia
	*/
case object Red extends Color {
	def red: Double = 255.0
	def green: Double = 0.0
	def blue: Double = 0.0
}

case object Yellow extends Color {
	def red: Double = 255.0
	def green: Double = 255.0
	def blue: Double = 0.0
}

case object Fuchsia extends Color {
	def red: Double = 255.0
	def green: Double = 0.0
	def blue: Double = 255.0
}

/**
	* Custom color that can be defined by the user
	*/
case class CustomColor(red: Double, green: Double, blue: Double) extends Color


/**
	*  Creating shapes with colors
	*/
sealed trait Shape {
	def sides: Int
	def perimeter: Double
	def area: Double

	def color: Color
}

/**
	* Defining a rectangular shape with width and height
	* and some extra properties like number of sides, permieter and area
	*/
sealed trait Rectangular extends Shape {
	def width: Double
	def height: Double

	override def sides: Int = 4
	override def perimeter: Double = 2 * (width + height)
	override def area: Double = width * height
}

/**
	* A square as subtype of Rectangular
	*/
case class Square(size: Double, color: Color) extends Rectangular {
	def width: Double = size
	def height: Double = size
}

/**
	*  A rectangle
	*/
case class Rectangle(width: Double, height: Double, color: Color) extends Rectangular

/**
	* A circle
	*/
case class Circle(radius: Double, color: Color) extends Shape {
	def sides: Int = 1
	def perimeter: Double = 2 * math.Pi * radius
	def area: Double = math.Pi * radius * radius
}

/**
	* Creating a Draw singleton object which will take as argument
	* either a Shape or a Color and return its description
	*/
object Draw {
	def apply(shape: Shape): String = shape match {
		case Circle(radius, color) 						=> s"A ${Draw(color)} circle with an area of ${Circle(radius, color).area}"
		case Square(size, color)							=> s"A ${Draw(color)} square with a perimeter of ${Square(size, color).perimeter}"
		case Rectangle(width, height, color)	=> s"A ${Draw(color)} rectangle with an area of ${Rectangle(width, height, color).area}"
		case _																=> "Some form of shape that may have a color"
	}

	def apply(color: Color): String = color match {
		case Red					=> "Red"
		case Yellow				=> "Yellow"
		case Fuchsia					=> "Fuchsia"
		case color:Color	=> if(color.isLight) "light color" else "dark color"
		case _						=> "Some unknown color"
	}
}

/**
	* Run everything
	*/
object Run {

	// some custom colors
	val blue: CustomColor = CustomColor(0.0, 0.0, 255.0)

	// the shapes
	val circle: Circle = Circle(2.5, Red)
	val square: Square = Square(4.0, Yellow)
	val rectangle: Rectangle = Rectangle(2.0, 3.0, blue)

	// main function
	def main(args: Array[String]): Unit = {
		println(Draw(circle))
		println(Draw(square))
		println(Draw(rectangle))
	}
}

The result of the above code should looks something like:

A Red circle with an area of 19.634954084936208
A Yellow square with a perimeter of 16.0
A dark color rectangle with an area of 6.0

NOTE!!! traits cannot have constructor parameters. Actually, in Scala the only difference between a class and a trait is that traits cannot have constructor parameters.

This was a short introduction to traits in Scala.


[+] Scala series
  • Small steps to Scala 1/20 - the basics
  • Small steps to Scala 2/20 - functions
  • Small steps to Scala 3/20 - arrays
  • Small steps to Scala 4/20 - maps and tuples
  • Small steps to Scala 5/20- classes
  • Small steps to Scala 6/20 - objects
  • Small steps to Scala 7/20 - inheritance
  • Small steps to Scala 8/20 - files and regex
  • Small steps to Scala 9/20 - operators

  • Disclaimer: This is by no means an original work it is merely meant to serve as a compilation of thoughts, code snippets and teachings from different sources.