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]
    }
    
  • GADTs are polymorphic ADTs that specialize some term or introduce another type variable.
    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
    

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
    

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

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:
    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.
    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.
    def f1(n: => Int): Int = ???
    def f2(f: () => Int): Int = ???
    
  • parameter types:
    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:
    1. 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
    2. Typeclass Pattern: put a constraint on a parameter that have certain behavior