0:00

Over the first two weeks we have introduced a model of evaluation of programs

based on term rewriting. That was the so-called substitution model.

It's a fair question to ask whether when we have now these new concepts,

classes and methods and self-references, whether these concepts can be ported to our

evaluation model. Whether they can be extended to cover these as well.

In this session you're going to find out how this is done.

So far we've only developed this model for pure functions.

We have defined the meaning of a function application using a model based on substitutions.

And now we extend this model to classes and objects.

The first question we need to ask is how is an instantiation of a class

like new C with some argument expression evaluated.

The answer is surprisingly simple. We will evaluate the expression arguments,

e 1 to e m. Just like the arguments of a normal

function. And that's it.

In fact the resulting expression new c of some values, v one to v m is already a

value. So we just take these new instance

creation expressions as values. Next question.

Let's suppose we have a class definition, like the one you see here.

Where we have a class C with a method F. And the formal parameters of the class are

named X1 to XM. And the method F has parameters Y1 to YM.

The function parameters or the class parameters can both be empty.

And for simplicity we have omitted any parameter types.

Now the central question is if we have an expression like that.

We have a class instance new C of V1 to VM, and we call them F at F, and pass it

further arguments values, W1 to WN. How is that evaluated?

Let's see how we would answer these questions.

So as a reminder we would have the class, C, and that has the X parameters.

And then it has a function f. And that would have the y parameters.

And a body B. If you have an expression like this one

here, then it is re-written not using one substitution as for plain functions, but

three substitutions. The first substitution is the one you have

seen before. We would have to replace the actual, the

formal parameters of the function f with the actual argument values w1 to wn, so

that's that substitution that you see here.

The next substitution affects the class. So we have former parameters of the class,

this also have to be replaced by the actual argument values that we have here

when we create the class. So that it gives us the second

substitution. The third substitution is important,

because the body of the function F here could contain

reference to this, the current class itself and of course that reference to

this outside of the class wouldn't have any meaning.

So we have to replace it with something else.

What do we replace it with? Well, the idea is simply the receiver of

this call. So the value new C v1 to vm

itself, so that's the object that takes the method call and so that's the

object that replaces "this". So you see, there's three substitutions at

play. It's quite a bit more complicated than in

the purely functional model, but it's still the same model so we can model

evaluation with exactly the same mechanisms as before.

So let's demonstrate that with an example. We're looking at the method called new

Rational 1, 2 dot numer. As a reminder, I've put up the essential

parts of class rational here. So let's see what happens for this method

call here. I put up the formal definition.

We have three substitutions at play. There's a substitution for the class

parameters where the actual values one, two replace the x and the y.

For the method in this case, there's nothing to replace because numer doesn't

have any parameters. And finally, any occurrence of this would

have to be replaced by the object itself so that we would be new Rational(1, 2).

In this case, it turns out that the right hand side of numer is very simple, it's

just x. So the only piece

of this substitution that applies is the left most one here.

One for X and the result is one. Let's do a more complicated example, let's

see whether new Rational of one two is less than new Rational of two three.

How would we go about that? So here you see the definition of less.

So we would have three substitutions again.

There's the substitution of one and two for x and y, as before.

Then there's the substitution of the argument here, a new rational of two,

three for the That parameter of Less. And finally, there's a substitution that

replaces the "this" in less's body by the receiver of the call, new Rational of (1, 2).

And all these three substitutions are applied to the body of less that you see

here. So here, it's written out in slightly

nicer fonts. But that's the body that you see here.

Note that I have made explicit that this is the argument for references

because we need that for the substitution model to work correctly.

So what that means is that, if I apply the substitutions, then the "this" gets

replaced by new Rational (1, 2). So that's the two occurrences you

see here. And the "that" gets replaced by new

Rational (2, 3). That's the two occurrences that you see

here. And if we then apply the further

substitutions for numer, and an analogous substitution for denom, then we arrive at

this expression here, which reduces to true, as usual.

So one more thing we want to cover this

session is operators. You see, in principle the rational numbers

defined by our class are as natural as integers, and mathematical abstractions

just as good as integers. But for the user of these abstractions

there's currently a noticeable difference. You write if x and y are integers, you

write x plus y. But if R and S are rational numbers, you

need to write r.add(s). So that's not very natural, and in fact,

in Scala we can eliminate this difference. To do that, we proceed in two steps.

At the first step, we introduce in fix notation for methods.

It turns out that any method with a parameter can be used like an infix

operator in Scala. So instead of having to write r.add(s),

you can write just as well, r add s.

R dot less s becomes r less s, and for maximum it's the same thing.

So the two things mean exactly the same things.

The left sides expand to the right sides. You can do that for any operator that you

have in Scala. The second step is about relaxing the form

of identifiers. Normally in programing languages,

identifiers are alpha numeric, so they start with a letter, then they are

followed by a sequence of letters or numbers.

