Cook Computing

Why No Top-Level Functions In C#

June 24, 2009 Written by Charles Cook

I've speculated for a long time about why C# doesn't have top-level functions, for example in this post, where the solution to the problem is rather ugly because the static functions have to be qualified by their class name, i.e. instead of this:


Rgx.Expr e = 
  Rgx.Seq(Rgx.Char('c'), 
    Rgx.Seq(Rgx.Plus(Rgx.Alt(Rgx.Char('a'), Rgx.Char('d'))), 
      Rgx.Char('r')));

It would have been nicer to write this:


Expr e = 
  Seq(Char('c'), 
    Seq(Plus(Alt(Char('a'), Char('d'))), 
      Char('r')));

So I was interested to read Eric Lippert's post Why Doesn't C# Implement "Top Level" Methods? Eric discusses the cost-benefit analysis of implementing this feature, in particular:

In this particular case, the clear user benefit was in the past not large enough to justify the complications to the language which would ensue. By restricting how different language entities nest inside each other we (1) restrict legal programs to be in a common, easily understood style, and (2) make it possible to define "identifier lookup" rules which are comprehensible, specifiable, implementable, testable and documentable.

By restricting method bodies to always be inside a struct or class, we make it easier to reason about the meaning of an unqualified identifier used in an invocation context; such a thing is always an invocable member of the current type (or a base type

).

He describes how C# was originally intended to be a component-oriented language designed for large-scale application development, but that with the increasing popularity of REPL languages like F#, top-level functions are being considered for a future version of C# (with the emphasis on being considered).

Interestingly, a comment on the post notes that Java has the static import construct which allows unqualified access to static members. This allows you to import static class members either individually:


import static java.lang.Math.PI;

Or en masse:


import static java.lang.Math.*;

You can then use the imported members without qualification:


double r = cos(PI * theta);

The motivation for static import was to provide a way of avoiding the constant interface antipattern, described here by Joshua Bloch. This technique involves defining an interface which contains only static final fields. A class using these constants implements the interface and so code within the class doesn't need to qualify the constant names with a class name. Bloch provides this example:


// Constant interface antipattern - do not use!
public interface PhysicalConstants {
  // Avogadro's number (1/mol)
  static final double AVOGADROS_NUMBER   = 6.02214199e23;
  // Boltzmann constant (J/K)
  static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
  // Mass of the electron (kg)
  static final double ELECTRON_MASS      = 9.10938188e-31;
}

Which is used like this:


class hello implements PhysicalConstants {
  public static void main(String[] args) {
    System.out.println("Avogadro's number is " + AVOGADROS_NUMBER);
  }
}

Fortunately or not, depending on your viewpoint, this antipattern cannot be used in C# because interfaces cannot contain fields.