Homework 2 Solution

Due Date: July 5 before midnight;
Max Points: 100

Important Reminder: As per the course Academic Honesty Statement, cheating of any kind will minimally result in your letter grade for the entire course being reduced by one level.

To be turned in via the submission link on brightspace.

Please remember to justify all answers.

You are encouraged to use the web or the library but are required to cite any external sources used in your answers.

It may be the case that some questions cannot be answered as requested.

  1. The solution to Project 1 implements error recovery using an internal undo facility. Discuss how you could extend this internal facility to an externally visible undo-redo facility. 10-points

    The current undo facility grabs hold of the states of a cell in a mapping between cell-id's and the corresponding cells before making any assignment to it while executing a single update to the spreadsheet. Generalizing this to a undo/redo facility would be fairly straight-forward using two stacks: an undoStack and a redoStack:

    1. Reify the list of undo cell states into some kind of Undo object.

    2. As each spreadsheet update is processed, push its Undo object onto undoStack.

    3. To apply an external undo command, pop the last Undo object off undoStack and apply its cell updates. However, before each update is applied to a cell, save the state of the cell in a new Undo object and push the new Undo onto the redoStack.

    4. To apply an external redo command, the redoStack should be non-empty. In that case, pop the last Undo object off redoStack and apply its cell updates. However, before each update is applied to a cell, save the state of the cell in a new Undo object and push new Undo onto the undoStack.

    5. Before any command other than an undo or redo, clear redoStack.

    Note that in (b), it is probably a good idea to put a limit U on the size of undoStack; i.e. only the last U commands may be undone; any attempt to undo more thant U commands would results in some kind of "no more undos" error.

  2. Many of the functions passed as parameters to functions in the JavaScript standard library provide the invoking object as a parameter. For example, when Array:map() is used as in array.map(f), the third parameter passed to f() will be array. Give an example of a situation where this behavior is useful. 5-points

    One situation where the third argument is useful is when the invoking object is an intermediate value in a chain of function applications. For example, the following function applies a function f() to elements of array a and then reduces the mapped array to the sum of each mapped element multiplied by the next mapped element:

    \[ \mathtt{fn}(\mathtt{f}, \mathtt{arr}) = \sum_{i=0}^{n - 1} \mathtt{f}(\mathtt{arr}[i]) \times ((i \;\mathtt{==}\; n - 1) \;\mathtt{?}\; 1 \;\mathtt{:}\; \mathtt{f}(\mathtt{arr}[i + 1])) \]

    where \(n\; \mathtt{==}\; \mathtt{arr}.\mathtt{length}\).

        function fn<T>(f: (_ : T) => number, arr: T[]) {
          const n = arr.length;
          return arr.
            map(x => f(x)).
    	reduce((acc, x, i, mapped) => {
    	    const next = (i === n - 1) ? 1 : mapped[i + 1];
    	    return acc + x*next;
    	  }, 0);
        }
    

    Note that the entire result of the first map() is an intermediate value which is used by the reduce() (in the 4th argument arr to reduce's function argument) to access the value at the next index. Without this additional argument, it would be necessary to explicitly store the result of the first map() in a variable.

  3. Assume that you are a evil spammer. Discuss how would you use JavaScript regular expressions within a program to harvest email addresses from a document. Your program should cope with anti-spam workarounds deployed by users like spelling out email addresses using sequences like user at example dot com. Your answer should deal only with the use of regex's to extract the email addresses; you may assume that the contents of the document are available as a string. 10-points

    A regex like the following provides a good starting point (whitespaces added for legibility):

        /[^\@]+\@[^\.]+(\.[^\.]+)+ |
         \w+\s+at\s+\w+(\s+dot\w+)+ |
         [^\@]+\@(gmail|yahoo|...) |
         \w+\s+at\s+(gmail|yahoo|...)/i
    
    • The first alternative allows traditionally written email addresses user@domain where the domain contains at least one period.

    • The second alternative allows spelled out email addresses of the form user at domain where the domain contains at least one occurrence of the word dot.

    • The third alternative allows email addresses to be written as simply user@domain where the domain is simply the name of popular email providers like gmail or yahoo.

    • The fourth alternative allows email addresses to be written as simply user at domain where the domain is simply the name of popular email providers like gmail or yahoo.

    • Flag i make the regex case-insensitive.

  4. Discuss how you would implement a random() function in JavaScript (without using any library random functions). The details of the random number generation algorithm are unimportant; what is required is the setting up of the function so that subsequent calls usually return different results. 5-points

    The essential idea would be to retain a state like seed but to hide it within a closure. So something like the following IIFE:

          const random = (function() {
            let seed = ...;
    	return rand() { ... }
          })();	
    
  5. Assuming no earlier variable declarations, the following JavaScript code will output 4 when run in non-strict mode.

        x = 1;
        obj1 = { x: 2, f: function() { return this.x; } }
        obj2 = { x: 3, f: function() { return this.x; } }
        f = obj1.f.bind(obj2);
        console.log(obj1.f() - obj2.f() + obj2.f.call(obj1) + f());
    

    Explain why this is so. 10-points

    The call to obj1.f() has this set to obj1; hence it will return obj1.x which is 2. The call to obj2.f() has this set to obj2; hence it will return obj2.x which is 3. The call to obj2.f.call(obj1) has this set to obj1; hence it will return obj1.x which is 2. Finally, the f() call has this bound to obj2, so it will return obj2.x which is 3. Hence the value printed will be 2 - 3 + 2 + 3 which is 4.

  6. Novice programmer Ben Novitia asks you for help in identifying the problem in the following nodejs code:

          
          ...
    
          const DIR = `${process.env.HOME}/my-stuff`;
    
          function readFile(filename) {
            fs.readFile(`${DIR}/${filename}`,
    	  function(err, result) {
                return result;
              });
          }
    
          const contents = readFile('test.data');
          console.log(contents);
    

    What do you tell Ben? 5-points

    fs.readFile() is asynchronous. Hence the return result statement in its handler does not serve to return a value from readFile(). The only way to log the contents is to move the console.log() statement into the handler for fs.readfile().

  7. Multiple objects obj1 and obj2 have a common interface \(I\) (i.e. the same set of methods) but all the methods in obj1 are async whereas the methods in obj2 are synchronous. There is a function f(obj1) which is set up to expect a read-only obj1 argument; so the code for f() assumes that obj1 methods always return promises. Hence calling f(obj2) would not work because even though obj2 has the same methods as obj1, those obj2 methods do not return promises.

    How would you set things up so that f(obj2') can be successfully called, where obj2' is derived from obj2.

    You answer should not assume anything about \(I\), obj1, obj2 or f(); in particular, you should not assume that you have access to the code for obj1, obj2 or f(). 15-points

    The key is to set up asynchronous functions in obj2' which wrap the synchronous functions for obj2.

    So something like the following pseudo-code:

        const obj2' = Object.assign({}, obj2);
        for (const [k, v] of Object.entries(obj2)) {
          if (typeof v === 'function') {
            obj2'[k] = async function(...args) { return v(...args); };
          }
        }
        f(obj2');  
    
  8. The following is an attempt to log the connection information for a mongo database:

          const MONGO_URL = 'mongodb://localhost:27017/data';
          ...
          function getConnection(url=MONGO_URL) {
            const result = mongo.connect(url);
            return result;
          }
    	
          const conn = getConnection();
          console.log(conn);
    

    Assuming that mongo contains a MongoDB client object, what is wrong with the above code? 5-points

    mongo.connect() is asynchronous. Hence the return result statement in does not serve to return the connection. Instead it will return a Promise for the connection; to get the connection, it is necessary to either chain off the result using a .then() or await the results of the connect() call.

  9. A web application needs to make asynchronous calls to independent web services \(s_1\) and \(s_2\), followed by an asynchronous call to a web service \(s_3\) which combines the results from \(s_1\) and \(s_2\) into a single result.

    1. How would you set this up so as to maximize performance.

    2. Assume that web service \(s_1\) is not particularly reliable and that an alternate web service \(s_1'\) is available which is functionally equivalent to \(s_1\). How would you modify your answer to the previous question to make the application more robust by using the results from either \(s_1\) or \(s_1'\) depending on which of \(s_1\) or \(s_1'\) responds? 15-points

    The answers follow:

    1. Since the calls to \(s_1\) and \(s_2\) are independent, they should be run in parallel. Since they are asynchronous, they can be set up to return promises and we run them in parallel using something like Promise.all([s_1(), s_2()]). Then the result of those parallel calls can be provided sequentially to service \(s_3\).

      	const results = await Promise.all([s_1(), s_2()]);
      	return await s_3(...results);
      
    2. Use Promise.any() to run \(s_1\) or \(s_1'\) in parallel with the first one which succeeds determining the result. So something like:

      	const s1Promise = Promise.any([s_1(), s_1'()]);
      	const results = await Promise.all([s1Promise, s_2()]);
      	return await s_3(...results);
      

      Note that Promise.any() is preferable to Promise.race() as Promise.race() will reject if the first settled promise rejects, whereas Promise.any() will reject only if all promise arguments reject.

  10. Write a function fib(n) which can be used as in

        for (f of fib(6)) console.log(f);
    

    to print out successive Fibonacci numbers 1, 1, 2, 3, 5, 8. The return value of fib() is not allowed to be any kind of collection. 5-points

    Use generators:

        function* fib(n) {
          if (n > 0) yield 1;
          if (n > 1) yield 1;
          let n1 = 1, n2 = 1;
          for (let i = 3; i <= n; i++) {
            const f = n1 + n2; n1 = n2; n2 = f; yield f;
          }
        }
    
  11. Discuss the validity of the following statements. What is more important than whether you ultimately classify the statement as true or false is your justification for arriving at your conclusion. 15-points

    1. this for a fat-arrow function can be changed using call().

    2. The await keyword can only be used within a function declared async.

    3. A JavaScript event handler will run soon after the event is received.

    4. A function declared async can be called without using an await.

    5. A function called using await must have been declared async.

    The answers follow:

    1. For a fat-arrow function, this is always the value which was captured lexically within the scope in which the fat-arrow function was defined and it is not possible to change it using call().

      So the statement is false.

    2. This statement was true until recently. However, in the past couple of years, JavaScript has started allowing a top-level await at the top level, outside of any functions. Hence the statement is false.

    3. The event handler will run after the event is received and the currently running code has completed (because of JavaScript's run-to-completion semantics). Hence the statement is false.

    4. A function declared async can indeed be called without using an await as long as the caller is prepared to handle the returned Promise. Hence the statement is true.

    5. A function which simply returns a Promise but not declared async can be called using await. In fact, even a normal synchronous function can also be called using await. Hence the statement is false.