# Illustrated guide to Types, Sets and Values.

This document is a graphical exploration on the relationship between Types, Sets and Values.

The goal is to help develop an intuition about types by representing them graphically in a very concrete way: as labels or post-its attached to expressions.

Audience: *Beginner*.

We’ll be talking about two different notions of functions:

i) Mathematical functions. They are abstract, and can be defined between two arbitrary Sets. They “live” in our heads, so to speak.

We’ll use the notation

`A → B`

in this case.ii) Scala functions and methods. We’ll use

`A => B`

in this case.The two notions are

notthe same: a Scala function corresponds to a mathematical function only when it is pure, and there are many mathematical functions that are not expressible as Scala functions.

# 1. Preliminaries

## 1.1 Types as labels

We’ll start by declaring types to be just **labels** (think of tags / post-its, etc) assigned to expressions.

## 1.2 Types and values

Let’s call `𝕋`

the set of all types, `𝕍`

the set of all values, and consider the function `V`

that maps each type to its set of possible values:

` V: 𝕋 → 𝒫(𝕍) `

Note:

𝒫(𝕍)is thepower setof

𝕍(the set of all its subsets).

As you can see, the function* *`V`

allows us to distinguish values of different characteristics from each other.

In a dynamic language such as Python on the other hand, all values have a single label assigned to them, which we can call `Dynamic`

.

You might be thinking that even in Python there are different kinds of values: numbers, lists, dicts, etc.

This is true: they definitely have a different representation in memory, but the distinction between the number `1`

and the string `"one"`

exists only at **runtime**. As far as the interpreter is concerned they are the same, and will happily attempt to perform `1 / "one"`

only to throw an exception.

`>>> 1 / "one"`

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

TypeError: unsupported operand type(s) for /: 'int' and 'str'

What about people writing or reading the program? Well, we *try* to be aware of such a difference, with varying degrees of success.

So when we talk about types, we are talking about a **static** property that the typechecker can verify without running the code, not the specifics of the in-memory binary representation of values at runtime.

## 1.3 Some examples of types and corresponding set of values

In this list, some types are “atomic” or “simple”, while others are derived from other types by combining them in different ways.

This is going to be one of our main themes: we’ll systematically explore different ways to *combine* types and analyze the resulting *set* of values.

Another important theme is that in order to keep things simple we’ll basically ignore the *meaning* of values of a given type. Values will be represented as mere featureless points in a set.

# 2. Simple types

First thing first: how do we even create new types?

Consider the following definitions:

At this point we have introduced 4 new types (labels) into our program:

# 3. Families of types: Functions in 𝕋

A trait declaration such as

defines a (math) function from types to types:

` F: 𝕋 → 𝕋`

A ↦ F[A]

We can *apply* this function to a type, and the result will be another, **brand new** type.

We’ll represent type functions as incomplete labels (or labels with holes). Once the missing label is provided we get a proper label.

Using `List`

as an example:

The top rectangle represents `{𝕋 → 𝕋}`

, the **set of all (simple) type functions**, of which `List`

is a member.

To be clear: `List`

is **not** a Scala function. But it is a mathematical function.

In the expressions above:

`List`

is not a type, it needs a type argument to become a type. It is a*type constructor*.`List[Int]`

**is**a brand new type.`List[Try]`

is invalid, because the declared argument of`List`

has to be a proper type.

Even though `List[Int]`

and `List[String]`

are different types, they are clearly related (and we can expect the corresponding values to also be related somehow). Thus type functions allows us to create *families* of related types:

` { List[A] | A ∈ 𝕋 }`

Observe that this is just the image of `List`

.

Another example:

One difference between type functions and regular (math) functions is that applying type functions just “accumulate” its arguments. i.e. there is not automatic simplification.

For example:

`List[Either[(Int, A), Map[Int, String]]]`

(hence the need for type aliases).

## 3.1 Functions of multiple arguments

Otherwise identical to single-argument functions, except with more than one hole.

For example, the signature of `Map`

is:

` 𝕋 × 𝕋 → 𝕋`

The signature of a type function is called its **kind**:

- Regular types have kind
`Type`

(i.e. 𝕋 ). They are also called*proper*types. `Map`

and`Function`

have kind`(Type, Type) => Type`

(i.e. 𝕋 ×𝕋 → 𝕋 ).

## 3.2 More type functions

Before continuing let’s examine different expressions that give rise to type functions:

Note: The square block

∎is not valid Scala syntax!

It’s just a way to represent the fact that a type is missing at that position, and if we fill the gap, then we will have a proper type.

