Scala Explained!

Scala tips and code snippets.

Classes

Classes are templates for new objects.

Available structures

In Scala language there are several structures that look like classes. These structures are class, trait, abstract class, case class and object.

The following table shows the main features of each structure:

  can extend can be extended has init. params. can be instantiated compare instances by structure
class
case class
abstract class
trait
object

Additional features:

Structure to use depending on the context

Which type of class to use for which purpose:

The goal The structure to use
Create a singleton use an object
Create instances that store information use a case class
Create instances that share the same features use a class
Create a base class for related classes use an abstract class
Create a base class for unrelated classes use a trait
Other use a trait first then change later if necessary

Class

A class is a structure that can be instantiated and extended. Class instances can be created with initialization arguments.

Attribute modifiers: private and protected

A class attribute visibility can be tuned using an attribute modifier:

Example:

// Print robot journalist articles.

class Bot {
  // a `private` member that can only be accessed in the current class
  private val commandAndControlServer: String = "bot.example.com"

  // a `protected` member that can be used in the current class
  // and its derived classes
  protected val author: String = "a Robot"

  // a member that is `public` by default
  val date: String = "today"
}

class NewsBot extends Bot {
  // the protected member `author` is used in the class that extends `Bot`
  def writeArticle: String = s"This is good news ── $author ($date)"
}

val botJournalist = new NewsBot

println(botJournalist.writeArticle)
// --> This is good news ── a Robot (today)

The class initialization parameters visibility can also be set:

  • class ClassName(val x: Int): the attribute is public.
  • class ClassName(protected val x: Int): the attribute is protected.
  • class ClassName(private val x: Int): the attribute is private.
  • class ClassName(x: Int): the attribute is also private.

Class member access control: getter and setter

To control the access to a private class member, two methods must be defined:

A getter method that has the name of the member, example:

A setter method that has the name of the member followed by _=, example:

Full example:

// Display the high scores of a game.

// the game class
class Game {
  // creates a private variable
  private var _highScore: Long = 0

  // create a getter for the private variable `_highScore`
  def highScore = _highScore

  // create a setter for the private variable `_highScore`
  def highScore_= (newScore: Long): Unit =
    if (newScore > _highScore) _highScore = newScore
}

val game = new Game

// run the setter
game.highScore = 13

// run the getter
val currentHighScore = game.highScore

println(s"New high score is $currentHighScore")
// --> New high score is 13

// run the setter again (the given value is discarded this time)
game.highScore = 1

// run the getter again
val actualHighScore = game.highScore

println(s"New high score is $actualHighScore")
// --> New high score is 13

Define multiple class constructors: this

Classes has a member named this that is bound to the current instance. this member is also used to define constructors:

// Store some text.

// `TextHolder` default constructor. It takes a `String` parameter
class TextHolder(val text: String) {

  // a second constructor that calls the default constructor
  def this(base: String, repeat: Int) = this(base * repeat)

  // a third constructor that calls the second one
  def this() = this("A", 3)
}

// use the third constructor
val a = new TextHolder

// use the second constructor
val b = new TextHolder("A", 3)

// use the default constructor
val c = new TextHolder("AAA")

val allSame = (a.text == b.text && b.text == c.text)

println(s"${ if (allSame) "same" else "not same"} value")
// --> same value

Calling super classes’ members: super

super keyword gives access to the members of the super classes:

// Convert inches to meters.

// base class
class Converter {
  def inchToMeters(x: Double): Double = x * 2.54 / 100
}

// derived class
class SimpleConverter extends Converter {
  override def inchToMeters(x: Double): Double =
    // the base class's `inchToMeters` method is called through `super`
    super.inchToMeters(x).round
}

val converter = new SimpleConverter

println(s"a 40″ TV as a diagonal ~= ${ converter.inchToMeters(40) }m")
// --> a 40″ TV as a diagonal ~= 1.0m

Class modifiers: final and sealed

abstract is also a class modifier, see abstract classes.

Callable instance and assignment expansion: apply and update

A class will produce callable instances if it has a method named apply:

