Week 6 Code Snippets

CSC 447 - Concepts of Programming Languages

Based on the code snippets developed in class, summaries and explanations in this document were drafted with the assistance of generative AI. The author verified all facts, revised the text for coherence, and takes full responsibility for the final content.

Week 6 Overview

Week 6 covers static and dynamic types, undefined behavior, l-values, argument passing, and closures. These snippets start with static versus dynamic typing, then work through the closure material: capturing local state, building a stateful closure, and using closures for memoization. Scope and lifetime, which the snippets also touch on, continue into week 7.

Static and Dynamic Types

The static/dynamic types lecture frames why the higher-order code from week 3 feels robust in Scala:

You can see this in function types such as (Int, Int) => Int and List[Int] => Int, which make behavior explicit and composable.

Scala 3 Book pointers: - Type inference and static typing basics: https://docs.scala-lang.org/scala3/book/taste-type-inference.html

Practical relevance: In large codebases, strong static types reduce integration errors and make refactoring safer, especially when higher-order functions are used heavily.

Scope, Lifetime, and Closures

This walkthrough uses three small Scala examples to show how closures capture variables from their defining environment. We move from simple partial application to stateful closures, then to a practical higher-order function pattern (memoization).

1) Closure from a local function (partial application style)

A function returns another function that remembers the greeting prefix s.

def greeter(s: String) : String=>String =
  def g(t: String) : String =
    s"$s, $t!"
  end g
  g
end greeter

val greet = greeter("Hello")
println(greet("Alice")) // Hello, Alice!
println(greet("Bob"))   // Hello, Bob!

The returned function g outlives its definition site, but still has access to s. Next, we use the same mechanism to preserve evolving state across calls.

2) Stateful closure: cumulative average

The function cumulativeAvg creates local variables that are captured by the returned function. Each call updates shared state and computes the running average.

def cumulativeAvg() : Int=>Int =
  var sum = 0
  var count = -1

  def g(x: Int) : Int =
    sum += x
    count += 1
    sum / count
  end g

  count = count + 1
  g
end cumulativeAvg

val cavg = cumulativeAvg()
cavg(2) // returns 2
cavg(4) // returns 3
cavg(6) // returns 4

This is a closure with mutable captured variables (sum, count), illustrating scope and lifetime directly. Now we apply closures to performance optimization.

3) Decorator pattern with memoization

We first define a function with an observable effect (println) and then wrap it with a cache.

def square(x: Int) : Int =
  println(s"$x^2")
  x*x
end square

def memoize[X,Y](fn: X=>Y) : X=>Y =
  val cache = scala.collection.mutable.Map.empty[X,Y]
  x => cache.getOrElseUpdate(x, fn(x))
end memoize

val cachedSquare = memoize(square)
cachedSquare(5)
cachedSquare(5)

On the first call, square(5) is computed and printed. On the second call, the value is returned from the captured cache, so the expensive work is skipped.

These examples connect the following themes: closures preserve access to lexical scope, can maintain state across calls, and enable reusable higher-order abstractions.