Tutorial

One Function, Many Ranks

1

D

Let's run programs with Tensors.
Quick warm-up: what is 1 + 1?

2

W

2. But are those even Tensors?

3

D

Yes, 1 has type Tensor[int][[]].
Try this next: Tensor([1]) + Tensor([1]).

4

W

Hmm... if we break them into smaller tensors, then at the matching position, we have 1 + 1 again--a problem we've already solved.

5

D

Great instinct. Indeed, we add the matching positions.

6

W

Then it is Tensor([2]), right?
Likewise, Tensor([1, 2, 3]) + Tensor([5, 7, 9]) gives Tensor([6, 9, 12]).

7

D

What about Tensor([[1, 2, 3], [3, 2, 1]]) + Tensor([5, 7, 9])?

8

W

Wait, their types are different: Tensor[int][[2, 3]] and Tensor[int][[3]].
Can we add Tensors with different types?

9

D

For this one, yes. A Tensor[int][[2, 3]] contains two Tensor[int][[3]]s.
So we run + twice. Each still adds matching positions.

10

W

Right.
First: Tensor([1, 2, 3]) + Tensor([5, 7, 9]).
Second: Tensor([3, 2, 1]) + Tensor([5, 7, 9]).
Then we combine the results as Tensor([[6, 9, 12], [8, 9, 10]])?

11

D

What is the result type?

12

W

The result is Tensor[int][[2, 3]], the same as the larger input shape.
It's a nice trick to split the larger Tensor!

13

D

The trick is called rank polymorphism.

14

W

An intimidating name!

15

D

It's actually warmer and fuzzier than it looks. Here, rank is just the length of the shape.
Tensor[int][[3]] has rank 1, and Tensor[int][[2, 3]] has rank 2.

16

W

Then 42 has type Tensor[int][[]], so rank 0?

17

D

Yes.
When we design a function, we define its expected types for inputs and result.
Rank polymorphism lets us apply that function to higher-rank inputs when they are compatible, and it wraps the result type accordingly.

18

W

So one function works across different input ranks.
Is + a function?

19

D

+ is a function. Just like
5 has type Tensor[int][[]],
+ has type (x: Tensor[int][[]], y: Tensor[int][[]]) -> Tensor[int][[]].

20

W

This function type suggests three things:
+ expects two inputs, x and y.
x is Tensor[int][[]], and so is y.
The result is also a Tensor[int][[]].

21

D

Now imagine applying + with 1 as x and 1 as y.

22

W

1 is Tensor[int][[]], so both inputs match exactly.
Therefore 1 + 1 has result type Tensor[int][[]], which is the type of 2.

23

D

Next: Tensor([[1, 2, 3], [3, 2, 1]]) + Tensor([5, 7, 9]). In the earlier exercise, we virtually ran the program and found Tensor[int][[2, 3]] by looking at the result value.
Now we derive the result type, without running the program.
Let's start by validating the inputs.

24

W

Here, x has type Tensor[int][[2, 3]] and y has type Tensor[int][[3]].
Neither exactly matches the expected Tensor[int][[]].
But they must be compatible, since we added them just now!

25

D

We need a rule for the compatibility between List[int]s.

26

W

Let's see it!

27

D

Two List[int]s are compatible, if we line them up from the right and find a suffix in the longer one that matches the shorter.* The remaining part in the longer list is called the prefix.
Find the suffix and prefix for x and y.

28

W

For x, compare [2, 3] with []: suffix [], prefix [2, 3].
For y, compare [3] with []: suffix [], prefix [3].
That means: each given input is compatible with its expected type!

29

D

Great. Next, validate compatibility between the two prefixes.

30

W

Does compatibility also work on prefixes?

31

D

Yes. Compatibility applies to List[int]s. Prefixes are List[int]s too.

32

W

So we compare [2, 3] with [3].
The suffix is [3] and the prefix is [2].
That means the prefixes from the inputs are also compatible.

33

D

Good progress! So far:
for each input, the given type is compatible with the expected type;
the two prefixes from the inputs are compatible with one another.
So Tensor[int][[2, 3]] and Tensor[int][[3]] are valid inputs for x and y in +.

34

W

Nice. What's our next step?

35

D

Now we wrap the result type. Under rank polymorphism, we repeatedly apply + to generate many Tensor[int][[]]s.
The number of repetitions depends on the longer prefix.

36

W

Got it. The longer prefix is [2, 3], so we wrap Tensor[int][[]] with [2, 3]. That makes Tensor[Tensor[int][[]]][[2, 3]], which simplifies to Tensor[int][[2, 3]].
Exactly the same as running the program!

37

D

Great work!
Try a few failing examples too: cases where a given type is incompatible with an expected type, or where the prefixes are incompatible.
It helps to internalize this trick.

38

W

Will do.

39

D

Let's recap rank polymorphism:
for each input, validate compatibility between the given and expected types, find the prefixes;
validate compatibility between those prefixes;
wrap the result type with the longer prefix.
This process applies to all functions.

40

W

Are there other functions besides +?

41

D

Yes, you can define as many as you need. We'll cover that in the next chapter.
Time for a break!

42

W

Okay, ciao!

* We match the ints in two List[int]s in matching positions, from the right end. Two ints match, if they are identical, or if either of them is 1.