A value can be assigned to a class instance call if the class has a method named update. This is known as assignment expansion:

Example:

// Work with a two-dimensional matrix.

// using a mutable map to be able to change the matrix content
import scala.collection.mutable.{Map => MutMap}

class Matrix(x: Int, y: Int) {
  val map: MutMap[(Int, Int), Int] = MutMap()

  private def checkBounds(i: Int, j: Int): Boolean =
    i >= 0 && i < x && j >= 0 && j < y

  // makes the instances callable
  def apply(i: Int, j: Int): Int =
    if (checkBounds(i, j)) map.getOrElse((i, j), 0) else 0

  // allows assignment expansion
  def update(i: Int, j: Int, newValue: Int) =
    if (checkBounds(i, j)) map.update((i, j), newValue)
}

// create a matrix with bounds: i ∈ [0, 4[ and j ∈ [0, 4[
val matrix = new Matrix(4, 4)

// implicitly call `apply` to retrieve the value at i = 1 and j = 1
val valueAtPositionOne = matrix(1, 1)

println(s"matrix[1, 1] = $valueAtPositionOne")
// --> matrix[1, 1] = 0

// implicitly call `update` to modify the value at i = 2 and j = 2
matrix(2, 2) = 4

// retrieve the updated value
val valueAtPositionTwo = matrix(2, 2)

println(s"matrix[2, 2] = $valueAtPositionTwo")
// --> matrix[2, 2] = 4

// indices 10 are out of bounds, the change is dropped by `update` method
matrix(10, 10) = 100

// retrieve the default value, because indices 10 are out of bounds
val valueAtPositionTen = matrix(10, 10)

println(s"matrix[10, 10] = $valueAtPositionTen")
// --> matrix[10, 10] = 0

Case class

A case class is a class created primary to store data. case class instances have the following features:

  1. They support pattern matching.
  2. They can be compared with each other a == b,
  3. They produce a readable message when printed, ex: Point(1,2).

Examples of case class.

Copy and modify an instance: copy

A case class instance has a built-in method copy that creates a copy of the instance.

The new instance attributes values can be set through the copy method.

// Sell fruits.

case class Fruit(name: String, price: Double)

// the original instance
val mango = Fruit("mango", 1)

// the copy, `price` attribute is modified
val expensiveMango = mango.copy(price = 10)

println(s"this ${ expensiveMango.name } costs €${ expensiveMango.price }")
// --> this mango costs €10.0

Extract instance parameters

case class allows the extraction of instance parameters:

// Describe a vacation place.

case class VacationPlace(landscape: String, ratings: Int)

val location = VacationPlace("heavenly beach", 5)

// extracting `location` instance parameters into `place` and `stars` values
val VacationPlace(place, stars) = location

println(s"Going to a $stars stars $place.")
// --> Going to a 5 stars heavenly beach.

Other features: productPrefix, productIterator, etc…

case class instances expose the following methods:

Example:

// Get info about a movie.

case class Movie(title: String, budget: Double, rating: Int)

val blockBuster = Movie("Triple-W 4 The Prequel", 500e6, 7)

// retrieve the number of fields
val nbFields = blockBuster.productArity

// get the `case class` name
val className = blockBuster.productPrefix

println(s"$className has $nbFields fields")
// --> Movie has 3 fields

// retrieve an instance parameter value
val rating = blockBuster.productElement(2)

println(s"$className rating is $rating/10")
// --> Movie rating is 7/10

// iterate through instance parameters
val fields = for (field <- blockBuster.productIterator) yield s"- $field"

println(s"""
  Full $className info:
  ${ fields.mkString("\n  ") }
""")
/* -->
  Full Movie info:
  - Triple-W 4 The Prequel
  - 5.0E8
  - 7
*/

Abstract class

An abstract class is a class that cannot be instantiated but can take initialization parameters. A class can only extend one abstract class.

Trait

A trait is a structure close to a class that cannot be instantiated. In addition to that, traits doesn’t have initialization parameters.

A class can extend several traits.

Trait mixed-in pattern

