Week 7: Notes

There was no lecture or tutorial this week.

Our topics this week are local functions, delegates, lambda expressions, and extension methods. You can read about them in Essential C# 7.0:

They are also covered in Programming C# 8.0:

Here are a few notes briefly summarizing these topics.

local functions

A local function is a function that is defined inside a method. (Sometimes local functions are called "nested methods", but strictly speaking they are functions, not methods, since they are not invoked on an object.) For example:

  static void xyz(string[] args) {
    WriteLine("hello");

    double arg(int n) {
        return double.Parse(args[n]);
    }

    double d = arg(0);
    double e = arg(1);    
    WriteLine(d + e);
  }

A local function may access parameters and local variables in its containing method. For example, the local function arg above accesses the args parameter.

delegates

A delegate is a value that represents a function or method. It is similar to to a function object in Python, or a function pointer in languages such as C.

The delegate keyword declares a new delegate type. For example:

  delegate bool IntCondition(int i);

With this declaration, an IntCondition is a type of delegate that takes an integer argument and returns a boolean. We can now declare a variable of type IntCondition, and use it to refer to a method of corresponding type:

  static bool isOdd(int i) {
    return (i % 2 == 1);
  }

  static void Main() {
    IntCondition c = isOdd;
    

We can invoke the delegate using function call syntax:

    WriteLine(c(4));    //  writes False

In the example above, the delegate c refers to a static method odd. A delegate may also refer to an instance method, in which case it actually references a particular object on which the method will be invoked. For example:

  class Interval {
    public int low, high;
    public Interval(int low, int high) { this.low = low; this.high = high; }
  
    public bool contains(int i) {
      return (low <= i && i <= high);
    }
  }

  static void Main() {
    IntCondition c = new Interval(1, 5).contains;
    IntCondition d = new Interval(3, 7).contains;
    WriteLine(c(2));  // writes True
    WriteLine(d(2));  // writes False
  }

Here is a method that counts how many elements in an array of integers satisfy an arbitrary condition:

  static int count(int[] a, IntCondition cond) {
    int n = 0;
    foreach (int i in a)
      if (cond(i))
        ++n;
    return n;
  }

We can invoke this method as follows:

  static bool isEven(int i) {
    return (i % 2 == 0);
  }
  
  int[] a = { 3, 4, 5, 6, 7 };
  WriteLine(count(a, isEven));  // writes 2

Delegates may be generic:

  delegate bool Condition<T>(T t);  // maps type T to bool

Here is the count method from above, rewritten to work on an array of any type T. Notice that the method itself must also be generic (indicated by the "<T>" after the method name).

  static int count<T>(T[] a, Condition<T> cond) {
    int n = 0;
    foreach (T val in a)
      if (cond(val))
        ++n;
    return n;
  }

lambda expressions

A lambda expression is an anonymous function that can appear inside another expression.

For example, here is a delegate type for a function from integers to integers:

delegate int IntFun(int i);

And here is a method that applies a given function to every element of an array of integers:

  static int[] map(int[] a, IntFun f) {
    int[] b = new int[a.Length];
    for (int i = 0 ; i < a.Length ; ++i)
      b[i] = f(a[i]);
    return b;
  }

We can define a named method and pass it to map:

  static int plus2(int i) {
    return i + 2;
  }

  static int[] add2(int[] a) {
    return map(a, plus2);
  }

Alternatively, we can invoke map using a lambda expression:

  static int[] add2(int[] a) {
    return map(a, i => i + 2);
  }

Here, i => i + 2 is a lambda expression. It is an anonymous function that takes an integer parameter i and returns the value i + 2.

Like a local function, a lambda expression may refer to parameters or local variables in its containing method. For example, suppose that we want to write a method that adds a given value k to each element in an array. We could write a local function and pass it to map:

  static int[] add_k(int[] a, int k) {
    int f(int i) {
      return i + k;
    }
    return map(a, f);
  }

Or we can use a lambda expression that adds k directly:

  static int[] add_k(int[] a, int k) {
    return map(a, i => i + k);
  }

The lambda expressions in the examples above are expression lambdas, writen using a compact syntax similar to the expression syntax for methods. Alternatively, a lambda expression can be written as a statement lambda, which can include one or more statements and can use the return statement to return a value. For example, we can rewrite the last example above like this:

  static int[] add_k(int[] a, int k) {
    return map(a, i => { return i + k; } );
  }

functions as return values

In the examples above we've seen that a method can take a delegate (i.e. a function) as an argument. A method can also return a delegate constructed using a lambda expression.

Here is a simple example:

  delegate bool IntCondition(int i);
  static IntCondition divisibleBy(int k) {
    return i => (i % k == 0);
  }

Now we can invoke this method and use the delegate that it returns:

  IntCondition div3 = divisibleBy(3);
  WriteLine(div3(6));  // writes 'True'
  WriteLine(div3(7));  // writes 'False'

In this example, note that the delegate returned by divisibleBy can refer to the parameter k even after the method divisibleBy has returned! To put it differently, the lambda expression i => (i % k == 0) has captured the parameter k. Local variables may also be captured by a lambda expression.

Using lambda expressions we can write functions that transform other functions. This is a powerful technique that you will explore further in more advanced courses (e.g. Non-Procedural Programming). Here are a couple of examples. First, here is a function that composes two functions f and g, returning the function (f g), which is defined as (f g)(x) = f(g(x)):

  static IntFun compose(IntFun f, IntFun g) {
    return i => f(g(i));
  }

We can call compose as follows:

  IntFun f = compose(i => i * 2, i => i + 1);  // now f(x) = 2 * (x + 1)
  WriteLine(f(4));   // writes 10

Second, here's a function that computes the nth power of a function f, defined as

    fn(x) = f(f(...(f(x))) [ f appears n times in the preceding expression ]

  static IntFun power(IntFun f, int n) {
    return i => {
      for (int j = 0 ; j < n ; ++j)
        i = f(i);
      return i;
    };
  }

Addition to a power is multiplication:

  IntFun f = power(i => i + 10, 4);
  WriteLine(f(2));  // writes 42

extension methods

You can add extension methods to an existing class. An extension method can be used syntactically as if belonged to a class, even though it is written outside the class.

For example, suppose that we are using a C# library that provides this type:

  class Vector {
    public double dx, dy;
    public Vector(double dx, double dy) { this.dx = dx; this.dy = dy; }
  }

We wish that the author of the class had provided a length method that calculates the length of a Vector. Since we cannot modify the class, we can write an extension method:

  static class Util {
    public static double length(this Vector v) =>
        Sqrt(v.dx * v.dx + v.dy * v.dy);
  }

The keyword this before the argument "Vector v" indicates that this is an extension method for the Vector class.

Now we can call the method as if it had been defined inside the Vector class itself!

  Vector v = new Vector(3.0, 4.0);
  WriteLine(v.length());  // writes 5.0

Note that an extension method must be static. The containing class can have any name, but must itself also be declared as static.