Skip to content

Language

Types

  • opaque type aliases are transparent inside the scope they are defined in.
  • extension methods defined within scope defining the opaque types are available without importing
  • Structural Types are checked, at run-time, based on their overall structure
  • Refined Types are properties added by the type-system to any existing base type.
  • Path Dependent Types allow type of one object depend on another object.
  • Self Type are used for specifying additional requirements/constraints on the trait/class that uses this trait.
  • Type Lambda, Kind Projector refer to types defined in a block, class or trait
  • useful for specializing HKT that take multiple types to fewer types
  • Following are equivalent
type IntOrA[A] = Either[Int, A]   // partially applied type with an explicit name
({type L[A] = Either[Int, A]})#L  // anonymously projected type
Either[Int, *]                    // using kind-projector compiler plugin
  • Path-dependent Types Type defined in different objects are distinct from one another.
  • defining type directly in a class doesn't work because it then is just a type alias which gets resolved to the underlying types
  • instead, define type in a trait so Scala cannot statically prove they are same (because in trait, they could be abstract)
  • GADT, cases can extend the base type with different type arguments:
enum Tree[T]:
  case True extends Tree[Boolean]
  case Zero extends Tree[Int]

GADT

  • GADTs are polymorphic ADTs that specialize some term or introduce another type variable.
  • GADT can be used to enforce certain constraints at compile time. E.g.
// enforce sequencing or state transition: https://youtu.be/aSS-CIe_V0g?t=1502
final abstract class Idle
final abstract class Moving

enum Command[Before, After]:
  case Start extends Command[Idle, Moving]
  case Stop extends Command[Moving, Idle]

  // A Chain(Start, Start) won't compile
  case Chain[A, B, C](cmd1: Command[A, B], cmd2: Command[B, C]) extends Command[A, C]

Inheritance

  • inheritance is not commutative, i.e. A with B != B with A
  • class fields are resolved using following algorithm (called linearization)

Intersection and Union

  • intersection is not same as inheritance
trait A {def foo: X}
trait B {def foo: Y}

e: A & B
e.foo: X & Y

e: A with B  // with is not commutative
e.foo: Y
  • for union types, member must be present in common ancestor
e: A | B
e.foo  // error: member foo is not found in Anyref

Kinds

  • Kinds are types of types:
  • * Ordinary types, types which take no parameters Int, String etc
  • * -> * type constructors taking one type parameter: List, Option etc
  • * -> * -> * type constructors taking two type parameter: Either, Map etc
  • (* -> *) -> * type constructor taking another type constructor Functor
  • Examples:
Type Kind Notes
Int *
List * -> *
List[Int] *
Map * -> * -> *
Map[String, *] * -> * (partial application, kind projector)
Map[String, Int] *

Type constraints

{ def a: A }             // Structural type
[A <: B]                 // Upper Bound, A is a sub-type of B
[A >: B]                 // Lower Bound, A is a super-type of B
[A <% B]                 // View Bound
[A: B]                   // Context Bound
(implicit ev: A =:= B)   // Equality
(implicit ev: A <:< B)   // Conformance

