Is static typing and refactoring really connected?
Written by Jevgeni Kabanov on August 11, 2008 – 1:47 pmOne of the main problems brought out when comparing dynamic languages to static ones is lack of proper refactoring support. It is usually implied that dynamic languages are not conceptually refactorable, which speeds up code rotting.
Although there is plenty of evidence that dynamic languages do support refactoring, I’d like to concentrate on the other claim — that statically typed languages are refactorable. Challenging this claim may seem laughable, as there is no lack of refactoring tools for Java or C#. But let’s examine a more advanced language that is touted as Java successor — Scala.
Scala supports structural types, which allow treating classes as records of methods that can subtyped by the presence of appropriate methods. This example was given in the Scala 2.6 release notes:
In this code the type { def getName(): String } refers to any class with the method getName(): String in it. Now what happens if we try to rename the method in the structural type?
- We can rename all the instances of the getName() methods found in all classes anywhere.
- We can just rename the method in the structural type and update everything else manually
Both of these approaches are useless. The first one is basically a search and replace done on all code and may rename methods that we never intended to rename (e.g. getName() in the Person class). The second one doesn’t really do anything for us.
The truth of the matter is that structural types miss an inherent scope associated with nominative (i.e. usual) types. Since every method signature in a nominative type originates from a single type, it gives refactoring a natural scope of all the subtypes of the originating type. Without that scope many refactoring techniques are essentially useless.
What is worse is that the presence of structural types also breaks refactoring in usual classes. E.g. if we try renaming getName() in the File type, we are also presented with a decision whether or not we can rename the method in structural type. And if we do rename it, we will break the code that accesses java.io.File the same way. Therefore if we want to refactor working code to working code we can again only rename everything or nothing at all.
Luckily it looks like the main refactorings broken by the structural types is renaming the methods and changing their signature. Unluckily these are the most common refactorings and having a same named method in any of the structural types breaks refactoring also in the usual classes. At the moment this mainly affects Scala and some other functional languages, but if the structural types become more spread it may come to a language near you :)
Interestingly, Cedric Beust brought out that you can refactor structural types as opposed to the duck types. Since I obviously think differently it would be interesting to hear his (and your) comments on the matter. Perhaps I’m missing something obvious?
Tags: java, scala, types
Posted in creative | 12 Comments »
12 Comments to “Is static typing and refactoring really connected?”
Leave a Comment
Additional comments powered by BackType
August 11th, 2008 at 4:19 pm
“Now what happens if we try to rename the method in the structural type?”
Then all code containing the old method name and not the new method name no longer compiles, until you rename them.
All code containing both continues to compile.
All code containing only the new name starts to compile, and didn’t beforehand.
None of these are a problem, because you only use structural typing where the names are important, rather than the semantics. I.e., not very often, and in code where you want a rename in a structural type to change behaviour. Not all renames are refactors.
August 11th, 2008 at 4:26 pm
@Ricky
The point of refactoring is to take working code to working code. The point of this article is to point out that rename cannot always be a meaningful refactoring in statically typed language in the presence of structural types. Compiler can still be a lot of help, but you have to handle everything case-by-case. I think we are saying the same thing in the end.
August 11th, 2008 at 4:39 pm
Your point about rename is unrelated to structural types.
Rename x() to toString() in Java, and if the IDE doesn’t complain at you, you’ll probably have broken code somewhere. Not all renames are refactors *even without structural typing*.
August 11th, 2008 at 4:49 pm
I thought the point of structural typing was to add a certain amount of “dynamism” to Scala. It gives you most of the benefits (and drawbacks) of dynamic typing in the small amount of your code that would actually benefit from dynamic typing while leaving the rest of your code properly statically typed. So I’m not that surprised that structurally typed code is about as difficult to refactor as dynamically typed code as it effectively is dynamically typed code.
Seeing as it’s meant to be used sparingly (unsurprisingly, it has a large performance hit) I doubt refactoring would be that much of an issue, simply because there shouldn’t be that many instances of it in your code.
August 11th, 2008 at 5:32 pm
@Scot
You are right that refactoring structural types won’t be too often, but what if you rename a method name in a type that just “happened” to coincide with one in a structural type? This is a very annoying side effect…
August 11th, 2008 at 5:33 pm
@Ricky
I really feel that we miss each other’s point. Of course rename is not always a refactor, but what I say is that it cannot always be made into a safe refactor in a statically typed language with structural types.
August 12th, 2008 at 3:35 am
As I think Ricky was hinting at, this isn’t so much a problem with structural types as it is a matter of defining the desired behavior. The bit where things get tricky is the fact that structural types can impose an interface on an *external* type (in this case, java.io.File). In cases where code affects external types, refactoring can never be “from safe code to safe code”. This can occur in Java as well if an API is refactored without changing the external “consumer code”.
In terms of behavior, I should think that the correct thing to do would be to change all internal calls to the structural type (obviously). Additionally, all defined instantiations of the structural type should be located (in this case by finding method calls) and all definitions which can be changed, should be. In this case, this means that the File#getName() method should be refactored, while java.io.File should remain untouched. Thus, the refactoring will lead to an error on line 12. This is the expected behavior I think because a mismatch in the structural type indicates that the *usage* is wrong, obviously not the external definition.
August 12th, 2008 at 3:38 am
@Jevgeni,
You’re mostly right about refactoring structural typing.
Still, there’s one area where structural typing can support static analysis and refactoring. If you have a structural “type hasFoo = {def foo()}” and a method somewhere else “def doSomething(x:hasFoo)” and a another method somewhere else still “def doSomethingElse(x:hasFoo), then renaming the method required by the hasFoo type from foo to bar can cause a refactoring that statically finds all classes/traits that are ever used as parameters for the doSomething and doSomethingElse methods(assuming whole program availability, which is something most rename refactorings assume), plus of course rename the calls to foo inside those methods. At that point you’ve got a normal rename refactoring.
What it misses, of course, is all the classes/traits that potentially could be used as parameters types for the doSomething and doSomethingElse methods but didn’t happen to be used in the program being analyzed. There’s no reason you couldn’t have a refactoring utility find all such potential uses, but as you say now we’re into the land of slightly sophisticated search and replace – sophisticated because it’s not just regex searching, Scala’s refinement types can be pretty rich and give a lot more context than my simple hasFoo type.
August 12th, 2008 at 4:36 am
@Daniel
Actually the problem isn’t “external” code at all. That the example used it was just a coincidence. Consider what a common name “getName” is. You can have such methods all over your application. Only a fraction of them will be ever called with the example method. Choosing which ones to rename with the structural type is a non-trivial task.
August 12th, 2008 at 4:39 am
@James
I’m glad we mostly agree.
Even with type synonyms you’re still hitting the case when you rename a method in a usual class/object/trait, but it matches also a method in a structural type. Should you rename the structural type? Both decisions may cause uncompilable code, unless you rename everything in the application, which is overkill.
I’m not sure if practically it will be such a big problem, but it does somewhat hinder refactoring. Mainly I posted this to challenge the “static types make refactoring easy” crowd.
August 12th, 2008 at 5:15 pm
@Jevgeni
The type alias was just a convenient way to explain what I was talking about. Even if we drop it or make it two different type aliases the argument still stands. It just may require tracing “backwards” and “forwards” in the relationships between classes/traits and structural types.
The same goes for the case you mention. If I rename foo to bar in MyClass then it’s possible to statically find all the places that MyClass (or subtypes) is being assigned to a variable, parameter, or return with a structural type and start doing fixups. There’s no reason we should end up moving from compilable code to uncompilable code in the process (as long as all the affected source is available – but again, that’s a limitation of all rename refactorings).
Look at it this way: a compiler statically analyzes code for type incompatibilities, missing methods/functions/classes/traits, etc. There’s no reason that a rename refactoring tool can’t use the same or similar analysis and incrementally play “what if” with changes until it gets code to pass that analysis.
Still, you and I are on the same page about the central points. First, structural type refactoring can create incompatibilities in code we can’t see (e.g. if we’re writing a library). But then again, renaming a method in a normal nominative class/interface can cause problems in code we can’t see if users of our library have subclassed our code.
Second, there are some excellent refactoring tools for e.g. Smalltalk. Refactoring in such a dynamic environment is usually more of an incremental affair than in a statically analyzed environment, but it still works very well – especially if you remember the cardinal rule of always having good test coverage to back you up.
August 12th, 2008 at 7:15 pm
@James
I think you are right, type propagation is much easier than value propagation. You can reconstruct the scope from the program and refactor in it. It may not do exactly what you’d expect it to do, but it would definitely compile and work.
Can you do a runtime instanceOf/cast to a structural type? And wouldn’t that make analysis more complicated?