Fengyun Liu

Make Statically Typed Languages More Friendly

In this post I propose that statically typed languages should have two modes:

  • prototyping mode (default): report most code problems as warnings
  • production mode: report most code problems as errors

The language design should ensure semantic equivalence of programs under the two different modes.

For the production mode, the following code problems by default should only be reported as warnings instead of errors:

Problem

I am writing a simple Java program:

import java.lang.reflect.*;

public class Main {
  public static void foo(String message) {
    System.out.println(message);
  }

  public static void main(String[] args) {
    Method method = Main.class.getMethod("foo", String.class);
    method.invoke(null, "hello");
  }
}

However, the Java compiler insists that the exceptions NoSuchMethodException and IllegalAccessException must be handled or declared to throw.

import java.lang.reflect.*;

public class Main {
  public static void foo(String message) {
    System.out.println(message);
  }

  public static void main(String[] args) {
    try {
      Method method = Main.class.getMethod("foo", String.class);
      Object o = method.invoke(null, "hello");
    } catch (Exception ex) {
      System.out.println("Method not found: " + ex.getMessage());
    }
  }
}

I just want to play with the language, why it obstinately rejects to compile and run the program?

This is not or not only an issue with checked exceptions, but a common problem in statically typed languages.

Statically Typed Languages

Programming language researchers have long touted the benefits of type systems. While dynamic languages of the last century, such as JavaScript, Ruby, Python, are still active, we do witness a transition to statically typed languages. If we look at the active programming languages created in the last decade:

  • Go (2009)
  • Rust (2010)
  • Kotlin (2011)
  • TypeScript (2012)
  • Julia (2012)
  • Elm (2012)
  • Swift (2014)
  • Zig (2015)

All of them are typed! (Julia enjoys an unobtrusive type system.)

It is widely known that statically typed languages enhance reliability and performance of programs and make code refactoring, navigation and maintenance easier. However, if type systems are so great, why we have not seen the adoption of more advanced types in daily programming, such as dependent types, linear types, and session types?

Drawbacks of Type Systems

That brings us to two drawbacks of type systems:

  • Verbosity: expressive type systems generally require user type annotations.
  • Rigidity: type errors prevent compilation and running of programs.

While type inference can cut down the verbosity of type systems, real-world programming languages equipped with expressive type systems usually need manual type annotations. More expressive the type system is, more annotations are required and more difficult it is to get the types correct.

The second problem is that typing problems are reported as errors and they prevent the program from being compiled and run, which is quite annoying during prototyping, as we have seen in the case of Java checked exceptions!

Dynamic languages address the two problems by throwing types away for the sake of usability. Can we do better without throwing the baby out with the bathwater?

Prototyping and Production Mode

When a programmer is drafting a program, usually he wants to focus on the problem at hand and does not want to be distracted by typing errors. For prototyping, having the program running is more important than getting the types correct. Not to mention many of such errors are usually false positives, due to the over-approximation nature of type systems and analysis.

Typing errors only become helpful during debugging and refining of the program. No one can deny that a well-typed program is better.

To address the prototyping problem, I propose that compilers should have two modes:

  • prototyping mode (default): report most code problems as warnings
  • production mode: report most code problems as errors

In the prototyping mode, some typing errors might still help the programmers in drafting the program, e.g., naming errors, and the mismatch of method arguments. These code problems can be reported as errors. It may also issue errors to protect the compiler from crash, e.g., cyclic class inheritance. However, the following code problems should only be warnings:

  • Variance checks for class type parameters
  • Typing errors related to F-Bounds, recursive types and higher-kinded types

I can foresee it will be a technical challenge to treat some typing errors specially, due to the fact that the type system is an intricate unity. For languages that support typed-based code synthesis, e.g., Scala implicits, this is more difficult, as the meaning of the program depends on the types. Having different rules for different modes implies that a program might behave differently under a different mode, which will be a huge concern.

While maintaining semantic equivalence of programs between two modes will be an academic and engineering challenge, there are some low-hanging fruits. For example, the following code problems will not and should not incur semantic difference:

  • Checked exceptions
  • Null pointers
  • Initialization errors
  • Exhaustivity check

In the prototyping mode, the checks for code problems above can be skipped. In the production mode, they should by default be issued as warnings instead of errors.

Why Not An Interpreter mode

Some readers might be wondering, why not have an interpreter mode for prototyping, so that all naming and typing errors can be delayed until run-time? The reason is that I believe some basic error checking will still be helpful even for prototyping. While having quick runtime feedback is cool, having runtime crash can be frustrating.

From another perspective, an interpreter is just an implementation strategy of the language specification, in constrast to ahead-of-time (AOT) compilation. Therefore, the challenge of being faithful to the language specification or maintaining semantic equivalence of programs under different modes remains.

Nevertheless, as an implementation strategy, an interpreter does have an edge for implementing the prototyping mode: For example, it may ignore errors in a class which is not used at runtime. In another word, in the prototyping mode, it has the freedom to execute illegal programs rejected by the production mode. Semantic equivalence only matters for legal programs under the production mode.

Conclusion

A type system can be a friendly adviser, instead of a stern rule enforcer!