[SOUND] In this segment, I want to show some more examples of using nested
pattern matching. So let's just do it.
Let's jump over here and let's write some code.
The first example I want to write is a function that I'll call non-decreasing.
it's just going to take a list of integers and return a [INAUDIBLE].
And true means that, at no point in the list, do you find a number that is
smaller than the number that comes before it.
Alright, so if we did this our old fashioned way with simple pattern
matching we might case on whether the list is empty or not.
x colon onto x is prime. And the empty list, that's definitely
true. You never see a decreasing situation.
But in this case, I kind of need to look at the next element as well.
And so you might, now do another pattern match on that tail of the list.
And if that were empty, well this would be the situation, where you have a one
element list. And that's also non-decreasing.
Otherwise, you would add that next element of the list, and then really, the
tail. And you would have to say, make sure that
X is less than or equal to Y and also that, we were non-decreasing.
Actually, and a common bug would be to say y is
prime, that's actually wrong, you actually need x is prime, in order to
make sure you're checking every position, and not just every other position.
So this would work, but these nested case expressions are a little clumsy.
And with nested patterns, we can express what I consider to be a more direct and
elegant algorithm. So in, the empty list case is just fine.
I have no problem with that. But now, let's use nested pattern
matching to handle the one element case directly.
So this pattern will match when x's is some head of the list, which we can match
against x, and then the rest of the list matches against the empty list.
So this pattern will match exactly against one element lists.
So we can just write true. And then, if that does not match, now we
can have another pattern, which is maybe, let's say, head cons onto neck.
Get it, get it? That's the thing in the list after the
head. Maybe on to rest,
okay? and this pattern matches all lists that have two or more elements cause head
will match the first, neck will match the second, and rest will match the rest of
the list. And now we can just check that indeed
head is less than or equal to neck. And also non-decreasing, neck cons onto
rest, all right, something like that. And that is it.
That's our whole function, and the type checker will actually make sure that our
patterns are exhaustive, and indeed they are.
We have a case for the empty list, a case for the one-element list, and a case for
all lists that have two or more elements. All right?
So that's another example of massive pattern matching that I actually rather
like. one little thing we can do to clean this
up. Whenever you have a variable that you're
not using in the corresponding branch, you can also write underscore.
That's a slightly better style. What it does, is it's just like a
variable pattern, it always matches, but it doesn't actually introduce a variable.
And that makes your code a little easier to read.
It says to the reader of your code, that you don't actually need what's in that
position, you just need that it's there. All right, so that's fine.
Now let's do another example. this one's a little sillier but it shows
something that I find quite common and convenient.
I'm going to define a little data type for the sine of a number, so p for
positive, n for negative, z for zero. And now what I want to do is write a
little function, multsin, it's going to take a pair of integers.
And it's going to return. So it takes two integers.
It's going to return the sign of the number you would get if you multiplied
those numbers together. But it's not actually going to do the
multiplication. I'm going to define a little helper
function here inside my function that just tells you the sign of a number.
and so if you have zero then you end up with z.
Otherwise, you get positive of x is greater than zero, is negative, and I'm
going to use that to figure out the sign that you get when you multiply X1 and X2.
Now it turns out there's in the extreme nine different cases based on the sign of
X1 and the sign of X2. Three possibilities for each, three times
three is nine. and if I tried to do this with nested
case expressions it would get a little messy.
But what if I just pattern matched on the pair, of calling sign with X1 and calling
sign with X2? And now what I could do right here is
simply have my nine cases. So I could have z comma z and have a case
for that. I could have p comma z.
I could have n comma z and so on. And I would continue that for nine cases.
And it would, the code would read like a nice table of exactly what the answers
should be for each combination. But we can do even let, better than that
when we realize that patterns are matched in order.
And so we know that if either thing is Z, the result will be Z, because if you
multiply anything by zero you get zero. And so let's use these nested patterns to
clean that up a bit. And let's say that if I have this first
pattern. So if the sign of X1 is zero, then no
matter what the sign of X2 is. So I could have a variable here like Y,
or I could use underscore since I don't actually care what that sign is.
And that's it. That's three of my nine cases, just
handled like that. And here's another three.
If the second position is z, then the entire thing is z.
So I've already handled six of my nine cases.
And now, in handling, the other, the remaining cases.
I can use the fact that I now know neither position will be z,
because I've already handled all those cases.
so another possibility is if both things are positive.
Then the result is positive. Another possibility is if both things are
negative, then the result is positive.
And there's two more cases, N comma P which would be negative, and P comma N
which would be negative. And, not only do I have a nice table
here, but my type checker will again check I haven't left anything out and
that's pretty neat. by the way, you can do this slightly
differently. You could just here at the end use a
single wildcard which will match anything including a pair and say hey, in all
remaining cases, the answer is negative. Now you'd better get rid of these or the
type checker will complain that you have unreachable, impossible cases.
and now whether this as I now have it written, is better style.
Or the one where I don't have this line, and instead I have these two as better
style, is really a matter of taste. The way I have it now is a little bit
shorter. It kind of reads nicely, it says,
otherwise the result is negative. But you are giving up a little bit of the
type checkers helpfulness, because if I wrote this.
The type checker will still say that this pattern match is exhaustive, but my
code's now wrong, because I forgot the case of N comma N, whereas if I leave
this case out and do it this way, here, the type checker will, in fact, give a
warning, telling me that the N comma N case has been forgotten.
In fact, lemme show you that. Use more nested patterns dot SML.
And in fact, up here, it says, warning, match non-exhaustive.
And then you can go and look at it, and try to puzzle out which case you forgot,
okay? But nonetheless, let's just leave it as
this one, since that's the shortest version and sometimes people like to see
nice and short code. Let me finish up with one more much
simpler example. which is just to compute the length of a
list, so we've done this plenty of times, at least things very similar to it.
If you have the empty list then return zero, otherwise x pull in, x is prime,
one plus line of x is prime. And this is just another example even
though there's not anything particularly nested or fancy here, where I would argue
it's a little better style to go ahead and put this underscore here to emphasize
that we don't care about the value at the head of the list, we just care that we
have a non-empty list, and we do need the tail because we need to call Len
recursively, with x as prime. All right,
so those are examples, let me switch back to the slides very briefly here to just
talk a little bit more about style and how these nested patterns lead to elegant
and concise code, and give you an intuition on how to look for
opportunities for nested pattern matching.
And the first is to try to avoid, where convenient, nested case expressions.
If instead of having nested case expressions, you can just have more
branches using nested pattern matching, it often leads to simpler code.
We saw that with unzip three in the previous segment, and we saw that with
non decreasing in this segment. That's not to say that nested case
expressions are always a bad idea. It's just a good hint to yourself to look
and see if nested patterns might be a little better.
Another very common idiom is to match against a couple of data types.
So instead of pattern matching against one data type, and then another data
type, and then another one, go ahead and match against a couple all at once.
We just saw that with the malt sign example where I matched against two
things of type sgn. We also saw that in the zip3 example.
Where we actually matched against a triple of lists and that was in the
previous segment. And finally, as a separate issue of
style, I do encourage you to use wild cards instead of variables.
A variable and a wild card both match against everything.
The difference is the variable actually introduces a local binding for the thing
it matched against. And the wild card does not.
And when you don't need that corresponding data in the branch,
wildcard concisely communicates that to the person reading your code.
That is instant pattern matching. In the next segment, we'll take a step
back, and give a more precise definition of how nested pattern matching is
actually defined in ML and similar programming languages.