Tip:
Highlight text to annotate it
X
[music]. So far, our story for how to implement
this two-dimensional grid of operations and variance has been simpler than it
sometimes is. And in this segment, I want to show you a
very interesting complication that arises. And that is when you have an operation
that is defined over multiple arguments of the kind of data you're defining.
I'll show you an example in just a second. And what we'll see in this segment is that
functional decomposition handles this pretty well.
And in object oriented programming style, we'll either have to abandon OOP or do
something much more sophisticated, and I'll show both of those in the next
segment. So, to show the issue, let's extend our
language with two more kinds of data: strings and rational numbers.
But much more interestingly, let's extend the addition operation to work over any
pair of them. So if you had an int and an int, or an int
and a rational, or a rational and an int, it's mathematics.
If you have a string and a string, it's string concatenation.
And if it's a string and some kind of number, then we convert the number to a
string and then concatenate. That is just the definition of our
language. Now if we want to do it this way, we now
have another two-dimensional grid. It's different than the one we were
talking about before. Which is, to implement addition, there are
now nine cases. It's a binary operation.
There's a left operand and a right operand.
And you need to figure, you need code for every combination of int string and
rational, int string and rational. So in our eval function, we're going to
recursively evaluate the two subexpressions.
We now have three kinds of values in our language, numbers, strings, and rationals.
And addition works on all combinations of values.
So we call addition a binary method or binary operation because it takes two
things of the,where int, string and rational are the possible kinds of things.
In general, you could take any number. But two is already complicated enough.
So, from here, I'm really just going to show you the code and see how we do this
in a functional program. So here is my data type definition, and
you'll see compared to what we had before, I've added string and rational.
Now, we know that when we add string and rational, we have to go and change all of
our old operations, so I've already done that.
Let me show you that quickly before moving on to the main point.
So here's the eval operation. Strings are values, so you just return the
expression itself. Rationals are values, so you just return
the expression itself. Here's the two string operation.
A string, let's just return the underlying string.
A rational has a numerator and a denominator and let's convert it to a
string with this code here. For hasZero, neither a string is never a
0, so we return false. For a rational, return 0 if the numerator
is 0. And for no negative constants, remember,
this was this pre-processing, where we got rid of all negative constants.
For a string, we can just return the expression itself.
It has no negative constants. And for rationals, I went ahead and
removed any negatives in the numerator or the denominator by adding appropriate
negation arguments. But that's really not the focus of this
segment. Okay?
So the focus of this segment is addition. So, up here in eval, let's look at this
add case. Because, as always, I want to recursively
evaluate e1 and recursively evaluate e2. But now, I do not want add to raise an
exception if the results are not ints. That's what mult does.
Look at mult, right? Recursively evaluate if they're both ints.
Then build a new int by multiplying the underlying numbers.
Otherwise, raise an exception. But for add, we decided that any
combination of int, rational, or string, which is the only thing that eval ever
returns, it always returns one of those three, is possible.
And now, we want to add those 2 things together.
So, I have made this a helper function. Just to separate it out, and make clearer
that we are going to use a function to define this green grid.
But you could have done it just as a nested case expression.
Okay? So add values up here, takes in two
things, each of which might be an int, a string or a rational, and adds them
together. So, the most natural thing to do is to
pattern match on the pair so we can lay out the nine cases.
Int string rational cross int string rational.
Nine positions in our grid. And just say what to do in each of them.
It's a great use of nested pattern-matching.
The type checker doesn't know that these vs couldn't be some non-value kind of
expression. So I do have this tenth case that raises
an exception if either v1 or v2 is not some combination of int, string or
rational. And now we just break it into cases.
If I have an int i, and an int j, then I return int i plus j.
That's the case we had even in the previous segment.
If I have an int and a string, then create a new string, out of concatenating,
converting i to a string and s. And you just continue down.
And int to rational, let's just make the rational, which is just i times k plus j
over k. I'm not reducing this or anything, like
we've done before, but it is a correct rational value for adding.
If I have a string and an int, then go ahead and do the appropriate
concatenation. If I have two strings, do the appropriate
concatenation. If I have a string and a rational, then do
convert the rational to a string in a certain way and then concatenate s on the
front. Here's the interesting I want, one I want
to emphasize. If you have a rational and an int, I could
have absolutely just written here, let me just paste it down actually.
This code. Right?
And it would have worked just fine, assuming that I didn't have wild card
patterns over here. But you know, when you're pasting code,
there's often a better way. I could have had a helper function for
this. But the thing to notice about adding a
rational and an int is that you get the same result as if you add an int and a
rational. Right, the same one.
It's a commutative operation to use the math word.
And one, what I did here, which is I would argue reasonable style, is to say I've
already covered this case in the opposite order, so let's just recursively call
add_values with v2 and v1. It's quite common, when you have a binary
operation, to have lots of commutative cases.
And I find this a convenient way to reduce the amount of code duplication, or the
number of cases you have to explicitly handle.
It turns out that in this function, add_values, this is the only case where
I'm able to do this. But in other things, even things you might
see on your homework, there are more situations where this works.
And just to finish up, if you have a rational and a string, it's a lot like
concatenating a string and a rational, but the order matters.
So you can't just treat it as commutative with this previous one.
So for a rational and a string we do this concatenation.
And finally two rationals, a over b and c over d, this is the basic arithmetic that
produces the result of adding them. So this code is functional decomposition
of nine cases. This is our implementation of this green
grid. I find this very natural.
I find it far less awkward and cumbersome than the OOP decomposition of this same
grid that we'll see in the next segment.