A proposal to help editors work better with dynamic languages — by not pretending they are static, and by leveraging their unit tests.
As a Test Driven Developer, using dynamic languages, editors frequently disappoint me. The main thrust of editor research, for the past few decades, targets debugging static languages. This post suggests a very simple fix.
“Static” means languages whose source code explicitly declares the type of every object, in every context. “Dynamic” languages can only reveal types at runtime. This is why editors enjoy static languages; it’s why Eclipse makes Java so easy to code. At every juncture, its code completion system knows what you are most likely to enter next. It reads the code, as you input it, to analyze and predict all its types.
C++, Java/C#, Eiffel, and Pascal use static typing.
Perl, Python, Ruby, and Smalltalk use dynamic typing. C++, for
example, requires each method to declare exactly what you can
pass into it.
foo(Bar &bar) will accept objects of type
or of a type descended from
Bar. Ruby, by contrast, uses “Duck
Typing”. If it walks and quacks like a duck, it’s a duck. The
foo(bar) can accept any object, so long as it satisfies
foo() expects. Reading all of a project’s source,
to look up every possible type
foo() might receive, would
be a wild duck chase.
Static typing helps editors navigate between modules. A well-factored, well-tested application spreads each feature over several layers (and their tests) and unites them all by type. A good editor can easily bounce between related tests, views, controllers, and models, simply by tracking the types they use.
Test Driven Development turns this situation upside down. Tests provide positive reinforcement when types work correctly - static typing only provides negative reinforcement, when they might not. In many software domains, these forces make dynamic typing more productive than static typing. Under TDD, sometimes a “mistake” with types “accidentally” works. If all the tests pass, then this “mistake” is really Emergent Design at its best — serendipitously providing more features from less code.
Your editor probably won’t be able to interpret the result.
This article lists some common problems encountered in a recent survey of editors. Their fixes don’t require any elaborate rearchitecting. Then I propose a common solution that turns the biggest weaknesses into strengths. Editors should not resist our efforts to navigate between source blocks, to automatically write & refactor well-formed statements, and to run our unit tests.
The simplest form of navigation is flicking between open documents. More elaborate navigation requires some forms of searching. Award your editor one point each if…
- the editor navigates seamlessly without mouse abuse. Anything you can do with a mouse, a menu tells you a keystroke to do the same thing. The editor never dumps the keyboard focus into an unrelated window, forcing you to mouse back to the client window
- a shifted Tab or Arrow key bounces to the most recently edited document — not the least recently edited one
- you can open any document by entering its partial filename. You are not required to enter the complete name, or the path, or any redundant characters, and you are not required to scroll through least-recently used files
- the editor opens a document from a complete or relative path, if you know it. (A common problem in all user interfaces is a user who knows exactly what they want, pitted against a system that tries too hard to “help” them get it!)
- the editor provides a command-line option to open a file into the current session
- you can determine the current file’s location from a “Properties” display, without abusing Save As
- the search system seamlessly integrates “Find in all Files” with its basic “find” operation
- the search system supports “Find in Open Files“. There’s a reason all those files are open, and editors should exploit that reason instead of thwart it
- the search system, even when searching more than one file, lets you navigate through hits FROM THE KEYBOARD, without any mouse abuse
- similarly, the Bookmark system prefers jumping between files, not just jumping within a file
- Undo & Redo store and return to the launch point of jumps.
Finally, award points if you can instantly navigate between related modules in different layers, and to any of a class’s declarations. And some editors don’t understand that some dynamic classes may have different extensions in different files. These techniques require code analysis.
The best dynamic code completer I have auditioned so far is TextMate’s infamous Escape keystroke. When you input a fragment of an identifier, the first Escape seeks the nearest identifier that starts with the same alphanumeric characters. A second Escape seeks the second-nearest identifier, and so on. (A Shift+Escape reverses these directions!)
That simple, dynamic system easily competes with a static code-completer, such as Eclipse or Visual Basic, that reads a type library. However, when certain other editors attempt to complete your code, they often think they must use a type library. Under a dynamic language, many objects have everyone else’s methods (including a few they shouldn’t have!;). Some code completers respond by reading all your application, and all your libraries, and “suggesting” every method that has ever been written. When these arrive, in ASCIIbetic order, you will typically respond by turning the code completion system off.
TextMate’s simple Escape key might not infer types (or refactor), yet it still encourages good design. Identifiers should be long and unique, and related code should group together.
I have never encountered an editor that presents a basic understanding of Test Driven Development.
To write production code, you should first write a test case, and get it to fail for the correct reason. Then you pass the test by adding new code, and you refactor to remove lines, and integrate the new ability into the existing abilities. If a test fails unexpectedly, you revert to the last state where all tests passed. This technique replaces many long hours debugging with short minutes writing simple tests.
The more edits you make between tests, the less effective this cycle gets. The cycle rewards instant turnaround. If testing is very expensive, in terms of keystrokes, you might start batching up edits and testing them all. This mixes the signals from the tests; a slippery slope to debugging.
To test, some editors require you to save your code, navigate to a test case, and perform an elaborate key-claw. This is counterproductive. Testing should require…
No matter what file you are looking at (even one in a different language!), the test button should save all changed files, then run a registered batch of tests. It should leave your editor available and focused, and should provide the option to navigate to any errors.
The editor should also checkpoint all the source’s state, each time all tests pass, to help you revert automatically if something goes wrong.
When our editors learn that testing is more important than debugging, they will streamline development.
Each time you run the tests, the editor should instrument your interpreter to extract type information.
Each test run should update a type library, containing the fully-derived type of every object found on every line of the source code, complete with the call stack that put it there.
The type library would permit these benefits:
- navigation based on the call-stack from the tests to the code
- code completion, using types sampled directly in their contexts
- automated refactoring
- incremental testing, leveraging awareness of who tests what.
Tests should call every method with every type they expect to experience at runtime. Refactoring assists test coverage when you unify your code under many test cases, each enforcing different features. So each test run generates a wealth of information that our current crop of editors simply throw away.