In the third and final part of the Discrete Event Simulation we will look at the implementation of the core libraries. And then we will put everything together, and run a concrete simulation to demonstrate and test how everything works. All we have left to do now is to implement the simulation trait. The principle idea is to keep in every instance of that trait an agenda of actions to perform. The agenda would be a list of simulated events. And each event would consist of an action and the time when that action should be produced. We sort the agenda list in such a way that the actions to be performed first are in the beginning, that way we can simply pick them off the front of the list to execute them. That leads to the following start of the implementation of the trait simulation. There's a type action that takes an empty parameter list to unit. Then we would have a class of events. For convenience, we make it into a case class. That way it's easy to [INAUDIBLE] that an event would have a time when it should be executed and an action which gives us the function that should be executed. There's the agenda, which is just a list of events. And that's the private variable agenda of type Agenda, which is initially the empty list. To handle time, we do that with another private variable, call it curtime, that contains the current simulation time, and that one can then be accessed with the getter function currentTime. So the implementation of the method afterDelay with a given delay and a given block, would then insert the task event at current time plus delay to the action that when invoked will perform the operations in the block. And it would put that event into the agenda list at the right position. Here's the implementation of afterDelay. As we've seen, it creates the event at the given time with the given actions to perform and it inserts it into the agenda. The implementation of the insert function is straightforward. Remember what it needs to do is insert an event at the right place in the list that it appears at the right position given its time. So we do that by doing a pattern match on the agenda. If the agenda has a first element that has a time that is less or equal to the current item's time, then we insert the item in the rest of the list, because we should still keep first as the first element. If that's not the case that means the list is either empty or the first element of the list should appear at a later time than the current item. In both of these cases we take the item as first element of the list and follow with the agenda. The last part of the simulation implementation has to do with actually running the event loop. So that event handing loop would remove successive elements from the agenda. That would be correct because we know that the agenda is already time sorted and performed the associated actions. Here is the implementation of the event handling loop. It performs a pattern match on agenda. If agenda is non empty then it will strip off the first item from agenda, set the current time to the indicated time of that item, perform the items action and continue with a recursive call to loop. If the agenda is empty then the simulation has ended and the function can accept. So to run the simulation, we simply call loop(), but before we do that we install our first action to be performed and that action actually tells us that the simulation has started and to what the current time is. Before we can launch the simulation, we still need a way to examine the changes of the signals on the wires. So far, the simulation doesn't give us any output, so we would be flying blind. To change that, we define a function probe. So probe is something that you can attach to a wire. Sort of like an oscillator that tracks a digital signal. Something like that. [NOISE]. And the form it takes here is just like a gate or another component. So probe takes the form of a component, much like And gate or all the other components in the system. Here's the function probe. You attach it to a wire. It has a name that is typically the name of the wire. It has an action, the action consists of printing the name of the wire, the current time, and the new signal on that wire. And what it would do in installation is it would add this action to the wire. So, as always, the action gets executed each time the signal on the wire changes. What the example also shows is a neat feature of in string formatting. So you can write a string like this. You prefix it with an s, which means it's of string interpolator and then you can write anything you want to embed in the string with a dollar and the name of the thing that you want to embed. Or if the thing that you want to embed is more than a single identifier, you can put it in braces and write that here. So, a string that you see here, you could have done also by concatenating strings with plus, would look some thing like this, name + space + current time +value = + wire.get signal. The two would print to the same strength but of course the first one is far easier to understand what actually gets embedded where in the string than the second version that relies on string called get connection. The last thing we have to do before we can start the simulation is define all the technology dependent parameters. Remember, that our gates use constants InverterDelay, AndGateDelay, and OrGateDelay which were abstract on the level of basic circuit simulations. We need to define them to fit the technology that we use in the simulation. A convenient way to do that is to pack them all in a separate tray that can then be mixed in in the actual simulation object. So you could define a trait parameters with the delays, and then your actual simulation object would extend the circuits class with parameters. If you look at the class diagrams then there would be a another trait called Parameters, that gets inherited from the concrete simulation objects. Let's try out a concrete simulation before we do that in a worksheet, let's take a tour of the classes that we've seen so far. So, that's first, the simulation class that you have seen here with its action type, its event type, and the simulation API which consists of the method afterDelay run and CurrentTime. We then have the level of gate where we have the three delays as abstract method, so these need to be filled in later on. And we have the definition of the wire class that we have seen and then of the gates, inverter gate, and gate, and or gate. And we also put for convenience the probe method on that layer. Finally, we have the layer of circuits which contains things like the halfAdder that you have seen and the fullAdder. And the last part is Parameters, which is the trait that fixes the technology specific constants. So here we have taken 2 for InverterDelay, 3 for the AndGateDelay, and 5 for the OrGateDelay. With all the in place, let's launch a worksheet that does a concrete simulation. So I create a new Scala worksheet, call it Test. What I need to do is first create an object for the actual simulation. So that would extend the circuits that I have defined so far and would mix in the parameters. I import everything in that object for convenience. Then I can get started by creating some wires for a halfAdder. That's called in1, in2, and the output wires would be sum and carry. So that would give us four wires that we see here. We next connect these wires with a halfAdder. So we write halfAdder(in1, in2, sum, carry) and to see something we put probes on the two output wires, so we would have our probes on the sum wire. And the probe carry on the carry wire. So that already gives us immediately the values of the wires. Sum and carry would both be false. That's a side effect of immediately executing an action once we install it and the action of the probe is simply to print the current value of the wire. To run a simulation that change a single of one of the wires, so, let's say, let's put a one on, in1 and run the simulation. What we see is the welcome message simulation has started at time 0 and at the time 8 new-value of the sum would be true. The value of the carry has not changed so we don't see anything there. We can continue by placing an input signal on in2 and running the simulation again. What you would see now is that the simulation has restarted at the time [INAUDIBLE]. The last time that we saw a signal change earlier. And now, we see at time 11, the carry signal would get a new value, true. And sometimes later at time 16, the sum would be false. We can now retrack the signal on in1 and run the simulation a third time. And that would give us a time 19 that the new carry value is false and to sum value is again true at some later time. So I hope that gave you a little taste what you can do with simulations. Of course, you could also in the same way simulate other circuits and also create your own circuits to simulate them. Let's finish the simulation by studying a variant of the gates that we have seen so far. We have seen so far the Or gate, in a manner completely analogous to the andGate. It defines an action that gets installed on its input wires. But in fact an alternative way would be to construct an Or-gate out of the And-gate and the inverter, because after all we can make use of the formula that a or b is the same as not not a and not b. So to implement that formula, inverters and And-gates are enough. We can have a circuit like this one here. You have the a and the b, then we invert both inputs. We make an And-gate and we invert the output. So that leads to this circuit here where we have three internal wires. Not in one, not in two, and not out. You place the invertus at the And-gate as you see here. My question to you is, if we replaced our implementation of orGate by the alternative orGate that you've just seen, what would happen? What you compare to the earlier simulation would you see a change? Possible answers are no, nothing would happen, the two simulations behave the same. Or maybe the two simulations would produce the same events, but the indicated times are different. Or maybe the times are different and also the alternative application of orGate could produce additional events that you don't see in the original simulation. Or as a fourth choice, the two simulations would produce completely different events all together. What do you think? Let's try this out, I have changed the alternative version of the orGate to the standard one, and I rename the first standard one to Alt. So let's launch the simulation again with this alternative version of the orGate and what we see is that initially we get some glitches on the sum value. So I change this value before we can stabilize. We see at time 5 it has a value of true, at 10 it is false and again at 10 it is true. So there's some different behavior here. Let's see whether we can explain that. So what I've done here is I've placed the new version of the orGate into the halfAdder circuit, and let's see what happens. So initially the value of this wire here would be 0, after 2- Time units, the inverter would make that wire to be 1, after again, 2 time units that inverter would make that wire to be 1. So, after 5 time units, the andGate here would return a true value, a 1 value. After 10 current time units, similarly, the initial signal here, the 0 here, would propagate to be a 1 here, a 1 here, and everything would flip around. And again, at the same time after 10 time units, the final set value of the signal a would be take place. The propagation would chase each other and we'd put use the final value true for the sum signal. So, the moral of this is that in these circuit simulations you have to give it sometime to let the circuit stabilize itself, that's also true for the actual simulations. So the initial results here can sometimes be misleading. So the answer, which maybe was surprising to you would be the times are different and the added version of orGate can also produce additional events, why? Because it's built from more components that take more time to stabilize themselves. So to summarize, it's seen that adding state and assignments makes our mental model of computation more complicated. In particular, we lose the property of referential transparency, which says that it doesn't matter whether we use a name or the thing it refers to. We've seen with the bank account example that it matters quite a lot, but we refer to it in an existing bank account or we create a new one. The other thing that we lose is the substitution model so we do not have any more an easy way to trace computations by rewriting. On the other hand, assignments allow us to formulate some programs in an elegant and concise way. We've seen that with the example of discreet event simulation where a system was represented by a list of action and that list was a neutral variable, it changed during the time of simulations. The effect of the actions when they're called would, in turn, change the state of objects. And they could also install other actions to be executed in the future. You've seen that in this way, combining higher functions and assignments in state led to some very, very powerful techniques that let you express fundamentally complex computations in a concise and understandable way. In the end, it's a trade-off. You get more expressiveness that helps you tackle certain problems in a simpler way. But on the other hand, you lose tools for reasoning about your program preferential transparency into substitution model. So, I guess the moral would be that you should stick to the purely function model whenever you can. And you should use state responsibly when you must.