Vanilla Scala 2.x does not provide native syntax to create anonymous type functions, but we can use the compiler plugin Kind Projector to overcome this limitation.

For example the type function `Map[Int, ∎]`

obtained by omitting the second type argument of `Map`

can be expressed using kind projector as `Map[Int, ?]`

.

Dotty on the other hand adds the syntax `[A] =>> F[A]`

for this purpose.

## 3.3 (Type) function composition

As expected, type functions can be composed.

For example, the composition of `List`

and `Future`

is the function `F[A] = Future[List[A]]`

` F: 𝕋 → 𝕋`

A ↦ F[A] = Future[List[A]]

## 3.4 Identity function

As with any set, there is an identity function in the set of types `𝕋`

. This can be expressed in Scala using a *type alias* like so:

`type Id[A] = A`

## 3.5 High order functions

We can also describe high order type functions that receive (simpler) type functions as arguments.

A high order type function has an “hole” with a very specific shape: *only simpler* type functions can be used as arguments. Once this argument is provided the result is a proper type.

Since `H`

takes a function and returns a proper type, it has kind `(𝕋 → 𝕋) → 𝕋`

.

Here’s an attempt to represent this graphically:

More examples:

Taking the idea further, how about a function that accepts arguments of a shape/kind like `Functor`

?

`trait `**N**[G[_[_]]]

takes**N***one*argument, named`G`

.- The argument
`G`

is a type function; it takes one*anonymous*argument of shape`𝕋 → 𝕋`

(i.e. a type function such as`List`

). - Hence the signature (kind) of
`G`

is`(𝕋 → 𝕋) → 𝕋`

. - And

has signature (kind)**N**`((𝕋 → 𝕋) → 𝕋) → 𝕋.`

// valid:N[Functor]N[Monad]// invalid, not the right shape:

// N[List]

// N[Int]

Graphically:

# 4. Subtypes and Sets

In Scala and similar languages the set of types `𝕋`

has the structure of a lattice under subtyping:

` A ≤ B ⟺ A <: B`

The above diagram of

𝕋does not show all subtype arrows; it only shows the "direct" or "minimal" arrows. But since subtyping is a transitive relation, if

A <: Band

B <: Cthen

A <: C.

Using our mapping `V`

between types and sets of values we get an analogue structure on 𝒫(𝕍):

` if A <: B then V(A) ⊂ V(B)`

To `A <: B`

we can associate the inclusion function `i: V(A) ↪ V(B)`

between the corresponding set of values.

# 5. Set related operations

The correspondence between subtyping and subsets mentioned above naturally leads us to consider what other operations on types can we do based on operations on the corresponding set of values.

Let’s start by creating a little dictionary between sets and types.

## 5.1 Union of two types (Dotty)

The union of types `A`

and `B`

is the type `A | B`

defined as

` V(A | B) = V(A) ∪ V(B)`

i.e. its values are the union of the values of `A`

and `B`

.

Properties:

`|`

is commutative and associative.`A`

is a subtype of`A | B`

(and similar for`B`

)- If
`A <: C`

and`B <: C`

then`A | B <: C`

## 5.2 Intersection of two types (Dotty)

The intersection of types `A`

and `B`

is the type `A & B`

defined as

` V(A & B) = V(A) ∩ V(B)`

i.e. its values are the intersection of the values of `A`

and `B`

.

Properties:

- If
`A <: X`

then`A & B <: X`

- If
`B <: X`

then`A & B <: X`

- In particular
`A & B <: A`

and`A & B <: B`

- If
`T <: A`

and`T <: B`

then`T <: A & B`

which are rather natural if we think on its effect in the corresponding sets of values.

Intersection of types is defined in a *structural* way in the sense that values of `A & B`

must have all the *properties* (members) of `A`

and all the members of `B`

.

Note: In Scala 2.x the keyword `with`

can be used for similar purposes. The main difference is that it is not commutative.

# 6. Subtypes and type functions

Let’s discuss now the interaction between subtypes and types functions.

## 6.1 Domain and image of type functions

The **domain** of a type function `F`

is the set of all types `A ∈ 𝕋`

for which `F[A]`

is defined. The **image** of F is the set of all types of the form `F[A]`

for some `A`

.

In the examples we’ve seen so far our functions (such as `List`

or `Future`

) have been defined for all types, making the *domain* the whole set of types `𝕋`

.

On the other hand, the *image* of `List`

is a proper subset of `𝕋`

:

What if we want to restrict domain of a type function to be not the whole `𝕋`

but rather just a proper subset of it?

There are different ways to accomplish this, as we’ll see in the following sections.

## 6.2 The set of subtypes of a given set

