Homework 1 Solution

Due Date: Jun 20 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. For problems 1-12, please submit a file hw1-sol.ts which can be compiled using:

$ tsc --target es2022 hw1-sol.ts --outFile hw1-sol.js

[The provided tsconfig.json file is set up so that simply typing tsc in the directory containing hw1-sol.ts will compile it with the above options. A skeleton hw1-sol.ts file is also provided.]

For the remaining questions, please submit a PDF file named hw1-sol.pdf.

Justify all answers.

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

Note that some of the questions require you to show code. You may use a JavaScript implementation to verify your answers.

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

Many of the questions are meant to familiarize you with the built-in functions available in JavaScript (and many other languages) for arrays and strings.

Notes:

  • We use C:fn() to refer to C.prototype.fn().

Restrictions

Some of the questions specify "Subject to the above restrictions". These restrictions are to force you to write code in a strictly functional style without any mutation. The specific restrictions are:

  • Your code may not make any explicit use of mutation, iteration or recursion.

  • You code may not contain any let or var declarations.

  • The answer provided for a specific question may contain only a single top-level function.

What you are allowed to do:

  • Your code may declare const variables with an initializer.

  • A function may contain nested functions.

  • A function provided for a particular answer may call a function defined in an earlier answer.

  • You may also use the full power of JavaScript regular expressions for functions which manipulate text.

  • You may use any String, RegExp, Array, Number or Math functions which are used only for their return value and not for any side-effects. So for example, you may use Array:sort() if you are only using its return value and not for the side-effect of changing its argument. You are not allowed to use Array:push() as it is destructive, but you may use Array:concat().

