Study note of the chapter 19, 20, and 21 of the book: Programming In Scala, 4th Edition.
Chapter 19: Type Parameterization
19.1 Introduction
Type parameterization
allows type contructors
using generic
classes and traits. A constructor type of genecric class or trait is not a regular data type. A constructed type with all specified type arguments is called a parameterized type
. Varianace
defines subtype relationships of parameterized types.
The List
class uses a fully persistent
data structure, where old versions remain available even after extensions or modifications.
Scala doesn’t have an explicit primary constructor
. The primary constructor is defined implicitly by the class parameters and body. You can make it private by putting a private
modifier in front of the class parameter list.
You can define a public trait and define a private class in a singleton object.
19.2 Variance
If S
is a subtype of T
, then
- if
F[_]
iscovariant
/flexible
,F[S]
is a subtype ofF[T]
. - if
F[_]
iscontravariant
,F[S]
is a supertype ofF[T]
. - if
F[_]
isnovariant
/regid
, there is no subtype relationship betweenF[S]
andF[T]
.
In Scala, Array
is novariant. The asInstanceOf[Array[SuperType]]
allows an array to be casted as an array of its supert type.
The compiler checks that type parmaters are used in right positions. Covriant types must be in positive
position, controvriant types in negative
position, and novariant types in neutral
position.
To classify the positions, the compiler starts from outside to the inside. usually the nesting levels are classified the same as the enclosing level, with the following exceptional rules:
- Method value parameter positions are classified to the
flipped
classification. The flip of a positive is a negative, a flip of a negative is a positive and a flip of a neutral is neutral. - The position of the type parameter of a method is also
flipped
. - The classification of the position of the type parameter of a type depends on the variance of the corresponding type parameter. For covariant type, the position stays the same. For contravariant, the it flips. For novariant, the position is neutral.
It is worth pointing out that var
class parameters are treated as methods because the compiler generates getter methods for them. An exeption is no checking is done if the class parameters are object private by having a private[this]
modifier.
The [U >: T]
syntax defines a lower bound T
for U
. U
is in a negative position and T
is in a positive position. The lower bound is often used to obtain covariance.
The [U <: T]
syntax defines an upper bound T
for U
. U
is in a negative position and T
is in a positive position. Upper bound is not commonly used in Scala alone because implicit parameter
and context bound
often provide better solutions.
Chapter 20: Abstract Members
20.1 Abstract Memebers
Scala allows abstract fields include method, val, var and type. An abstract type
in scala is a type T
member, without defintion, of a class or trait. Abstract classes or abstract traits are not abstract types.
A type member has two purpose: (1) a short, descriptive alias for a concrete type and (2) an abstract type that must to be defined in subclasses.
An abstract val defines a value that stays the same after initialization. It cannot be overridden by an abstract var or an abstract method.
A class parameter argument is evaluated before it is passed to the class constructor. A val
defintion in a subclass, is evaluated after the superclass has been iinitialized. Use pre-initialized
vals or lazy
vals.
Singleton objects are lazy.
Upper bound types are often used with abstract types to specify that type constraints for subclasses.
20.2 Path-dependent Type
Type defined in a class or an object is a path-dependent type
that can be referenced using an object identity, for example a.T
or b.T
. The type of different objects that has the same class share the same path-dependent type.
Inner classes are path-dependent type. However, different objects don’t share the same inner class. All objects’ inner class are subtypes of their general type Outer#Inner
.
The #
is used for classes and .
is used for bojects. An inner class must hold a reference to an enclosing outer class instance to allow the inner class to access members of its outer class.
The Enumeration
defines an inner class Value
and a method Value
that returns an instance of the path dependent class Value
. Therefore each subtype of Enumeration
has a different path-dependent type of the Value
inner class.
When a class inherits from another, the first is sadi to be a nominal subtype
of the other one. It is nominal because it has a name. Scala also supports structural subtype
via refinement type
. A structural subtype has compatible members to its parent. to use refinement type, simple write the base type, followed by a sequence of members listed in curly braces.
Chapter 21: Implicit Conversioins and Parameters
Implict defintions are converts that the compiler inserts into a program in order to fix a type errors. Implicits are used in three places:
- conversions to an expected type
- coversions to the receiver of a method call
- implicit function parameters.
21.1 Implicit Conversion Rules
The implicit conversions are governed by the following rules:
- Only defintions marked
implicit
are available. - The converter must be a single identifier in scope, or be associated with the source or target type. For example, in the companion object of the source type or target type. The type could be high kind type such as
F[A]
and bothF
andA
’s companion object are available. - Only one implicit is inserted.
- Explicit converter has high priority. Insert happens only when there is a type error.
The receiver conversion servers two purposes: integration of new classes by converting existing ones that can work with the new classes, often using existing methods; supporting new methods to use domain specific languages (DSL).
21.2 Implict Class
An implicit class
is a class that is marked by the implicit
keyword. According to the SIP-13-implicit class
, the purpose is to provide extension methods to another type. The compiler generates an implicit conversion from the class’s constructor parametr to itself. Therefore defining an implicit class also defines a conversion method and it comes with three limitations:
- It must be defined where method defintions are allowed, i.e., within some other object, class, or trait. The compiler will generate a class and an implicit method.
- An implicit class cannot be a case class. Because the generated method has a name conficting with the generated companinon object of the case class.
- Its constructor must have exactly one parameter. The conversion is one to one.
21.3 Implicit Parameter
Implicit parameter is used in method calls and new
class instances. It is used to provide more support functions for an earlier parameter list. It is better than upper bound because upper bound requires a stronger is-a
constraint where the implicit parameter only provides additional functions without changing the existing types.
Though an anonymous function type is enough, it is a style rule to use a meaningful name for the implicit parameter type. This gives the purpose of the function and reduce the chances of unintentional imiplicit usage.
The standard library define an implicitly
method: def implicitly[T](implicit t: T) = t
. Thus t
can be replaced by implicitly[T]
when there is an implicit t
in scope.
The context bound
of [A: T]
adds an implicit parameter of (implicit t: T[A])
as the last parameter list.
Working together, the imiplict class and the implict parameter allow converting a type T
to another type F
to uses methods defined in F
. The F[T]
is called a type class
.
21.4 Resolving Ambiguity
Scala reports an error if there are multiple conversions available. Use specific rules to avoid the multiple conversion problem. An implicit conversion is more specific than antoher if
- The argument type of the first is a subtype ofanother’s
- Both conversions are methods, the enclosing class of the first extends the enclosing class of the latter.
To use the second rule, it is often to define low prirority more-general conversions in a base type and high priority more-specific conversions in a subtype.
Use -Xprint:typer
to see the implicit converters being applied.