The simplest way to come up with a proper subset of types (and corresponding values) is to use subtyping.

Consider the set `Sub(A)`

in `𝕋`

of all the subtypes of a given type A:

` Sub(A) = { Y | Y <: A } ⊂ 𝕋`

(This is sometimes described as `↓ A`

)

For example, given

Then

`Sub(Pet) = {Pet, Fish, Dog, Null, Nothing}`

Graphically:

Analogously for supertypes.

## 6.3 Type constraint: Invariant type functions

Consider this definition:

`F`

is a type function whose argument `A`

is now restricted to be a subtype of `X`

. In other words

` dom(F) = Sub(X)`

(If `X`

happens to be `Any`

then we're back to the "no restrictions" case).

`F`

as defined does not preserve the subtyping relationship: the elements of its image have no subtyping relationship amongst each other.

## 6.4 Type constraint: Covariant type functions

If we add the `+`

annotation then we're declaring that subtype relationships (arrows) will be carried over to the image of `F`

:

## 6.5 Type constraint: Contravariant type functions

The annotation `-`

will reverse the subtype arrows:

Note: Variance annotations

+,

-cannot be applied to anarbitraryfunction

F[_]. They are only allowed on type functions whose implementation satisfy the variance rules (more information).

Subtyping gives an easy way to create sets of types and sets of values. The downside is that it’s not very flexible, in the sense that we cannot add pre-existing types to a given hierarchy (i.e. we cannot retroactively make one type extend another).

The **Type Class** pattern provides a way to overcome that limitation. Before examining it we need to talk briefly about implicits.

# 7. Implicits

The implicit mechanism allows us to *mark* a value of a given type (say `a: A`

) and elsewhere in our code summon this value (i.e. retrieve it, get a reference to) by referring to the type `A`

.

In practice this amounts to having a **compile time** function from types to values (called `implicitly`

in Scala 2.x).

`implicitly: 𝕋 → 𝕍`

A ↦ a ∈ V(A)

for example:

implicit val ec: ExecutionContext = ???// laterimplicitly[ExecutionContext] == ec

Given that in general there can be many values of a given type, in order for this mechanism to work: 1) the specific value `a`

has to be selected *beforehand* by marking it as an implicit value, and 2) we cannot have more than one value marked as implicit at the point where it is summoned.

Another interesting aspect of this function is that even though its codomain is the whole set of values `𝕍`

, the actual result is not arbitrary, but rather it's always a value of the given type.

# 8. Type Classes

Quoting Scala with Cats:

A

type classis an interface or API that represents some functionality we want to implement.… [It] is represented by a trait with at least one type parameter.

It has three ingredients:

**1) A Type Function**: capture all the desired properties of a type `A`

in a trait parameterized by `A`

.

Example: Monoid

**2) Instances**: Make a given type a member of the type class by creating implicit lawful instances for required types:

“Make a given type a member” is not something that has a native construct in Scala. Instead, it is understood in the following sense:

We’re going to

overloadthe word Monoid to refer to the set of types for which a lawful implicit instance of`Monoid[A]`

exists! (see diagram below).

“Lawful” means that our instance actually passes the tests asserting the Monoid laws.

So given the definitions above we can say that “`Int`

*is* a Monoid", *because* there is an implicit instance of `Monoid[Int]`

in scope.

Consider the following function (and diagram below):

`theMonoid: 𝕋 → 𝕍`

A ↦ implicitly[Monoid[A]]

(for example`theMonoid[Int] == intMonoid`

)

`theMonoid`

maps a type`A`

into a*single*value: the implicit instance for`Monoid[A]`

.`theMonoid`

is*only defined*for types with an implicit instance of`Monoid`

. Otherwise you get a compilation error.

This means that the “*Monoid”* type class is just the domain of `theMonoid`

.

Note: Even though the type function

`Monoid`

can be applied to any type`A`

(hence the domain is all`𝕋`

), we can't necessarily create a lawful instances for every such`A`

.For example,

`Monoid[Char]`

is a valid type, but as long as there is no lawful implicit instance of it in our code then we don't consider it to be a member of the "Monoid" Type Class.

**3) Client code**: program against this trait whenever the functionality is desired:

`combineAll`

is actually a family of functions (one for each type `A`

):

` {combineAll_{A ∈ 𝕋}: List[A] × Monoid[A] → A}`

We can see that if no instance of `Monoid[X]`

(for a particular `X`

) exists, then there's no way we can use `combineAll[X]`

.

Type Classes gives us the ultimate flexibility to define subsets of types with desired properties: since we can “add” members one by one as we did above.