Variance

  • Box[A] is invariant, that is, if A <: B then Box[A] are not related Box[B]
  • Box[+A] is covariant, that is, if A <: B then Box[A] <: Box[B]
  • Box[-A] is contravariant, that is, if A <: B then Box[A] >: Box[B]
  • BP keep types invariant when mutability is involved (because data of different type can be substituted dynamically)
  • BP Use covariance when the type is output or is contained, i.e. `case claBox[+A]
  • BP Use contra-variance when the type is input or is consumed
case class Box[+A](value: Option[A]):          // For covariance,
  def getOrElse[A1 >: A](default: A1): A1 =    // input is contra-variant, and output is covariant
    value match
      case Some(a) => a
      case None    => default

scala e: C[?] // there exists type T such that e: C[T] e: C[? >: Lo <: Hi] // there exists type T such that T <: Hi and T >: Lo

Classes

Property case class simple class
member access public private
== compares value identity
primary use aggregation encapsulation
  • use case class when number of possible values are bounded
  • use trait +class when number of operations on the type are fixed

Reflection

  • value.isInstanceOf[T]
  • type erasure: JVM does not store exact types constructed by type constructors such as List or Option
  • pattern matching on open class/trait instance uses reflection and is unsafe

scala //unsafe auth match case _: MockAuth => ??? case _ => ???

  • safe pattern matching: final class, sealed class/trait, case class/object, primitives (Int, Boolean, ...)

scala // safe env match case Local => ??? case UAT => ???

Syntactic Sugar

  • infix types: Class Composite[A,B] can be written as A Composite B
  • setter methods: a method is setter if it ends with _=
  • single arg methods allow curly braces, e.g. .map { ... }
  • single abstract method: a trait with a single abstract method can be instantiated with just a lambda

Functions

  • Partial functions can be lifted to total function that return Option: val aTotalFn = aPartialFn.lift
  • .curried method turns a regular function into a curried function, e.g. (Int, Int) => Int becomes (Int)(Int) => Int
  • a function or a method can be turned into curried version by using Eta expansion:

scala def method(x: Int, y: Int) => x + y method(7, _: Int) // Int => Int

  • Lambdas using _ always refers to innermost scope. Hence, not all lambdas can be written using _
  • accessor methods are without parenthesis are not auto expanded, but methods with empty parenthesis are. E.g.

```scala def method1 = 42 def method2() = 42

def byFunc(() => Int) = ??? byFunc(method2) // okay byFunc(method1) // fails, compiler substitutes value of the method1 call ```

  • by-name parameters are different from no-arg functions. E.g.

scala def f1(n: => Int): Int = ??? def f2(f: () => Int): Int = ???

  • parameter types:

    scala def m1(byVal: A, byName: => B) = lazy val byNeed = byName // causes a by-name parameter to be evaluated at most once if needed at all

Tips and Tricks

  • implicit can be added to parameters: IO.executionContext.flatMap { implicit ec => ...}

Misc

  • Nothing is bottom type (sub-class of every type) and Any is top type (super type of every type)
  • Nothing type has no values, no values can be created of this type
  • def m: Nothing is meant to describe that the method will either run forever or end with an error
  • List[Nothing] is singleton, represented as Nil
  • Option[Nothing] is singleton, represented as None
  • None is sub-class of every reference type AnyRef, and has exactly one instance null
  • methods can't be values by themselves, so leaving out parenthesis means invocation. Function names OTOH can be values, and need parenthesis if intention is assign the returned value
  • for comprehension is sugar for flatmap,...,map
  • @ in pattern matching, binds the entire expression
  • @@ in type denotes a tagged type
  • Scala can convert a function literal to SAM Types
  • Type Hierarchy Type Hierarchy
  • Tagless Final
  • Phantom Types, Builder Pattern
  • Infix notation can be used with any object and its method that takes one parameter
  • a.b(c) can be writer as a b c; eg. 5 + 6 equivalent to 5.+(6)
  • a b c d e is equivalent to a.b(c).d(e)
  • infix expression is converted method calls using precedence rules
  • method name that end with : are right associative; e.g. 0 +: sequence => sequence.+=(0)
  • List's have :: (Cons) which is equivalent to +: of Seq, and ::: corresponds to ++: (prepend list)
  • All literals are objects.
  • Object literals defined using object X and are of type X.type. They are of Singleton type since no other objects can be defined with that type
  • val can override def in a trait; therefore prefer using def instead of val
  • Recursive data structures must generally define a base case to stop recursion. (eg. Cons, End, List)
  • Use pass-by-name to define lazy recursive data: case class LazyList(head: Int, tail: () => LazyList)
  • flatMap sequences operations, allowing value produced in one step to be referenced in a subsequent step -- the essence imperative programming.
  • reify : represent or convert computation as data (ref: Trampoline lecture in Udemy Cats)
  • BP implicit/using parameters:

  • Environment Pattern: Environment parameters are static in some given context. E.g

    • Per request: RequestId, TraceId
    • Context: Prod/Test, RandomGenerator, Clock. For test, they are preferred to generate the same value
  • Typeclass Pattern: put a constraint on a parameter that have certain behavior