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] |
* |
|
{ 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

- 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