You are on page 1of 7

[music] As we approach the end of the course, we still have plenty of new things to learn.

But we'll find ourselves more and more learning them in the context of comparing and contrasting what we already know. In the beginning of this section of the course is no exception, in fact, it's one of the great punch lines of the course. We're going to start by considering how object oriented programming and functional programming, decompose programs into more manageable pieces. So when we're programming in functional languages, what we generally did, was we took a program and we broke it into smaller functions, each function performed some operation over its arguments. In object-oriented programming, we find ourselves breaking a program down into classes, and all the methods of a particular class perform operations over one kind of data, the kind of data that, that class represents. And so what we're going to do over the next few segments, is understand that these two approaches are so exactly opposite, that they're actually just complementary perspectives, ways of viewing the same matrix of operations, and I'll show you that matrix in just a second. So, which perspective, which view is better, is largely frankly a matter of personal taste, but as we'll see in the next segment, it does, it is more then taste, if you think that your software may be extended in particular ways. And then after that, we'll get into the issues that object-orientated programming struggles quite a bit more on certain operations that take multiple arguments, of different varieties, and we'll get there, when we get there. So that's our road-map, and let's just jump into this matrix, by understanding the canonical example people use when explaining this issue. So suppose we have expressions for a small programming language, the exactly the kind of language we considered when we learned to write interpreters, earlier in the course. Well it turns out, that you really have two things, you have different variance of expressions, different constructors. So maybe you have integer constants, you have addition expressions, you have negation expressions, and so on.

Those are different kids of data, you also have different operations. The operation we've focused on is eval, evaluate an expression. But you could have other operations, you could have tostring, that converts an expression to a string. You could have hasZero, that just runs over an arithmetic expression and returns true, if there's a constant zero somewhere in that expression. Syntactically, not evaluating it, just looking for a zero. You could have any number of other operations. So, if you think of this in terms of variance and operations, you essentially have a matrix which is just a two-dimensional grid where, as I've shown it here, you have one row for each kind of data, and one column for each operation. And I would argue, that no matter what programming language you are using to implement this software, you have to decide the correct behavior for every square in the grid. How do you convert an integer to a string, how do you evaluate an addition expression, how do you evaluate a hasZero, and so on. Okay? So you have to fill out the grid and different programming styles just encourage you to fill out that grid in different ways. So let's do the functional or procedural decomposition first. When we're writing in a language like ML or racket, well, let's think about ML. We can define a data type with one constructor for each variant, our data type is saying what the rows of the rid are. Then, in our functional decomposition, we have one function for each column of the grid. We have an eval function, a two-string function and a has zero function. And then in those functions, we use case expressions, to have one branch for each row, for each square in this column. That's how we decompose our program. If multiple of your squares can be implemented in the same way, then you can use wild card patterns or something like that. But fundamentally you're decomposing it in terms of, for each operation which is a function, what are the various branches? So I'm going to make this very concrete,

because we're going to build on this in upcoming segments. Here is the ML code that does exactly that, here is our data type expression, that says what the rows of our table are, and then we have one function for each operation. So here's the eval function, it takes in an expression e. If It's an integer, it just returns the expression. If it's a negate, it recursively evaluates, make sure it gets an int out and it negates it, otherwise it raises an exception, and then I have one more case for that last row of the eval column. Which is to, in addition, recursively evaluate e1 and e2, and because where this is going in future segments, I'm using a helper function here add_values, which is right up here, helper functions are fine, and all this does is make sure that both values are integers, in which case it returns a new integer that adds them together, otherwise it raises an exception. That is only one column of my grid. The next column, toString, is right here, and I have one case for each branch. So in Int, I just call the int.tostring function in ML, for Negate I have some recursion here, for Add I have two recursive calls, and so on, and then a column for the hasZero function, that again has one branch for each row of the table. So those are my three columns. Okay? That is functional programming. Now let's look at OOP. In OOP, the way we would approach this in OOP style, is we would define a class, that describes the idea of expression. Now you don't actually have to do this in Ruby, because it's a dynamically typed language. But I'm going to present it that way, and think of Int, Add and Negate, as all subclasses, of that, superclass. You could justify Int, Add, and Negate as classes that weren't subclasses of anything other than object, but let's think of it as having a class x, within subclasses Int, Add, Negate. So then what we do, is to fill out our table. We have one class for each row. We have the int class, the add class, and the negate class. And then we fill in the entries of that