Trait mixed-in is a design pattern where a feature is injected into a class through a trait. To be able to do that both class and trait should extend the same base trait.

Example:

// Display screen specs.

// base trait with a method `specs` that is not fully defined
trait Screen {
  def specs: String
}

// a class that fully defines `specs` method
class LcdMonitor(val monitorType: String) extends Screen {
  def specs = s"${ monitorType }-display"
}

// a trait with a method that uses `specs`. It will be mixed-in `LcdMonitor`
trait ScreenWall extends Screen {
  def fullSpecs(nbScreens: Int) = s"Wall made of $nbScreens ${ specs }s"
}

// generate a new class by mixing the two structures above
class HighResLcdWall extends LcdMonitor("4K") with ScreenWall

val wall = new HighResLcdWall

println(wall.fullSpecs(16))
// --> Wall made of 16 4K-displays

Stackable trait pattern: abstract override

Stackable trait pattern is a programming pattern where a functionality is split into multiple components then assembled using multiple inheritance.

With abstract override it is possible to partially define a method in a trait. The actual method is implemented by an other trait and executed through super.parentMethod(...).

This feature is used to implement the stackable trait pattern:

// Create a database engine.

trait Engine {
  def store(entry: Int, dataset: String): Unit
}

trait SQLEngine extends Engine {

  /** Modifies the `dataset` parameter then calls an unknown implementation
    * of the method.
    */
  abstract override def store(entry: Int, dataset: String): Unit =
    super.store(entry, s"sql://$dataset")
}

trait Database extends Engine {

  /** This is the implementation that is called by the `abstract override`
    * variant of this method.
    */
  override def store(entry: Int, dataset: String): Unit =
    println(s"stored $entry in $dataset")
}

// stacking the traits
class SQLDatabase extends Database with SQLEngine

val database = new SQLDatabase

database.store(5, "prices")
// --> stored 5 in sql://prices

More about the stackable trait pattern on https://www.artima.com/scalazine/articles/stackable_trait_pattern.html.

Instantiate an anonymous class new Trait { ... }

An instance can be created from a trait by fully defining all its members. To define these members you can create an anonymous class that extends the trait.

Example:

// Print greetings.

trait Greet {
  // trait member `greetings` is not fully defined
  def greetings: String
}

// instance of a class created on the fly that extends `Greet` trait
val instance = new Greet {
  // `greetings` is defined here
  def greetings: String = "Greetings to the world!"
}

println(instance.greetings)
// --> Greetings to the world!

Object

An object is a singleton instance. Unlike classes, objects are not types and cannot be instantiated.

The main method

Scala program entry point is a method named main that should be defined inside an object.

The main method must have the exact signature: main(args: Array[String]): Unit

Example:

// Run a super fast software.

object SuperFastSoftware {
  // define the program entry point
  def main(args: Array[String]): Unit = {
    println("Already completed!")
  }
}
// --> Already completed!

Importing object members

An object’s member can be imported in an other file or an other namespace using the following syntax: import packagename.ObjectName.objectMember

That is demonstrated in the example bellow. The example requires two files and can be executed using sbt tools:

// Running a worker.

// ---

/** file 1: project/Worker.scala
  *
  */

package project

object Worker {
  // define a method inside the object that will be imported in an other file
  def doWork: Unit = println("Working!")
}

// ---

/** file 2: project/Main.scala
  *
  */

package project

// importing the member of the object `Worker`
import Worker.doWork

object Main {
  def main(args: Array[String]): Unit = doWork
}
// --> Working!

Companion object and companion class

A companion object and a companion class are created when a class and an object share the same name. The class can use the object members as if they where static members of the class.

Extractor object: apply and unapply

With the extractor object feature it is possible to call an object and extract values from an object:

This mechanism is used with pattern matching.

Object protected members: private[this], protected[this]

The private[this] modifier is used to create an object members that is only visible inside the object and cannot be used by a companion class:

// Store a secret key, protected by a passphrase.

// this class cannot directly access the object private `key` member
// of its companion object
class KeyStore(passPhrase: String) {
  def getKey: Option[Long] = KeyStore.getKey(passPhrase)
}