In Scala you have an alternative form of identifiers where identifiers can be

symbolic. They start with an operator symbol, such

as plus or minus, anything other than a letter or a digit, and they're followed by

other operator symbols. In this treatment, the underscore

character underscored that we use a relaxed notion of identifiers in Scala.

Normally in programming languages, an identifier is alpha numeric.

It starts with a letter and it's followed by a sequence of letters and numbers.

In Scala, operators are also treated as identifiers, and to achieve that we

have introduced a second form of identifiers which we call symbolic.

Such an identifier starts with an operator symbols such as plus, or minus or question

mark, and is be followed by other operator symbols.

In the definition, the underscore

character, as usual, counts as a letter. And there's a final twist, we can also mix

alphanumeric and symbolic. We can start with an alphanumeric

identifier, followed by an underscore, then followed by some operator symbols.

So here I give you some identifiers in Scala.

x1, times, +?%&, that's not necessarily an identifier I would recommend you use,

vector underscore ++ or counter underscore =. And all these names are legal

identifiers in Scala. So let's see how we could apply that to

class rational. Let's start with the first operator, less

here. So a better name for less would be just

the less than or equal sign. Then we would change max accordingly.

To say if this less than that, and the program here compiles again.

And the usage of course, it would be the same thing.

I would write X less than Y. For maximum, I think maximum is a nice

operator as it stands, but I can write it infix.

Now that we've dealt with less and max. What about add and sub?

So for add, we would, of course, find plus.

For sub, we would, find minus. And the definition of sub would be this

plus that dot neg.

And in the code here, I can now use the arithmetic operations

as I'm used from mathematics. Now the final operations to look at would

be neg. You've seen that we can replace all the

arithmetic operations on rational by the natural mathematical symbols.

But there's still neg which sort of sticks out because, instead of neg, we would want

to write prefix minus. So how can we achieve that?

So if you look at the error message here, it tells us, well, unary underscore minus is

not a member of rational. So what that means is that, because the

prefix operator minus is really different from the infix operator minus,

there's a special convention in Scala. We have to call it unary minus.

And if we do that, then,

we see that, now everything works out, and we also get prefix operators.

One thing to guard against is that if a method name

ends in an operator symbol, then you need at least a space between that and the

final colon. Because otherwise, a colon is actually a legal

symbol. So the colon would just be merged

with the operator name, to form one long operator minus colon.

So that would be here, an error, that you see.

So, you've seen that we can now use the usual mathematical expressions like, x

times x plus y times y. But, there's one issue still, namely about

the precedence of the operators. As you're used, the times here binds

stronger than the plus, so implicit parenthesis go like this. But you might

ask, well how is that actually achieved if all operators are user defined, how is the

precedence between those operators established?

Well, there's actually one universal rule in Scala, that the precedence of an

operator is determined by its first character.

And here's a table of all the characters and the precedence categories.

You will note that the table is very similar to precedence in a language such

as Java or C, and that's no accident. So, the first thing to do is to say, well

the lowest precedence is every operator that starts with a letter.

So alphanumeric operators have lowest precedence.

Then we take essentially the precedence groups of C and Java.

So the next lowest is bar, followed by caret, followed by ampersand, followed by

less than, greater than, equals, bang. The colon is inserted the colon is not in

java so we inserted in here then plus minus and finally times, slash and percent.

And every character which not in this list, every symbol character which is not in

this list is assumed to have a higher precedence than everything else.

You might say why these rules and not others and I don't really have a good

answer for that. The only answer is that it's important

that everybody uses the same precedence rules because otherwise it's very hard to

read other people's code. So that's why Scala has opted for a

single rule that determines the precedence of each operator.

So as an exercise for this session, I would like you to ask to provide a fully

parenthesized version of this string of this expression here.

What I mean by that is that you should put every binary operation into

parenthesis in a way that the structure of the expression doesn't change.

So let's see how we would go about that.

In this string here, what's the operator with the highest precedence?

Well it turns out it's the ?^

because question mark was not in our list yet it's a symbolic character.

So those are assumed to have the highest precedence.

So we are safe making a pair of parentheses around this one first.

The next highest one then would be the plus that we see here.

So that gives us here a pair of parentheses. Followed by the equals sign here.

So the arrow here would be in parentheses.

Of the remaining three, the next highest one would be the caret.

Then the bar, and finally the lowest precedence

would be the less. So that's the fully parenthesized version of

the string I'm after. Now, this might be a fun exercise

but you see that, if you are actually asked to read a string like that, then that's

maybe less funny.

Of course it's perfectly fine to define a

mathematical operator such as plus or bar. For something where this operator makes

sense. And in some domains it's also perfectly

fine to define new symbolic operators that have a special fixed meaning in these

domains. But please don't go overboard inventing

fancy operator names for all the operations in your API.

It's usually not appreciated by the users because they will have a hard time

understanding that and also will have a hard time setting the parentheses as you

see here.