Questions & explanations
1. Give an example of computing a large power using binary exponentiation.
To compute 3^10 using binary exponentiation: 10 in binary is 1010. Start result = 1, base = 3. Bit 1: base = 3^2 = 9, bit is 0, so skip multiply. Bit 0: base = 9^2 = 81, bit is 1, result = 1*81 = 81. Bit 1: base = 81^2 = 6561, bit is 0, skip. Bit 0: base = 6561^2 = 43046721, bit is 1, result = 81*43046721 = 3486784401. So 3^10 = 59049? Wait, that's wrong because we skipped a step. Actually, 3^10 should be 59049. Let's recalc: 3^10 = (3^5)^2 = 243^2 = 59049. The binary method should give same. But the example above had error. Correct: start result=1, base=3. Bits from most significant: 1010 -> bits: 1,0,1,0. For each bit: square base: first bit 1: result*=base -> result=3; base=9. second bit 0: base=81; third bit 1: result*=base -> result=3*81=243; base=6561; fourth bit 0: base=43046721. result=243? That's 3^5, not 3^10. Actually, we need to iterate from most significant to least. Standard binary exponent uses exponent's binary bits from LSB to MSB, but either way works. Let's fix: using left-to-right: exponent=10 (1010). result=1. for i from top bit: bit=1: result=1*3=3; base=3^2=9.
2. Give an example of a recursive function that has multiple fixed points, and explain which is the least fixed point.
Consider the recursive definition f(n) = f(n). This equation has many fixed points: any constant function works. The least fixed point is the function that is undefined for all n (bottom). For a more interesting example, consider f(x) = if x=0 then 1 else f(x-1). This actually has a unique fixed point: the constant 1 function. But if we allow partial functions, bottom is also a fixed point? Actually, bottom is not a fixed point because the operator applied to bottom gives a function defined at 0 (1) but bottom is undefined everywhere, so bottom is not fixed. Typically, with monotone operators on pointed domains, there is a unique least fixed point. An example with multiple fixed points is f(x) = f(x) over flat integers: any constant integer c is a fixed point; the least is bottom (undefined).
3. Give an example where lock ordering fails to prevent deadlock.
Suppose two threads need three mutexes: A, B, C. Thread 1 locks A, then B, then C. Thread 2 locks B, then C, then A. This follows a partial order (A before B before C for thread 1, B before C before A for thread 2). But thread 2 locks B, then waits for C, while thread 1 holds C and waits for B? Actually careful: if thread 1 holds A and wants B, thread 2 holds B and wants A? No. Let's say they both need all three. If order is not total, deadlock can still occur. For example, thread 1 locks A, B, then needs C; thread 2 locks B, C, then needs A. If thread 1 locks A, then B; thread 2 locks B, then C; then thread 1 waits for C, thread 2 waits for A – deadlock. So a total order (e.g., always A,B,C) is needed. Lock ordering fails if you don't enforce a single total order.
4. Compare defunctionalization with closure conversion. How do they relate to each other?
Both defunctionalization and closure conversion eliminate higher-order functions by making the environment explicit. Defunctionalization replaces function values with a data type that encodes which function is being used, along with its environment. Closure conversion also creates an explicit environment, but it keeps the function as a pointer combined with the environment data. Defunctionalization goes further by turning the function into a first-order code that dispatches on a tag. Defunctionalization is often applied after closure conversion to get a fully first-order representation. Closure conversion can be seen as a step towards defunctionalization, but defunctionalization completely removes functions as values.
5. Compare defunctionalization with another transformation technique like lambda lifting. Which one eliminates closures more completely?
Defunctionalization eliminates closures more completely by replacing function values with data types and a dispatch function. Lambda lifting only lifts nested functions to top level but still keeps function values as closures, unless combined with other techniques. Defunctionalization produces first-order code with no function pointers or closures at all, which is beneficial for targeting machines without support for higher-order functions. Lambda lifting still requires some mechanism to pass the environment (extra parameters), but it does not remove the concept of a function value. Defunctionalization is more powerful but can lead to combinatorial explosion of dispatch functions if many closures are used.
6. What closure properties do primitive recursive functions have?
Primitive recursive functions are closed under many operations. They are closed under composition: if f and g are primitive recursive, then h(x)=f(g(x)) is primitive recursive. They are closed under primitive recursion itself by definition. They are also closed under bounded sum and bounded product: for example, sum_{i<y} f(x,i) is primitive recursive if f is. They are closed under definition by cases (if-then-else with primitive recursive predicates). They include the constant functions, identity, and all arithmetic operations. However, they are not closed under unbounded minimization (an operation that finds the smallest number satisfying a property), which is needed to get all computable functions.
7. Why is the Ackermann function not primitive recursive?
The Ackermann function is not primitive recursive because it grows faster than any primitive recursive function can. Primitive recursive functions have a limited growth rate; they can only handle recursion that is bounded in complexity. The Ackermann function uses nested recursion that cannot be expressed as a primitive recursive schema. It also diagonalizes over all primitive recursive functions. In formal proofs, one shows that for any primitive recursive function f, there exists an n such that A(n,n) > f(n). This property, called domination, proves that A is not primitive recursive. Therefore, it demonstrates that primitive recursion does not capture all computable functions.
8. What are primitive recursive functions?
Primitive recursive functions are a class of functions from natural numbers to natural numbers that are defined using a limited set of rules. The rules include the constant zero function, the successor function (adds one), and projection functions (pick an argument). New functions can be built by composition and primitive recursion. Primitive recursion means defining a function by recursion on one argument with a base case and a recursive step that uses the previous value. All common arithmetic functions like addition, multiplication, exponentiation are primitive recursive. However, not all computable functions are primitive recursive; the Ackermann function is a counterexample.
9. Explain how the monad laws ensure correctness in a monadic parser combinator library. What goes wrong if the laws are violated?
The monad laws guarantee that parsers composed with bind and return behave consistently. Left identity ensures that a parser that always succeeds with a value works as expected when combined with other parsers. Right identity ensures that a parser that does nothing to its result is harmless. Associativity ensures that the order of sequencing does not change the parsing behavior. If a law is violated, refactoring parsers by changing parentheses could change the meaning, leading to subtle bugs. For example, if bind is not associative, merging or splitting parser definitions could alter the input consumption or error handling. Thus the laws are crucial for compositional reasoning.
10. Explain why static single assignment (SSA) form is beneficial for compiler optimizations like dead code elimination and constant propagation.
SSA ensures that each variable has a unique definition, which simplifies data-flow analysis. For dead code elimination, if a variable's value is never used, its defining instruction can be removed easily. For constant propagation, if a variable is defined with a constant, all uses can be replaced with that constant directly. Because redefinitions create new names, there is no ambiguity about which definition reaches which use. This makes optimizations more precise and efficient. SSA also enables transformations like global value numbering to detect redundant computations. The use of phi functions at merge points handles control flow without losing precision.
11. Compare the fixed-point semantics approach with operational semantics for defining recursion.
Fixed-point semantics gives a mathematical meaning to recursive definitions using domain theory and monotone operators. Operational semantics describes how a program executes step by step, often using reduction rules. Fixed-point semantics is denotational, meaning it maps programs to mathematical objects, while operational semantics defines the actual computation. Both are equivalent for terminating programs, but fixed-point semantics handles non-termination through bottom elements. Operational semantics can be more intuitive for understanding execution, while fixed-point semantics provides a compositional model useful for reasoning about program properties.
12. Compare the aggregate, accounting, and potential methods. When might you choose one over the others?
All three methods give the same amortized cost, but they provide different ways to understand it. The aggregate method is simplest: just count total cost and divide. It works well when the sequence is simple. The accounting method is like prepaying for future costs with credits; it's intuitive for assigning costs to operations. The potential method is more powerful for complex data structures; you choose a potential function that captures the 'state' of the structure. You choose based on ease of calculation: aggregate for simple sequences, accounting if you can assign credits naturally, potential when the structure changes in a way that a function can track.