object KeyStore {
  // only visible inside the object
  private[this] val key = 123456789L

  def getKey(passPhrase: String): Option[Long] = passPhrase match {
    case "azerty" => Some(key)
    case _ => None
  }
}

val keyStore = new KeyStore("azerty")
val secretKey = keyStore.getKey

println(s"the secret key is $secretKey")
// --> the secret key is Some(123456789)

protected[this] ... defines a member that is only visible in the current class and its derived classes. The companion object cannot access it.

Combined classes

There are several ways to combine classes.

Outer and inner classes: instance.Inner and Outer#Inner

An inner class is a class defined inside an other class. The enclosing class is called outer class.

Compound types: TypeA with TypeB

A compound type can be created by the composing multiple types: TypeA with TypeB with TypeC with...

A compound type can be instantiated even if its components are traits and abstract classes as long as they are fully defined.

The compound type reproduces the subtyping relations of the component classes. In other words:

if TypeY < TypeX then (TypeY with TypeZ) < (TypeX with TypeZ)

Self-type mix-in: this: Type =>

self-type mix-in is a syntax used to inject the members of a class into an other class.

The instantiation of the resulting type must conform to the self-type:

// Get information about a place.

trait Place {
  def name: String
  def location: String
}

trait Reputation {
  // Mix `Place` trait in `Reputation` trait
  this: Place =>

  def stars: Int
  def describe: String = s"$name at $location has $stars stars"
}

// Extend both classes here to satisfy the self-type requirement:
// `Reputation with Place`
class Recommendation(
  val name: String,
  val location: String,
  val stars: Int) extends Reputation with Place

val reco = new Recommendation("The Music Club", "Lagos", 5)

println(reco.describe)
// --> The Music Club at Lagos has 5 stars

Generic class

A generic class is a class that takes type parameters. Generic classes produces an actual class when the type parameter is replaced by an actual type.

Type variance: invariant, covariant and contravariant: T, +T and -T

Let’s take two types Parent and Child that have a subtyping relation Child < Parent and a generic class Generic[T]. The variance of the type T defines the relation between Generic[Parent] and Generic[Child]:

By default Scala type variables are invariant. A covariant type variable is defined by adding + symbol to the class variable name, ex: +T. On the other hand, a contravariant type variable is defined by adding - symbol to its name, ex: -T.

Type upper and lower bounds: T <: Upper and T >: Lower

Optional upper and lower bounds define which types can be applied to a generic class.

Using type variance and type bounds together

Here is an example that uses type variance and type bounds at the same time:

// Compute input data to produce output data.

abstract class Output(val data: String)

class RichOutput(override val data: String) extends Output(data)

abstract class BaseInput(data: String) {
  def compute: RichOutput =
    new RichOutput(data.replace("-", "..."))
}

class Input(data: String) extends BaseInput(data) {
  override def compute: RichOutput =
    new RichOutput(data.replace("-", ""))
}

/** ComputingUnit type variables bounds and variance.
  *
  * Type P:
  *   Bounds: Input < P < BaseInput
  *   Variance: -P is contravariant
  *   P can then be replaced as follows:
  *
  *   `val x: ComputingUnit[Input, R] = new ComputingUnit[BaseInput, R]`
  *
  * Type R:
  *   Bounds: R < Output
  *   Variance: +R is covariant
  *   R can then be replaced as follows:
  *
  *   `val x: ComputingUnit[P, Output] = new ComputingUnit[P, RichOutput]`
  */
abstract class ComputingUnit[
    -P >: Input <: BaseInput,
    +R <: Output] {
  def run(input: P): R
}

class ComplexComputingUnit extends ComputingUnit[BaseInput, RichOutput] {
  def run(input: BaseInput): RichOutput = input.compute
}

val unit: ComputingUnit[Input, Output] = new ComplexComputingUnit
val input: Input = new Input("e-p-s-i-l-o-n")
val result: Output = unit.run(input)

println(s"result: ${ result.data }")
// --> result: epsilon

View bounds (deprecated): T <% View[T]

