Language¶
Types¶
opaque typealiases are transparent inside the scope they are defined in.extensionmethods 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
typedirectly in aclassdoesn't work because it then is just a type alias which gets resolved to the underlying types - instead, define
typein atraitso 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:
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
Kinds¶
- Kinds are types of types:
*Ordinary types, types which take no parametersInt,Stringetc* -> *type constructors taking one type parameter:List,Optionetc* -> * -> *type constructors taking two type parameter:Either,Mapetc(* -> *) -> *type constructor taking another type constructorFunctor- 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, ifA <: BthenBox[A]are not relatedBox[B]Box[+A]is covariant, that is, ifA <: BthenBox[A] <: Box[B]Box[-A]is contravariant, that is, ifA <: BthenBox[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 classwhen number of possible values are bounded - use
trait+classwhen 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
ListorOption - 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 asA 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 .curriedmethod turns a regular function into a curried function, e.g.(Int, Int) => Intbecomes(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¶
implicitcan be added to parameters:IO.executionContext.flatMap { implicit ec => ...}
Misc¶
Nothingis bottom type (sub-class of every type) andAnyis top type (super type of every type)Nothingtype has no values, no values can be created of this typedef m: Nothingis meant to describe that the method will either run forever or end with an errorList[Nothing]is singleton, represented asNilOption[Nothing]is singleton, represented asNoneNoneis sub-class of every reference typeAnyRef, and has exactly one instancenull- 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
forcomprehension 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 asa b c; eg.5 + 6equivalent to5.+(6)a b c d eis equivalent toa.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+:ofSeq, and:::corresponds to++:(prepend list)- All literals are objects.
- Object literals defined using
object Xand are of typeX.type. They are of Singleton type since no other objects can be defined with that type valcan overridedefin a trait; therefore prefer usingdefinstead ofval- 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) flatMapsequences 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/usingparameters: -
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