Some hints for writing code subject to the above restrictions:

  • In the absence of assignment and iteration, the only sequence of statements you can write are a sequence of zero-or-more const declarations followed by a return statement, or if-then-else statements with the bodies of the then and else subject to the same restrictions. In some cases, if-then-else expressions using ?: may be preferable to if-then-else statements.

  • Use higher-order Array functions like map() and reduce() to replace the use of iteration.

  • Note that the functions provided to many of the Array functions like map() and reduce() take multiple arguments including the current index of the element being operated on and the array being operated on.

  • The Array.from() factory method may be useful for setting up initial arrays. Specifically, Array.from({length: len}) can be used for creating an array of length len and Array.from({length: len}).map((_, i) => i) can be used to set up an array of indexes in [0, len).

  • Warning: One of the bad parts of JavaScript is that when return'ing a value from a function, the expression representing the returned value must start on the same line as the return keyword. So

        return
          expr;
    

    will return undefined, but

        return expr;
    

    or

        return (
          expr
        );
    

    will work.

  • To get an idea of what is expected, look at the Fibonacci function discussed in class.

  1. Subject to the above restrictions, show code for a function norms_2(pairs: [number, number][]) which when given a list pairs of 2-element list of numbers, returns a list containing the Euclidean norm of each pair. Note that the norm of a pair \([x, y]\) is \(\sqrt{x^2 +y^2}\).

        > norms_2([ [0, 0], [4, 3], [3, 4], [12, 5], [1, 1] ])
        [ 0, 5, 5, 13, 1.4142135623730951 ]
        > norms_2([])
        []
        > norms_2([[-3, 4], [4, -3]])
        [ 5, 5 ]
        > 
    

    Hint: use Math.sqrt(). 4-points

        function norms_2(pairs: [[number, number]]) {
          const norm = ([x, y]) => Math.sqrt(x**2 + y**2);
          return pairs.map(norm);
        }
    
  2. Subject to the above restrictions, show code for a function max_norms_2(pairs: [number, number][]) which when given a non-empty list pairs of 2-element lists of numbers, returns a number giving the maximum norm of all the pairs in the list.

    Hint: Use Array::sort(). 4-points

        > max_norms_2([ [0, 0], [4, 3], [3, 4], [12, 5],
                        [1, 1] ])
        13
    

    We use norms_2() from the previous question to compute the norms. We then sort the norms in non-ascending order and then return the first element of the sorted array.

        function max_norms_2(pairs: [number, number][]) {
          const norms = norms_2(pairs);
          const sortedNorms = norms.sort((a, b) => b - a);
          return sortedNorms[0];
        }
    
  3. Subject to the above restrictions, show code for a function norms_3(triples: [number, number, number][]) which when given a list triples of 3-element list of numbers, returns a list containing the Euclidean norm of each triple. Note that the norm of a triple \([x, y, z]\) is \(\sqrt{x^2 +y^2 + z^2}\). 4-points

        > norms_3([[2, 1, 2], [2, 3, 6], [4, 7, 4],
                   [1, 1, 1], ])
        [ 3, 7, 9, 1.7320508075688772 ]
        > 
    
        function norms_3(triples: [[number, number, number]]) {
          const norm = ([x, y, z]) => Math.sqrt(x**2 + y**2 + z**2);
          return triples.map(norm);
        }
    
  4. Subject to the above restrictions, show code for a function sum_of_squares(nums: number[]) which when given a list nums of numbers, returns the sum of the squares of the numbers in nums. If nums is empty, then it should return 0. 4-points

        > sum_of_squares([3, 4])
        25
        > sum_of_squares([12, 4])
        160
        > sum_of_squares([1, 2, 3, 4, 5])
        55
        > sum_of_squares([12])
        144
        > sum_of_squares([])
        0
    
        function sum_of_squares(nums: number[]) {
          return nums.reduce((acc, e) => acc + e*e, 0);
        }
    
  5. Subject to the above restrictions, show code for a function norms(tuples: number[][]) which when given a list tuples of number tuples (represented as lists), returns a list containing the Euclidean norm of each contained tuple. Note that the norm of a tuple \([a_1, a_2, \ldots, a_n]\) is \(\sqrt{a_1^2 +a_2^2 + \ldots + a_n^2}\). The norm of the empty tuple is defined to be 0. 4-points

        norms([[ 3, 4], [12, 5], [2, 3, 6], [4, 4, 7],
               [36, 3, 8], [1, 1, 1], [1, 1, 1, 1]])
        [ 5, 13, 7, 9, 37, 1.7320508075688772, 2 ]
        > norms([[], [1, 1, 1, 1]])
        [ 0, 2 ]
        > 
        > norms([])
        []
    
        function norms(tuples: number[][]) {
          const norm = tuple => Math.sqrt(sum_of_squares(tuple));
          return tuples.map(norm);
        }
    
  6. Subject to the above restrictions, show code for a function max_norms(tuples: number[][]) which when given a non-empty list tuples of lists of numbers, returns a number giving the maximum norm of all the tuples in the list.

    max_norms() must run in time linear in the size of the list (note that sorting is not a linear-time operation). 4-points

        > max_norms([[ 3, 4], [12, 5], [2, 3, 6], [4, 4, 7],
                     [36, 3, 8], [1, 1, 1], [1, 1, 1, 1]])
        37
        > max_norms([[], [1, 1, 1, 1]])
        2
    
        function max_norms(tuples: number[][]) {
          const nums = norms(tuples);
          return nums.reduce((acc, e) => acc > e ? acc : e);
        }
    
  7. Subject to the above restrictions, show code for a function sum_partials(nums: number[]) which when given a list nums of numbers, returns the list containing the sums of the prefixes of nums.

    The performance must be linear in the size of nums. 4-points

        > sum_partials([1, 2, 3, 4])
        [ 1, 3, 6, 10 ]
        > sum_partials([22, -3, 4, -5, 6 ])
        [ 22, 19, 23, 18, 24 ]
        > sum_partials([22])
        [ 22 ]
        > sum_partials([])
        []
        >
    
        function sum_partials(nums: number[]) {
          return (
            (nums.length === 0)
    	? []
            : nums.slice(1).reduce((acc, e) => acc.concat(acc.slice(-1)[0] + e),
    	                       [nums[0]])
          );			     
        }
    
  8. Given a series of \(n\) numbers \(a_0, a_1, \ldots a_{n-1}\), the alternating sum of the series is a sum where the terms alternate in sign.

    \[ \sum_{i=0}^{n-1} -1^ia_i = a_0 -a_1 + a_2 - \ldots + -1^{n-1}a_{n-1} \]

    Subject to the above restrictions, show code for a function alt_sum(nums: number[]) which when given a list nums of numbers, returns the alternating sum of the numbers. The result should be 0 when nums is empty. 4-points

        > alt_sum([1, 2, 3, 4, 5])
        3
        > alt_sum([3, 3, 3, 3])
        0
        > alt_sum([3, 3, 3, 3, 3])
        3
        > alt_sum([1, -1, 1, -1])
        4
        > alt_sum([])
        0
    
        function alt_sum(nums: number[]) {
          return nums.reduce((acc, e, i) => acc + e * (-1)**i, 0);
        }
    
  9. Subject to the above restrictions, show code for a function alt_sum_partials(nums: number[]) which when given a list nums of numbers, returns the list containing the partial alternating sum of the prefixes of nums.

    The performance must be linear in the size of nums. 4-points

        > alt_sum_partials([1, 2, 3, 4, 5, 6, 7, 8])
        [
          1, -1, 2, -2,
          3, -3, 4, -4
        ]
        > alt_sum_partials([23, 3, 19, 5, 4])
        [ 23, 20, 39, 34, 38 ]
        > alt_sum_partials([23,])
        [ 23 ]
        > alt_sum_partials([])
        []
        > 
    
        function alt_sum_partials(nums: number[]) {
          const s = (acc: number[], e: number, i: number) =>
            acc.concat(acc.slice(-1)[0] + e*(-1)**(i + 1));
          return (
            (nums.length === 0)
    	? []
    	: nums.slice(1).reduce(s, [nums[0]])
          );
        }
    
  10. Subject to the above restrictions, show code for a function short_words(text: string, n: number) which when given a string text returns a list of all the words in text having length less than n. A word is defined (for this and subsequent questions) to be a maximal sequence of non-whitespace characters.

    Hint: String:split() may be useful. 4-points

        > short_words('We hold these truths to be self evident',
                      4)
        [ 'We', 'to', 'be' ]
        > short_words('We hold these truths to be self evident',
                      5)
        [ 'We', 'hold', 'to', 'be', 'self' ]
        > short_words('  ', 5)
        []
        >
    
        function short_words(text: string, n: number) {
          return text.split(/\s+/).
            filter(w => 0 < w.length && w.length < n);
        }
    
  11. Subject to the above restrictions, show code for a function word_lengths(text: string) which when given a string text returns an object mapping the words in text to their length. Words which differ in case are considered equivalent. The keys in the return value should be all lower-case and should be sorted in lexicographic ascending order.

    Hint: Object.fromEntries() may be useful. 5-points

        > word_lengths(' Hello WORLD!!!  ')
        { hello: 5, 'world!!!': 8 }
        > word_lengths(` Twas brillig and the slithy toves
        ... Did gyre and gimble in the wabe:
        ... `)
        {
          and: 3,
          brillig: 7,
          did: 3,
          gimble: 6,
          gyre: 4,
          in: 2,
          slithy: 6,
          the: 3,
          toves: 5,
          twas: 4,
          'wabe:': 5
        }
        > word_lengths('   ')
        {}
        > 
    
        function word_lengths(text: string) {
          const pairs = text.split(/\s+/).
            filter(w => w.length > 0).
    	map(w => w.toLowerCase()).
    	sort().
    	map(w => [w, w.length]);
          return Object.fromEntries(pairs);
        }
    
  12. Subject to the above restrictions, show code for a function palindrome_words(text: string) which when given a string text returns a list of words in text which are palindromes. A word is a palindrome if the word spelled backwards is the same as the word, ignoring case.

    Hint: Use Array:reverse(). 5-points

        > palindrome_words(' Hello Madam your mom and kayak
            are on the racecar')
        [ 'Madam', 'mom', 'kayak', 'racecar' ]
        > palindrome_words(' The door went tattarattat
          promptly at noon')o
        [ 'tattarattat', 'noon' ]
        > palindrome_words('  ')
        []
        > 
    
        function palindrome_words(text: string) {
          const reverse = (w: string) => w.split('').reverse().join('');
          const is_palindrome =
            (w: string) => reverse(w.toLowerCase()) === w.toLowerCase();
          return text.split(/\s+/).
            filter(w => w.length > 0 && is_palindrome(w));
        }
    
  13. Fix type errors in the following independent TypeScript code fragments: 15-points

    1. Fix the error in the code below without changing any types:

      	type Result<T> =
      	  {ok: true, val: T} |
      	  {ok: false, err: string};
      	function applyFn(result: Result<number>,
      	                 fn: (v: number) => string)
                : Result<string>
      	{
      	  return {ok: true, val: fn(result.val) }
      	}
      

      TypeScript Playground.

    2. Fix the type error in the following function by only making a type change but do not change the return type:

      	/** return string corresponding to bit (which must be 0 or 1) */
      	function bitToString(bit: number) : string {
      	  console.assert(bit === 0 || bit === 1);
      	  switch (bit) {
      	    case 0: return 'zero';
      	    case 1: return 'one';
        	  }
      	}
      

      TypeScript Playground.

    3. Fix the errors in the code below by changing the type Val to a more precise declaration:

      	type Val = {isNum: boolean, value: string|number};
      	function twiceVal(val: Val) : string|number {
      	  return val.isNum ? val.value*2 : val.value.repeat(2);
      	}
      

      TypeScript Playground.

    The answers follow:

    1. Verify that result.ok === true before attempting to access result.val.

      	type Result<T> =
      	  {ok: true, val: T} |
      	  {ok: false, err: string};
      	function applyFn(result: Result<number>,
      	                 fn: (v: number) => string)
                : Result<string>
      	{
      	  if (!result.ok) return result;
      	  return {ok: true, val: fn(result.val) }
      	}
      

      TypeScript Playground.

    2. Make the type of the parameter more precise:

      	/** return string corresponding to bit. */
      	function bitToString(bit: 0|1) : string {
      	  console.assert(bit === 0 || bit === 1);
      	  switch (bit) {
      	    case 0: return 'zero';
      	    case 1: return 'one';
        	  }
      	}
      

      TypeScript Playground.

    3. Make the type Val more precise:

      	type Val =
      	  {isNum: true, value: number} |
      	  {isNum: false, value: string};
      	function twiceVal(val: Val) : string|number {
      	  return val.isNum ? val.value*2 : val.value.repeat(2);
      	}
      

      TypeScript Playground.

  14. Given a vocabulary \(\Sigma\) containing 2 symbols 0 and 1, give regex's for the following: 10-points

    1. All non-empty strings over \(\Sigma\) which have length less than 10.

    2. All strings over \(\Sigma\) which have length less than 20 and end with a 1.

    3. All strings of length 4 over \(\Sigma\) which are palindromes.

    4. All non-empty strings over \(\Sigma\) which are palindromes.

    5. All non-empty strings over \(\Sigma\) which are even, when interpreted as a binary number.

    The answers follow:

    1. /[01]{1,9}/.

    2. /[01]{0,18}1/.

    3. /0000|1111|1001|0110/.

    4. Cannot be done because regex's cannot retains the arbitary amount of state which is needed to recognize palindromes of arbitrary length.

    5. /[01]*0/.

    Note that these are abstract regex's. If actually running within JavaScript, you would need to use the ^ and $ anchors.

  15. An advantage of the AST representation chosen for Project 1 is that copying a formula from one cell to another can simply copy over the AST unchanged, since relative cell references are maintained as relative indexes. However, this will break if a spreadsheet allows deletion or insertion of rows and columns. How would you change the representation so as to allow deletion or insertion of rows and columns.

    Hint: All problems in computer science can be solved by another level of indirection (the fundamental theorem of software engineering). 10-points

    The problem with insertion and deletion of rows and columns is that formulas which refer to other cells would need to change. For example, if cell d3 has a relative reference to cell b3 with relative indexes [-2, 0], but then a column is inserted after column c, then what was d3 will become e3 and the relative reference would need to change to [-3, 0].

    One solution to the problem is to provide an immutable ID to each row and column which does not change during the lifetime of that row or column. This ID can be anything; in the preceeding example we could associate the original column b with some indirection ID x12az and row 3 with indirection ID z3x4. Then the relative reference [-2, 0] would change to an indirect reference [x12az, z3x4]. This reference would not change even after rows/columns are inserted or deleted.

    We would need data structures mapping between these indirection ID's and regular row/column ID's. These data structures would need to be manipulated as rows and columns are inserted or deleted. Using a separate array for mapping regular row/column ID's to indirection ID's makes it possible to,

    • Map a regular ID to an indirection ID in constant time.

    • Map an indirection ID to a regular ID in time proportional to the number of rows or columns.

    • Handle row/column insertion/deletion in time proportional to the number of rows or columns.

    This assumes that arrays are implemented as a contiguous block of memory; that may not be the case in JavaScript where arrays may be implemented as hash tables.

    In order to continue having the advantage of easy copying of formulas, formulas with relative references would still be used when copying but those formulas can be synthesized on the fly as needed from the formulas which contain the indirect references.

  16. Discuss the validity of the following statements.15-points

    1. The for-of loop can be used for iterating over both arrays and objects.

    2. The for-in loop can used for iterating over both arrays and objects.

    3. If a JavaScript data value has been assigned to x, then the expression x === x must be true.

    4. The expression a?.b.c || d should be flagged as suspicious during a code review.

    5. The length of an array is one greater than the largest index of its contained elements.

    The answers follow:

    1. The for-of loop can only be used for iterating over iterables but a JavaScript object is not an iterable. Hence the statement is false.

    2. A for-in loop can be used to iterate over the properties of an Object. Since an array is an Object a for-in loop can be used to iterate over an array. Hence the statement is true.

      Though a for-in loop can be used to iterate over the properties (including the indexes) of an array, a for-of loop is usually preferred. The problems with using for-in for iterating over the indexes of an array include:

      • The indexes may not be enumerated in order.

      • The iteration may include non-index properties of the array object, including inherited properties.

    3. JavaScript data values include NaN, but NaN === NaN is always false. Hence the statement is false because of this specific situation.

    4. Given the expression a?.b.c || d, it is clear that the programmer is handling a being nullish, but not for the other properties. An expression making it clear that all nullish checks are being done would be a?.b?.c ?? d.

      It is possible that there is some kind of program invariant which states that if a is non-nullish, then a.b and a.b.c will both be non-nullish. But even in that case, an expression like a?.b.c ?? d may be more appropriate.

      So the statement is definitely suspicious enough to merit flagging during a code review and the statement is true.

    5. The length of an array is 1 greater than the largest index which was ever assigned which does not change even when the element at the largest index is deleted.

      	as = []
      	[]
      	> as[10] = 42
      	42
      	> as.size
      	undefined
      	> as.length
      	11
      	> delete as[10]
      	true
      	> as[10]
      	undefined
      	> as.length
      	11           //still 11.
      

      So the length is not 1 greater than that of the maximum index of its currently contained elements, but is 1 greater than the largest index ever contained. Hence the statement is false.