This is a note for the book Essential Scala. It describes how to use Scala in-the-small.

Introduction

An expression is a part of a progam that evaluates to a value. An expression has a type that specifies some restrictions that are checked in compile-time. Values are data in a computer’s memory that is manipulated by a program. A value is an object in scala. An object is a grouping of data (field) and operations (method) on that data.

A method that takes one parameter can be written as an infix operation, i.e., a.b(c) can be written as a b c.

Numberic types: Int (32 bit), Long (64 bit), Float (32 bit), Double (64 bit), Short (16 bit), Byte (8 bit).

Booleans: ture, false.

Char (16 bit Unicode value), String is a seqence of chars.

Null is a type with a single value of null.

Unit, written as () represents a void value.

To declare an object, using the syntax:

1
2
3
object name {
    declarationOrExpression
}

Declaring a new type object Test {} is not an expression, it binds a name Test to an empty object value. The Test has a type of Test.type that is a singleton type. You cannot create another value of a singleton type.

To declare a method in an object, use the syntax:

1
2
3
4
5
6
def name(parameter: type, ...): resultType =
    bodyExpression

// or for method without parameters
def name: resultType =
    bodyExpression

To declare a field in an object, use the syntax:

1
2
3
4
5
// for immutable field
val name: type = valueExpression

// for mutable field
var name: type = valueExpression

The fields of an object are intialized only once when the object is first referenced. The methods of an object are evaluted every time it is called.

Declarations such as method declaration, field declaration and object declaration are not exxpressions and don’t have a type.

A class declares a template for creating objects. It binds a name to a type that is not a value and the name is in a different type namespace.

The Nothing is the type of any throw expression. It is a subtype of everything else, including Null type.

Function Application Syntax

An object can be called like a function if it has an apply method. This is called function application syntax: the method call object.apply(parameter,...) is the same as object(parameter,...).

Use companion object to define methods that logically belongs to a class but is independent of any class instance, for example, additional contructors. A companion object is not an instance of its companion class, it is a singleton object with its own type. A companion object name is in value namespace. A companion object must be defined in the same file as the associated class.

Case Class and Case Object

A case class is a shorthand for defining a class, a companion object, and a lot of sensible defaults. It is often used for creating lightweight data-holding classes. Features of a case class are:

  • A field for each constructor argument.
  • A default methods for toString.
  • Sensible defaults for equals and hashCode that operate on the field values in the class.
  • A default copy method that creates a new object with the same field values if no arguments are given.
  • Implementation of two traits: java.io.Serializable and scala.Product. scala.Product has methods for inspecting number of fields and the name of the case class.
  • A companion object contains an apply method with the same parameters as the class constructor.
  • The companion object implments an extractor pattern.

You can still define a companion object for a case class. Scala will merge the generated companion object with the user-defined one.

For a case class with no constructor parameters, you can define a case object. It has a meaningful toStrign method and extends the two traits: java.io.Serializable and scala.Product.

Pattern Matching

Pattern matching is an expression and works like an extended if expression. The syntax is the following:

1
2
3
4
5
expr0 match {
    case pattern1 => expr 1
    case pattern2 => expr 2
    ...
}

For case cases, the patterns (guards) use the constructor syntax. A literal value matches a value, a name binds a value, and an underscore _ matches any value and ignore it.

Traits

Traits define operations shared by multiple classes. A trait has fields and methods. It differs from a class in that it cannot have a constructor and it can define abstract methods. Use multiple fields in case class to model a product data type. It is a pattern of A has B and C. For the pattern of A has B or C, there are two implementations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//  A has D that is B or C
trait A {
  def d: D
}
sealed trait D
final case class B() extends D
final case class C() extends D

// A is D that has a B or is a E that has a C
sealed trait A
final case class D(b: B) extends A
final case class E(c: C) extends A

A trait can be sealed and all its subtypes are defined in the same file. It is useful for pattern matching validation. To seal a class, use final. It is useful to model data that has distinct cases - a sum data type. It is a pattern of A is a B or C. For the pattern of A is a B and C, use trait A extends B with C.

Structural recursion breaks down data into smaller pieces. It has two variants: polymorphism and pattern matching.

  • product type
    • polymorphism just uses the pieces
    • pattern matching matches the pieces
  • sum type
    • polymorphism uses the pieces in the derived classes
    • pattern matching matches either types

To implement a structural recursion, there are three approaches:

  • polymorphism: allows open extension of trait (not sealed), the OO style.
  • pattern matching in the base trait: if it only depends data in the same class.
  • pattern matching in an external object: if it uses other data outside the class.

The OO style allows adding data easily. The functional style allows adding method easily. The functional style has another advantage, the compiler knows all subtypes and can check if there is any missing cases. Use type class to achieve polymorphism without class hierarchy

When defining recursive algebraic data types, there must be at least two cases: one is recursive and one is not, like the following:

1
2
3
4
sealed trait RecursiveExample
final case class RecursiveCase(recursion: RecursiveExample)
extends RecursiveExample
case object BaseCase extends RecursiveExample

