We have now put the lot of the material developed in this course to work in a concrete case study. The task is to find a pure functional solution to run on problem the so-called water pouring problem. Another variant of the problem is found in the course by Peter Norvig, called Designs of Computer Programs and Udacity where Peter develops a very nice solution in Python. The solution developed in our course differs from his in some respects, first, it's purely functional, and second, it's also more general. You might find it instructive to compare the two solutions, if only to see that there's different ways to skin a cat. The final program we are going to develop in this course is the solution to a well-known problem called the water-pouring problem. The idea is as follows, let's say you are given a faucet and a sink, And a number of glasses. Right now, I will just draw two of them, of different sizes. So, just for the sake of the example, let's assume this glass has size four deciliters, and this glass has size nine deciliters. What you need to do is put use of a given quantity, let's say, for the sake of the argument, six deciliters of liquid in one of the glasses, It doesn't matter which one. However, you're not giving any marking on the glasses. So, the only knowledge you have is the total capacity of the glasses. And the moves that are available to you is, you can either fill a glass completely using the faucet or you can empty it in the sink or you can pour from one glass to the other, until either the glass from which you pour is empty, or the glass into which you pour is full. Now, the classical water pouring problem uses two glasses of the given sizes and the given target size of six deciliters. But we are going to generalize that a little bit. We want to have an arbitrary number of glasses of arbitrary given capacities, and an arbitrary target capacity in one of the glasses. So, let's see how we would model this problem. First, how do we represent the state of the glasses? Well, the idea would be to represent a glass as an int ranging from zero to the number of glasses minus one and then our state would be a vector of int that would give us, for each glass, the number of decilitres that are in that glass. And then the question is what kind of moves do we have? Well, we have three, we can empty a glass, you can fill a glass, or you can pour from a glass to another glass. So, let's see if we have, let's say, the two glasses of size four and nine, and we start in a state where both glasses are empty so that would be state zero and zero how that would evolve? So, one thing we can could do is we could fill glass number zero. And that will bring us into the state four and zero. Or we could fill glass number one. And that would bring us into the state, zero and nine. From there on, we could pour, starting from this state here, from glass zero into glass one, that could, would give us the state zero, four, the action with be pour zero to one. Or, we could for instance, pour one to zero here. Then, we would end in a state where the first glass has capacity four its full. And the second glass, has five ounces in it because that's what remains. Sometimes, moves lead back to state that we've visited before. For instance, if we empty the glass one in this state here, Then we would be left with a state four, Zero, which we can also reach shorter by just a single move here. Now, you've seen how moves span out. Question is, how do we find now the right solution? How do we generate moves so that we find the right sequence of moves to lead us to our target capacity, let's say, that's six here? So now, that we know what moves are, let's see how we can use them to find the solution to our problem. The idea would be that we generate all possible move sequences, call them paths, Until we hit on one that contains one of the glasses with the right target amount of liquid in it. So, we would then start from an initial state of all glasses empty. And then, generate all possible moves to new glasses. Once we have generated all possible moves of length one, we will generate then all possible moves of length two and so on, Like an onion, Until possibly we hit on a path where we would have one of the glasses with the target amount of liquid in it, in this case, six. The idea then, is that we generate these path sets from inside out, starting with the shortest ones and progressively lengthening the paths until we hit one which is the right one, or, that's another possibility, until we have exhausted our search space and there is no solution. So now that we have an idea for a solution, let's see how we can put it in code using the Scala Eclipse IDE. What I going to do is I will, I am going to create a glass. I'll call it pouring to contain the elements of the problem. So, pouring should take as a parameter, a vector of all the glasses and their capacity. So, that would be a vector a vector of int. One entry per glass, and for each entry, its capacity. The first thing I'm going to do is, I'm going to work on states. So what is a state? Well, a state is a vector of integers. One thing we could do to make that clear is make a type alias, saying, state equals vector of int. The next thing to do is, well, let's define what the initial state is. So, the initial state would consist of all empty glasses. So, what we're after is a vector of the length of capacity consisting of all zeroes. There are several ways to do that. Probably, the most elegant way would be to use a map. So, what should I apply in the map while it's a function that takes a glass, an integer, and returns zero for any integer it gets. Good. So now, we have the initial stat, how can we move from that state? Let's define what are our moves are. The idea there is that I would have a glass for each move. Let's make it a case glass for convenience. And they all would inherit from a common base trait move. So, I would have trait move here. And I would then define three case glasses, empty. Fill and pour that all extend move. So now, that we know what types of moves are available to us, we still need to generate all possible moves, so all possible moves would be empty, an arbitrary glass. Fill an arbitrary glass or pour from an arbitrary glass into another. So, let's generate those. As a, as an eight here, I have defined one auxiliary state of structure, all glasses that will be arranged, that goes from one until capacity length.. So, what would the possible moves are? Well, one way to do that would be to define it as for-expressions. Let me do that. So, let's say for g taken from glasses, Yield empty g. So, those are the first moves available to me, empty and arbitrary glass. Other moves are for g taken from glasses, fill g. And finally, for from, taken from glasses to, Taken from glasses if. From is different from to. We can't pour from one glass into the same. Then yield, pour from and to Okay. So, let's set up a test to see what we have so far. I'm going to create a new worksheet. Call it test. And what I am going to do is, I will set up an instance of my glass. We have to define a vector of initial capacity of glasses. Let's say, the first glass has capacity four and the second has capacity seven. And now, what I want to do is I want to say, well, what moves are available to me? And what we see here is I get a vector to say empty zero, empty one, fill zero, fill one, pour zero, one pour one, zero. So, those are all the moves that I have. But, of course, I could also generate another glass. Let's put another glass nine in here. And then I would have more moves that you can see over here. So, three empties, three fills, and six pours. Back to our glass. The next thing to consider is how moves change states. There are a number of ways we could record that. What I'm going to do here is I'm going to write a method change in the move trait that will have to be implemented by each case glass. So, change is defined on a move and it takes a state. And it gives us a new state. So, that would track how each move changes the state. Let's implement this method for each of our case glasses. So, for the empty case glass, how is the state changed? Well, we thought the new state would be the old state. With the updated methods, so that, there's a change at one point in the state. At what point is it? Well, at my glass, which now will be empty. So, it's the old state updated at the point of the glass where it's now empty. But remember, that updated doesn't destroy the state, the old state will still be available, it's just that, a new state object that gets generated by this updated method. Updated is a purely functional method here. Good. So, let's see for fill. Here, our change would be that the glass is updated not to be zero, but to be full to its capacity. So, glass gets updated to capacity of glass. And finally, for pour, There, the problem is a bit more complicated, because it depends whether one glass can fully fit into another or not. So, what I want to do is, I want to first define the amount that gets poured from one glass to the other. So, what would that be? Well, It could be that we take everything that is in the from glass in the given state, provided that there's enough room in the to glass. Or if there's not, then it could also be that we fill the to glass to its capacity. So, it would be state of capacity of to minus state of to. So, the amount is the smaller of the current filling grade of the glass we pour from and the free space in the glass in which, into which we pour. Now, that we know the amount to pour, the state can be updated straightforwardly. So, it's the old state updated at the from glass, where the new value of the from glass is the previous value minus amount. And the new value of the to glass is the previous value of the to glass plus the amount. So now, that we've seen moves and the changes they do to states, it's time to look at paths. So, paths would be sequences of moves. I'm also going to define a class for paths, so I define a path by its history, which is a list of moves. And the idea would be that the history is taken in reverse that makes it easier to extend the path with the new moves. So, that means that the last move in the path comes first in that history list. So, what I use for operations on our path? Well, one operation I would be interested in is, what state does it lead to? So, let's define that end state. And will, that would be a state. How do I define that? Well, one way to do it would be by a pattern match over the history list, and that would be a recursive function. So, let's try that. So, let's call this, track state of history. And we would have a auxiliary function, Which would be defined by a pattern match over an argument list. If the argument list is Nil, then we return the initial state. If it consists of a move and some remainder excess one, Then what we would do is we would track the state of the rest, remaining list. Remember, the last move comes first in the list. And then, we would apply the first move to change the result of that. So, that's simply move, change the result of track state. Because track state returns the state, and move is a change method that changes the state to give the new state. So, that computation of end state works, but let's look at it closer for a moment. Let's look at it graphically. So, what we have here is we have essentially done a set of changes with moves, where each cursive call is a previous version of the history, until initially when the list is Nil, we return initial state. So, what does that remind you of? Well, it's a foldRight. So, it's a foldRight where we go through the list, and we combine it each time with the change operator. But we can reformulate it as follows. We take the history, we do a foldRight. We state the initial state at the lower right as initial state. And our operation is, The one way we take each move from the history and we change each state on the right. And that will do exactly the same thing. Now, the new formulation is, without a doubt, much shorter and some would argue more elegant, than the recursive pattern matching solution. But for most people and particular beginners, it's also much harder to come up with and maybe to read. So, if you or your team would decide at some point that you prefer the recursive explicit pattern matching solution, that's perfectly okay. So, what we can do now is we can delete our previous auxiliary method track state. We have now condensed it into the foldRight and obtain a solution that's much shorter for pass. So, what else do we need to do on a path? Well, one useful operation is no doubt to extend it with another move. And that would just be a new path where the move precedes the history that we have so far. And finally, it's always good to be able to print objects in an intelligible manner so let's define a two-string function. So, to print the path, we want to print its history. But you probably want to reverse it first, so first moves first and later moves, later. And we want to print it, let's say, with a space between different moves. And finally, it's also good to know where the path leads to, so we are, interested in its end state, so let me write it this way. . So, that was paths. Well, that's defining the initial path. The initial path then would be the path that contains the empty history. Now, we have all the elements that we need for our solution. Let's take a quick look at the diagram. So, you see here that we would have a method that extends a set of paths with new moves, successively of longer and longer lengths. So, the way we will put that in code is that we have a function very similar to the from, from integers. Only now, we don't start from an integer and say, generate all successive integers, We from a set of paths. And we generate a stream of set of paths, so that means you generate longer and longer set of sets of paths in a string. So, one thing to take care of is the boundary condition what if path is empty. In that case, there is nothing to evolve so we should return the empty string. If paths is not empty, what do we do? Well, The idea is we need to generate all the possible new paths that, I have the paths in the set as a prefix. And from then on further more, evolve the stream. So, let's do the first thing first. So, let's call the new paths, more. So more gets generated by a for-expression. We let path iterate over all the paths in our set. And then for each of them, we generate a next path by extending the current path. And extending the current path, we do with all possible moves. So, the way we do that is that we have the call moves map path.extend. For each of the possible moves, we apply the operation extent with that move to the path and that gives us a new path. And all those mixed paths would yield my more set. Now, that I have my next generation set more, I can use that to define the full stream that starts with the given path set. And that is followed by the evolution of my next generation more. So, I, it would start with paths, then more. Because that would be the paths for the next iteration from more. Then more of more. So, twice paths of, of lengths two added to paths, and so on. So, that's my path-generating function So, let me call that pathSets. And that would be from applied to the set that consists only of the initial path. So, what that set gives us is, in the first set, it gives me set of initial path. In the second element, it gives me the set of all paths of length one that start with initial path. Then, all paths of length two and so on, until infinity. So now, that we've that much, we can have a quick look in the worksheet to see what we get. So, let's take a problem, and take all the path sets. Maybe reduce the size of the problem like this. So, we would get a stream, Which is the vector of 0,0. And then, because it's a stream, it doesn't evolve any further. So, let's do the usual trick, and say, let's take three elements and convert to a list, and see what we get. And we get an impressively long list of possible moves that are all the moves that we generate here in the int states. So now, that we have all possible paths, let's see how we would find the solutions. So, let's define a function solution. It takes a target, which is the volume that we want to see in one of the glasses, an int, and it should give us back a stream of paths that have the target volume in the end states. So, how would we go about defining that function? Well, the idea is to go through all the pathSets and from each of the pathSets, pick the paths that are solutions, that have the target volume in their end state, and finally, to concatinate them all in another stream. So, the result stream would then consist of all solution paths ordered by their length. So, let's do that. So, it would be a for-expression where we first go through the pathSets which are taken from the pathSets value here. And then, for each pathSets, we go through all of the paths in that set. And then, we want to demand that the path is a solution. So, what would that be? Well, it would be that the end state of the path contains the target volume. Then, the path is a solution and, in that case, we return it in the resulting string. Now, that we have the solution sets, let's put it to a test in our worksheet. Let me define problem that solutions of, of let's say, six. And I get the solution, let's see what it is. So here, we would have a sequence of moves that yields to the vector 4,6. That's fine. Let's try something else. Let's maybe try the original problem with nine. Then, we see it, unfortunately, it takes already a long time. That's not so good so it seems we have found a solution but not a very efficient one. So, let's go back and see what the problem could be. So, one problem is if we look at the diagram, that we move blindly. That means we generate from each possible state, Let's say, we have a state here, We generate new states, but we will also generate a lot of old states. So, we do a sort of random walk on the states, constantly revisiting also old states. And these old btates don't really bring anything to the solution Because when we go back to an old state, that's not a path we want to consider because we, by definition, there is a shorter path that leads to the same state. So, the problem we have is that all this exploration is rather aimless. And a better way to do it would be to exclude an state that we have visited before. So, let's try to do that. So, the idea would be that, in my stream generating function, I would have a second set, now a set of states that represent the explored states. And I will now restrict my moves to those that don't lead to a state that's already explored. So, what I would write here is, let's say, explored is not allowed to contain the end state of next. Now, it, I still have to fix up some bits that now yield type errors. So, the first thing is in the recursive call to from, I have to pass a new explored set, so that would be the old one. And also for each of our moves, our path, and also, for each of the paths in more, its end states. So, That would be more map end state. Finally, for the initial call to from, I would have the set of states that's initially explored is just the initial state. Okay, so with that solution, we can try again. And now, we have a solution to this problem, which is slightly longer than the one we've seen before, also, in very little time. We can also try something else. We can add more glasses, such as, let's say, nineteen. And we want to find seventeen. We can try that. And again, we would get solutions fairly quickly. Let's look at the problem again. We have a workable solution. It's reasonably efficient. Can we make it more efficient? Is there, is there room for optimization? Well, one area where we could optimize is the endState method. So, endState gets called a lot, you see, in the, in the exploration of possible solutions. And each endState is a foldRight over a complete history. So, a recursive function over the history as paths get longer, and state becomes more and more complicated to compute. There again, you could say, well, why recompute the end state of a path? Can't we just restore the end state once and for all in the path and that would avoid the recomputation? So, let's try that. So, What I'm going to do is I'm going to put the endState in the path. That means, I can remove it here. And because it should be available from the outside, I make it a L parameter, so now I can see it. And now, I have to compute a new endState when I compute a new path. So, what would be the endState be of the path that consists of the given move last, and the history before that. Well, it would be the change effect on the move, on the previous endState. So, that's the new one, where if you add a move to a path, then we add the new endState that consists of the change effect of the move on that endState. And the initial path then would be, The endState is the initial state. And if we do that and tested again, we find the same solutions. And maybe, it's a tad quicker. It definitely will become more apparent as paths would get longer. So, that completes the solution. So, what we did is, we modeled the problem in a glass pouring. We first modeled states, and we modeled moves and the changes they have on states. And we generated all the moves in this variable here. Then, we modeled paths as their own class. And finally, we modeled the state exploration function from as a for-expression. And the function that picked out all solutions from the generated paths as another for-expression. So, this was quite a complex program. In particular, there is a lot of choice of representations. So, we picked specific glasses for moves and paths but we could also have taken some encoding. We picked object-oriented methods. We could also have done some naked data structures with functions. Present elaboration is just one solution not necessarily the shortest ones but I believe it's actually quite clear from a domain modeling standpoint. Now, you might ask what are good guiding principles for a program like that? In the end, it comes down to experience. As the saying goes, you can learn programming in ten days, and then you improve for ten years. But there are a couple of guidelines anyway, that are useful. So, I believe the first guideline is, name everything you can. Function programming is great because it allows you to break up things in really small pieces, Expressions that consist of maybe just three words. Make use of that. Some of the assignments I've seen have crammed dozen or more operators on a single line. Taking advantage of the conciseness of functional programming in Scala. But I don't actually think that's good style. So, break things up in little pieces, Give a name for each piece, that makes programs much more intelligible and readable. So, for instance, we put the change method inside the move classes, because a move changes things. So, it makes sense to define change right where you have the move. And finally, when you do a design, keep in mind that you always want to keep degrees of freedom for future refinements. So, think of what might change in the future and how you could encapsulate this, the implementations from clients so that you could change it in the future without changing any of the client code.