View bounds use implicit conversion to convert types. This feature is deprecated.

// Compare values.

// using implicit convertion to convert `T` => `Ordered[T]`
// to be able to use `>` operator on `T` values
def moreThan[T <% Ordered[T]](x: T, y: T) = x > y

val compareIntValues = moreThan(4, 9)

println(s"result is $compareIntValues")
// --> result is false

val compareCharValues = moreThan('z', 'a')

println(s"result is $compareCharValues")
// --> result is true

Abstract types

An abstract type is a type alias defined inside a class an abstract class or a trait. The type that matches this alias is defined in the derived classes.

Constraints like upper are lower bounds can be applied to abstract types. Therefore, abstract types can be used to reduce the number of type parameters defined in a generic class.

// Create a network datagram.

trait Header {
  // define an abstract type `T`
  type T
  val header: T
}

trait Payload {
  // define an abstract type `T` (same name as above)
  type T
  val payload: T
}

trait Extract {
  // define an abstract type `R` that is a subtype of `String`
  type R <: String
  def extract: R
}

class Datagram(
    val header: Int = 0xA,
    val payload: Int) extends Header with Payload with Extract {

  // set the type that matches `T` in `Header` and `Payload` traits
  type T = Int

  // set the type that matches `R` in `Extract` trait
  type R = String

  def extract: String = f"|header=$header%02x|payload=$payload%04x|"
}

val datagram = new Datagram(payload = 1470)

val content = datagram.extract

println(s"datagram content: $content")
// --> datagram content: |header=0a|payload=05be|

Existential types (to avoid): T forSome { type T }

Existential types are some kind of generic types that matches any type. They are used mostly for compatibility with Java wildcard or raw types.

Existential types might generate obscure type errors if not used properly therefore they should be avoided.

// Count items that doesn't have the same types.

// the `import` here enables the existential types feature
// => this feature should be avoided in most cases!
import scala.language.existentials

// define an existantial type `A forSome { type A }`
def countAll(parameters: A forSome { type A }*) = parameters.length

val count = countAll(1, "lol", 'c')

println(s"counted $count unrelated values")
// --> counted 3 unrelated values

Implicit conversion

Implicit conversion is a system designed to define various operations that will be executed implicitly.

The implicit operations are defined using the keyword implicit. In addition to that, the implicit operations must be accessible from the place where they are implicitly used.

Operator overloading

Operators are functions. They are represented by symbols and use the infix notation. For example:

Operators features:

Example:

// Points that supports addition and subtraction operations.

case class Point(val x: Int = 0, val y: Int = 0) {

  // operator: `Point + Point`
  def +(that: Point): Point = new Point(this.x + that.x, this.y + that.y)

  // operator: `Point - Point`
  def -(that: Point): Point = new Point(this.x - that.x, this.y - that.y)

  // operator: `Point + Int`
  def +(that: Int): Point = new Point(this.x + that, this.y + that)

  // operator: `Point - Int`
  def -(that: Int): Point = new Point(this.x - that, this.y - that)

  // unary operator: `-Point`
  def unary_- : Point = new Point(-this.x, -this.y)

  // unary operator: `+Point`
  def unary_+ : Point = this
}

// implement commutative operations for + and -:
//    by using an implicit conversion
//    to extend `Int` values to `IntExtendedForPoint`,
//    a class that supports operations with `Point`
implicit class IntExtendedForPoint(val x: Int) {

  // operator: `Int + Point`
  def +(point: Point): Point = point + x

  // operator: `Int - Point`
  def -(point: Point): Point = -point + x
}

val a = Point(1, 2)

// `Point - Int`
val b = a - 4

// `Int + Point` (commutative operation)
val c = 2 + b

// `-Point` (unary operation)
val d = -c

// `Point + Point`
val e = d + Point(y = 2)

val isSame = a == e

println(if (isSame) "same point" else "different points")
// --> same point
You can leave a comment or request changes here.

This project is maintained by Y. Somda (yoeo)
Logo by Icon Island — theme by orderedlist — remixed by yoeo