When writing recursive code, there is a recursive case for recursive element and a base base that returns the identity for the operation.

Generics

Generic is used to abstract over types. Use [A, B, C] to add a type parameter to a class/trait or a method.

1
2
3
4
// invariant generic sum type pattern
sealed trait A[T]
final case class B[T]() extends A[T]
final case class C[T]() extends A[T]

Functions

A function is a value that abstracts over methods. A function has a type of (A, B, ...) => C where A, B, ... are types of the input parameters and C is the type of the result. the Parentheses may be dropped if a function has one parameter.

Placeholder syntax should be used only for very samll functions:

1
2
3
4
5
6
_ * 2 // expands to `a => a * 2`
_ + _ // expands to `(a, b) => a + b`
foo(_) // expands to `(a) => foo(a)`
foo(_, b) // expands to `(a) => foo(a, b)`
_(foo) // expands to `(a) => a(foo)`
// and so on...

To convert a method to a function, add a method with an underscore. Scala can automatcially convert a method to a function in a place where a function is expected. Methods can have multiple parameter lists where each parameter is defined in a parenthses.

A type like F[A] with a map method is called a functor. If a functor also has a flatmap method it is called a monad. The general idea is a monad represents a value in some context. use flatmap when to transform the value and provide a new context.

Variance annotation controls subclass relationships between types with type parameter.

For type Foo[A], if A is a subtype of B, there are three cases:

  • invariant: Foo[A] and Foo[B] are unrelated.
  • covariant Foo[+A]: Foo[A] is a subtype of Foo[B].
  • cotravariant Foo[-A]: Foo[A] is a supertype of Foo[B].

Scala has 23 built-in generic class for functions of 0 to 22 arguments. Functions are contravariant in terms of their arguments and covariant in terms of their return type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
trait Function0[+R] {
  def apply: R
}
trait Function1[-A, +B] {
  def apply(a: A): B
}
trait Function2[-A, -B, +C] {
  def apply(a: A, b: B): C
}

// and so on

It makes sense because usually we use function’s result value that has all the properties. A function with an argument that is a supertype of A works because A has all expected properties. A covariant sum type is defined as the following:

1
2
3
sealed trait A[+T]
final case class B[T](t: T) extends A[T]
case object C extends A[Nothing]

When define type parameter, you can use [AA >: A] to specify a type AA that is a supertype of A. It is useful to define type in a contravariant position, usually a function parameter. The syntax A >: B, A is a supertype of B, and C <: D, C is a subtype of D. Together they are type bounds.

Type Classes

A type class is a trait defined with one type parameters. A type class defines some behaviour that is supported by the type. Type classes allows extending existing libraries with new functionality without using inheritance and changing original code. The type class pattern separates the implementation of functionality from the type the functionality is provided for.

An implicit value must be declared within a surrounding object, class, or trait. It can be defined in four ways:

1
2
3
4
implicit val exampleOne = ...
implicit var exampleTwo = ...
implicit object exampleThree = ...
implicit def exampleFour = ...

The compiler searches for type class instances (implicit values) in an implicit scope. It includes local scope, enclosing class/object/trait, imported from elsewhere. An eligible implicit value must be a single identifier. The implicit socpe also includes the companion objects of types involved in the method call with the implicit parameter.

Elements of Type Classes

  • the actual type class itself
  • the type class instance
  • interfaces using implicit parameters
  • interfaces using enrichment and implicit parameters

A type class is a trait with at least one type variable. The type variables specify the concrete types the type class instances can have. Some type class patterns are defined as the following:

1
2
3
4
5
6
7
8
9
// Type Class Interface pattern: define an interface on the companion object using a no-argument (provided by an implicit parameter list) apply method
object TypeClass {
    def apply[A](implicit instance: TypeClass[A]): TypeClass[A] = instance
}

// Type encrichment via implicit classes: create an implicit class for a type class and add methods to the implicit class.
implicit class TypeClassHelper[A] {
    def extraMethod(...) = ...
}

The context bound notation name[A: Context] expands to name[A](...)(implicit c: Context[A]), i.e, a generic type parameter [A] along with an implicit parameter for a Context[A].

The context bound gives a short-hand syntax withoth having an explicit name for the implicit parameter. When there is a need to use it, use implicityl[Context] to get the implicit matching the given type.

Implicit conversions are a more general form of implicit classes. Use implicit keyword to tag any single-argument mehtod implict def aToB(in: A): B = ... to allow compiler to convert an A to B and use methods defined in B. Implicit classes are just syntactic sugar for an implicit conversion. Implicit conversion may cause many issues thus its usage is usually not recommended.

Together, there are three types of polymorphism in Scala:

  • Subtype polymorphism (inheritance): some classes share a common super class.
  • Parametric polymorphism (generic): one or more type is representd by abstract symbols. The type is a placeholder and usually has no assumption for its behavior (an Any type in Scala).
  • Ad hoc polymorphism ( type class): a common interface (behavior) for an arbitrary set of types. It is used with implicit parameter or implicit conversion.