A few months ago I tried to implement some refactorings for the Subtext “programming language”. I think I now understand some reasons why that did not work out.
A refactoring changes the shape of your program, while maintaining the program’s behaviour. You do it so the code gets into a shape that makes the changes you are aiming for easier to apply, and makes it easier to reason about their correctness. The correctness of the initial refactoring is guaranteed by your IDE.
Note that even a refactoring is a change; it is only guaranteed to be “correct” as long as you live in a closed world, where all the code you might interact with is contained in your IDE project. As soon as you leave that safe haven, using type casts, reflection, dynamic linking, or when you guarantee backwards compatibility for published interfaces, that’s over.
So, what about Subtext?
Firstly, Subtext does not have an external “behaviour” that might be observed. It just has a system state, and it’s up to the user to observe any interesting bits of that system state at any time, and even in retrospect. The system state encompasses both code, data, past states and an operation history. It’s all the same, actually, just nested nodes.
This makes it somewhat difficult to separate a program’s behaviour from the program itself. The solution I tried for transferring the refactoring idea is to guarantee some property of some usage of a node before and after a refactoring: “When invoked as a function with the same arguments as before the refactoring, the result subtree will look the same” – seems quite straightforward. I made a whole catalog of these.
However, here comes “Secondly”: Secondy, Subtext is completely untyped – at a sufficient level of abstraction, function invocation is the same as copying with changes, and is also the same as assignment, method overriding, and object creation, and it is exactly at this level that Subtext operates. Also, all the innards of a function are accessible (and overridable), not just its result. So essentially the only way to know what a “function” does to its arguments is to run the function. Especially, due to untyped higher-order data flow, an automated analysis can’t really tell where a node modified by the refactoring may eventually be copied to. But without this information, the refactoring cannot guarantee much about the system state. (I tried some heuristics, which turned out either too coarse or felt incomprehensible to a user).
Static types and other distinctions made by the programming language form the contract between you and your tools. As long as you stick to the types, your tools can and will help you. If you don’t have a type system, or the type system is too clever, you do not notice when you cross the line, and your tools become unreliable.
If a project is small enough and short-lived enough, and the language concise enough, you may not need tools as desperately. Some people also find tools like Eclipse or IntelliJ Idea too complex or heavy-weight and prefer to stick to their ASCII editor, and therefore focus on the “conciseness” aspect, or restrict the scope of their project, or rely on tests. That’s OK as long as you realize that by liberating yourself from types, you are also limiting yourself in various ways.
2 thoughts on “Subtext Refactoring”
I agree that dynamic typing and reflection make refactoring more difficult. IDE assistance features like code completion also depend upon type info, and likewise become harder in its absence. Static typing is all about discarding power and flexibilty to gain stability and certainty. But in the real world, types change, and programs change. Static typing merely says that if you agree to never speak of such things in your programs, you gain some benefits. But we MUST speak of these things, and so, for example, Java adds reflective mechanisms on top of the language that defeat the guarantees of the type system. In the presence of reflection, many refactorings become unsound or undecideable.
Dynamic languages like Lisp or Ruby are popular in large part because they are not straightjacketed by types, and can freely do metaprogramming tricks without laborious reflection and code generation as in Java. But this dynamism means that it is harder to say what code can and can’t do just by looking at it.
We need to escape this barren dichotomy. Erik Meijer proposes “Static typing where possible, dynamic typing when needed”. http://research.microsoft.com/~emeijer/Papers/RDL04Meijer.pdf. My own plans for Subtext are to add a form of “static” typing in predicates that attach to variables, performing runtime checks, but also being sufficiently declarative to allow a form of type inference.
Comments are closed.