26 May 2016

Statically Typed Languages Should Be As Productive As Dynamically Typed Languages

Dynamically typed languages are generally more productive to work in. It takes less thinking, less time and fewer lines of code to get something done. You just say what you want to say, without jumping through hoops imposed by the language.

While some of this is the overhead of static typing, a lot of it has no fundamental reason to exist. Here are some ways in which statically typed languages can be more productive for programmers to work in, without losing type safety.

These enhancements are illustrated using Java, but are applicable to most statically typed languages.

Collection Literals


Java should let you implicitly convert an array to a List, so that you can write:

List<String> cities = ["Bangalore", "San Francisco", "London"];

rather than having to write:

List<String> cities = Arrays.asList(["Bangalore", "San Francisco", "London"]);

Similarly, you should be able to write:

Map<String, String> cities = {
  "Bangalore": "India",
  "San Francisco": "USA",
  "London": "UK"
};


rather than the less clear:

Map<String, String> cities = new TreeMap<>();
cities.put("Bangalore", "India");
cities.put("San Francisco", "USA");
cities.put("London", "UK");


Likewise for sets:

Set<String> cities = {"Bangalore", "San Francisco", "London"};

Comprehensions


List comprehensions in Python let you create a list:

[name.upper() for name in names if name.startswith("A")]

with far less code than in Java:

List<String> uppercaseNames = new ArrayList<>();
for (String name: names) {
  if (name.startsWith("A")) {
    uppercaseNames.add(name.toUpperCase());
  }
}


Java should adopt list comprehensions, with type inferencing determining the element type of the resulting list. That is, if you have

class Foo {
  Bar bar() {
    ...
  }
}


Collection<Foo> foos = ... ;

then a list comprehension of the form

[foo.bar() for foo in foos if ...]

results in a List<Bar>. The input collection can be a List, Set, array or anything else that's Iterable — anything you can use a for-each loop on.

Java should also have set comprehensions, which would be identical to list comprehensions except that they create a Set, and use braces instead of square brackets:

{name.toUpperCase() for name in names if name.startsWith("A")}

And map comprehensions:

{name: name.toUpperCase() for name in names if name.startsWith("A")}

This creates a Map from each string to its upppercase version. The compiler would infer both the key and value type, from the type of the expressions.

Destructuring


Destructuring lets you conveniently extract data from objects. In that sense, it's the opposite of list and map literals, which create objects. Java should let you do:

List<String> phoneNumbers = ...;
[String primary, String secondary] = phoneNumbers;


If the collection isn't big enough, the variables get assigned null, rather than throwing an exception. If it has one element, secondary will be null. If it's empty, both primary and secondary will be null.

This should work for arrays and SortedSets as well.

If the variables were already declared, you should be able to do:

[primaryNumber, secondaryNumber] = phoneNumbers;

You should be able to destructure objects as well. If you have a class

class Employee {
  String name;
  int age;
}


you should be able to do

Employee employee = ...;
{String name, int age} = employee;


Each local variable name should match a field of the object.

Or a getter. In the above example, if the fields were private and you had a getter, you should still be able to destructure the object. This should work for both common naming conventions for getters, age() and getAge().

Putting it all together, the above assignment should be valid if the object has one of the three:
  • A method age() 
  • A method getAge() 
  • A field age

You should also be able to destructure a Map. Logically, a Map is no different from an object — both are a bunch of key-value pairs. The above destructuring assignment should work for a map, and compile into:

String name = (String) employee.get("name");
int age = (int) employee.get("age");


This syntax lets you access only string keys, and that too strings that are valid variable names. For other keys, you'd explicitly invoke get().


Properties


Java should support properties, so that you don’t have to write boilerplate getters and setters, and have ugly method call syntax at call sites.

This should happen with zero effort from your side. If you read a field foo of an object that doesn't exist or isn't visible, the compiler should automatically convert it into a getFoo() method call for you, if such a method exists. Likewise, field assignments that are otherwise invalid should be automatically converted into calls to setFoo().

All existing getters and setters would automatically be upgraded to properties without having to explicitly declare them as such.

There would also be syntactic sugar:

class Employee {
  synthesise int age;
}


This synthesises a getter, a setter and the underlying field. If you want to customise the logic of the accessor methods, or change the name of the underlying field, don't use the synthesise keyword, and instead declare the accessors and the field separately as you do today.

If you want to synthesise only the getter and the underlying field, but not the setter, do this:

class Employee {
  
synthesise readonly int age;

}

The underlying field isn't final, so you can assign to it. It's the property that's final to clients, not the field.

Again, the synthesise syntax is just syntactic sugar to autogenerate accessor methods and a field. You don't need to use it to be able to use the dot notation, which translates into a call to a getter or setter.


String Interpolation


Another example is string interpolation. Statically typed languages should be able to do:

String name = ...;
String greeting = “Hello, #{name}”;


rather than the verbose

"Hello, " + name

or

String.format("Hello, %s", name)

Safe Navigation Operator


Some languages let you do:

String name = article?.author?.name;

which is short for:

String name = (article != null && article.author != null) ? article.author.name : null;

More statically typed languages should have this operator.

Structural Typing


Go has a brilliant idea: if a class implements all the methods specified in an interface, the class automatically implements the interface. There's no need to say "implements <interface>".

This also solves the problem that if you have a class that implements all the methods of an interface by doesn't declare that it implements the interface, you're stuck. You can't change the code of a preexisting class, like a class from the standard library, from a third-party library, or owned by another team in your company.

In Java, the solution is to define a wrapper class that implements the interface and forwards all method calls to the other class. This is clumsy, inelegant, and can cause bugs, like accidentally comparing the wrapper with the wrapped object. That's not hypothetical — I actually encountered such a bug at one point.

All these problems go away with structural typing. You don't have to waste time carefully designing a class hierarchy, and then redesigning it when you find that it doesn't quite work.

Type Inference


Statically typed languages should all have type inference to make them less rigid, verbose and bureaucratic, and the code more reusable.

Type Warnings


Most type errors should actually be warnings. This will make statically typed languages less bureaucratic.

Dynamic Typing


Many statically typed languages support dynamic typing in some form or shape, via reflection, downcasting, fat interfaces or code generation. Why not make it less cumbersome to use?


In summary, statically typed languages have many opportunities to become more expressive and productive to work in, without losing compile-time type checking.

No comments:

Post a Comment