row, by Int having an eval method, a toString method and a hasZero method, Add having the same three methods, Negate having the same three methods, and those methods say, this is how a negation converts itself to a string. So we are essentially filling out the exact same table, organizing our code into rows, instead of into columns. As you mi, might imagine, I've done exactly that over here in the Ruby file. So again I have this class x, which I don't really need in Ruby. In fact, I'm even going to have a separate superclass for values in my language which currently are only Int expressions. This is similar to the sort of thing you'll do on your homework, which is why I have it, but it is overkill in this example. So now I have one class for each row. So here's my Int class, and if I look further down, here's my Negate class, if I look further down, here's my Add class. Now inside of each of these classes, I fill out the row. Alright? So I have a little bit of stuff for how to initialize an integer which has, you know, one instance variable for the underlying Int itself. But then I do have a method for eval, a method for toString and a method for hasZero. So evaling, an integer just returns the object itself. This is the same thing as in functional programming, in the branch of eval where an Int evaluates to be the entire Int. This object, when you call eval on it, returns self, returns the entire object. For toString, let's just call the toS method on the underling instance variable that holds the number, and for hasZero, let's see if i equals zero. These three entries in our table, are exactly the Int case of eval, the Int case of toString, and the int case of hasZero. We've just put them next to each other, so we can see all the operations on Int, in one place. Negate is similar. For eval, we have recursively called eval on this expre, this, e, this, I'm actually calling the getter method here, I could have just as well just directly done the instance variable. That a matter of dynamic dispatches we've seen. So, take, the subexpression, call its eval

method, then assume it is an Int. Which you can kind of see, because I'm saying .i here. Okay? This is the sort of thing that an ML, being statically typed, I had to have a branch here with an exception. But that's an orthogonal issue, that's really static for us as dynamic typing. And then, that will give me some number, and create a new integer, and that is how you eval and Negate, which is exactly this case of eval in the functional code. And similarly I have a method for converting a Negate to a string, and deciding if a Negate, has a zero in it, which is just recursively seeing if the underlying expression has a zero. And finally Add, works exactly the same way, I have, a constructor here that initializes two instance variables to hold, the subexpressions. I do, up here, see, I have getters for both of those. So now I have an eval method that recursively evals E1, by sending it the eval message, recursively evaluates E2 by sending it the eval message, and putting it together. Same thing here where I'm just assuming the result is an Int, so I'm calling the .i method, you can't do that in a statically typed language, works fine here, and return a new integer. And this exactly the addition case of eval from our ML code, similarly I have a toString method and it has zero method. Okay? So that's the Ruby code, I want you to understand you'll do something similar on your homework. Optionally, I do also have the Java version. Okay? And here, because we're in a statically typed language, your superclass x does need to indicate what methods every x has, but then it's really the same thing. I have an Int class with three methods, eval, toString and hasZero, a Negate class with those methods and an Add class with those methods. And you can look at this code if you're interested in the Java. Okay? So, to me this is a huge punchline of the course, and something you can only appreciate after you've studied functional programming, and object orientated programming, in a precise and conceptual

programming language's way. Which is that FP and OOP are often doing the same thing in the exact opposite way. It's a question of whether you want to organize your program by rows or by columns. And if I put it that way, I think it's reasonable to say, that it can be a matter of personal taste, and that it's a matter of perspective which is most natural. To me, if you're writing an interpreter for a programming language, the functional programming decomposition is just more natural, it's the way I think about it. I'm evaluating an expression and I have separate cases for the different kinds of expression. If I'm writing a graphical user interface, I generally find the object-oriented programming, approach different, I have lots of different things on my screen. For each kind of graphical element, how does it respond to mouse clicks? What colour does it have? What happens if I drag it with the mouse? These are questions I'd like to keep everything about that graphical object together, and I find the OOP decomposition, more natural. And finally, I would say that from this perspective, we're really talking about a matter of code layout. And there's no perfect way to layout your code in a large program because software has so much structure. There's so many connections between different parts of your, your program, but the way we write software in these programming languages, is that we have rows and columns and files. And there's just only so many ways we can lay out the code, and that's why modern development environments provide lots of features for viewing the code in different ways. And in fact, one way you can look at modern IDEs is maybe you're writing in an OOP language, but if you say, well I know my code is laid out in terms of rows, but please find me all the hasZero methods for the subclasses of x, you're essentially asking the IDE to find for you the column, even though your code is ra, laid out in terms of rows. You could imagine an IDE for a functional programming language doing the exact opposite thing. So, programming, these sort of things, like our expression language, is fundamentally about filling out a two

dimensional grid, and we have two programming styles that do it in the exact opposite way.

You might also like