I like strong typing and compilation errors
ayende.com
ayende.com
The entire subject is remarkably evidence-free. That's surprising; you'd think (on the surface, at least) that it could be studied objectively. Of course, we all have our individual experiences as programmers, but those aren't worth much as evidence, because we interpret our experiences to suit our pre-existing belief. Indeed, we don't even notice experiences that contradict it. (I've seen static typing proponents go on about how compilers catch bugs and then spend hours tracking down an InvalidCastException.)
In light of the above, I'm more interested in hearing what people have to say in favor of the position they disagree with. The debate is too tiresome otherwise, entirely predictable.
Although I currently favor dynamic languages, I recently started the thought experiment of asking myself, when fixing a bug, whether a good type system would have caught it. Certainly there are times that the answer is yes (though not always). The difficulty, of course, is whether the aggregate benefit would outweigh the cost.
How is that for an evidence-free assessment? :)
I've found that I rarely have type errors in dynamically typed languages, as long as I program as if they were statically typed. For example, if my JS function only takes strings or objects with certain fields, I just document those and I almost never make mistakes with them. However, if I start defining functions that return string|int|bool, then I start getting confused. Do this often enough and it becomes essentially impossible to reason about my program.
Ironically, I've found that a good portion of my bugs in statically typed languages are type errors, in the form of null pointer exceptions. Haskell's Maybe type is the way to go here; yeah, it makes looking things up in a dict inconvenient, but I can't count the number of bugs I've avoided by being forced to deal with nulls up-front and not having them pollute the rest of the program.
>Today I had to modify a piece of JavaScript code. The code used return a single string, and I needed to modify it to return an array of objects. Using C#, it would have been easy, change the return type, hit the compile button, fix the errors, rinse & repeat until it compiles.
The compiler does not save you from having to think, and like InclinedPlane says, compilation doesn't 'prove' correctness, only shows you that the compiler could at least sludge its way through your code.
The code should pass some mental unit tests of expected inputs and outputs before you even make the change.
EDIT: yes, it is just plain wrong, no excuse. But I am trying to point out that it truly happened and I am curious about why or how.
No good developer does what you describe, no matter what tools they use.
I've never seen developers who work this way: not in industry - not even in college.
In my experience, the "mess around with the code arbitrarily until it works" style described by liuliu is the programmer's equivalent of a moral panic. Everyone talks about it and laments it, but it doesn't really happen.
The advantage of doing it like Ayende says is when you already have a mental model of the transformation that you want to perform, the compiler can do the work of showing you the bits you need to edit to finalize the transformation. This process isn't mindless; because you've informed the type system about the substructure of your code, when you want to change the shape of that substructure, the compiler is then able to tell you exactly where it is now inconsistent.
Whereas with dynamically typed code, you need unit tests with 100% path coverage (line coverage isn't enough) to guarantee that you caught every interaction, if you were to approach the change in the same way. (As an aside: that's a whole lot of tests that need rewriting and refactoring, one reason I don't like unit testing for very early stages of a project when the design is quite fluid.) The alternative is eyeballing the code and perhaps getting a runtime type violation or two before all the locations are gotten right.
Note that here I'm only talking about finding the locations to modify, not about guaranteeing that the modifications are correct. If the structural change is simple, where all the in-place modifications are trivial, having a static type system that tells you about all the locations is really handy, whereas you need to do a lot more work with a dynamic type system.
IMO the benefits of a dynamic type system are more apparent when the systems you're working with are themselves dynamic and (relatively) quickly changing in nature, or when the substructure is too tedious to efficiently express in a static type system (especially apparent with mutually recursive generic types with dependencies among type parameters, like you might find in generic parsing and graph transformation libraries, etc.).
There are specific, disciplined ways in which you can use the compiler to reliably tell you what you need to fix when making certain kinds of changes. Such techniques are very valuable in large codebases. For example, you change the semantics of an interface method slightly, and this results in changing the type of the return value, or the arguments. After changing the method signature in the interface, the compiler tells you exactly where you need to make corresponding changes in the rest of your large codebase. Naturally, it can't tell you exactly what changes to make - for that you use your brain. But it tells you exactly where you need to make changes, and gives a good general idea of what changes to make.
That's the kind of technique this post is talking about. Whether you intend it or not, dismissing effective use of such powerful tools as "confusing compilation with correctness" is both misleading, and kind of insulting to users of strongly typed languages who know what they're doing.
Much simpler. Much safer. Much less chance of breaking existing code. (Decent unit tests could help verify this.) And you avoid duplication of logic.
If I wished to convert existing code to use the new function, I could do that at my leisure. Furthermore if someone happens to be developing code in another branch or at another site which expects the old function to be there, I don't break them.
Anyway, the non-typed languages I use tell me the line of code where the error occurred. Isn't that equivalent in this case?
Think of types as logical propositions, and type systems as theorem provers. The stricter and more powerful the type system, the more exact and precise the proof will be, and the more you can be guaranteed that things won't go wrong.
I understand that strict typing works by guaranteeing some things won't go wrong, but I can't see many meaningful situations where it wouldn't generate a runtime error in a scripted language, or at least results that make the source of the error painfully obvious.
Ahh, but that's not the problem. It's easy to test the code your changing, it's a much harder problem to find and test all the code that depends on that code. Having the compiler spit out every instance everywhere that you need to fix due to a new type-mismatch makes this significantly easier.
Some dynamic languages will happily convert values from one type to another even when that's not the desired result -- you might not get an error at all, just some bad data you might not find until much later.
This happens with algol style static languages too (And is one of the reasons I find people arguing for Erlang instead of python more persuasive than those instead arguing C++ instead of ruby).
You are confusing strong types with dynamic variables. Most of the algol languages have weak to medium type strength.
For instance, python has much stronger types than C. However, it's variables are all dynamic references.
We try to check everything, and we have decent unit test coverage, but there are bound to be places we don't hit instantly - and running the tests takes longer than compiling does.
That may be true for some dynamic languages with primitive tools and implementations, but is not inherent in dynamic languages. For example, SBCL will alert me to branches that are impossible to take, adding strings and numbers, and even more if I've specified types (which is optional). As an added bonus, if I'm using SLIME in emacs, the s-expressions in question will be highlighted with a note. Factor and Scheme will perform similar tricks, and there's even Typed Scheme, which lets you convert your dynamically-typed code to statically-typed code incrementally.
This is a common misperception:
http://pychecker.sourceforge.net/ http://www.logilab.org/857 (pylint) http://www.jslint.com/
And to head off the common "But but but it's not the compiler", I contend you're doing something wrong if you're not also running lint against a C program, as C compilers do not catch tons of things that people do wrong, but lint does.
Both types of languages have common type errors. Some can only be caught at runtime in both, but some sorts of errors (signature errors), static type checkers can catch them.
I work in both types of languages. In the amount of time I spend fiddling with type system stuff in objective C, I could write 2 test harness classes in python, and happily would.
Now if you're talking Erlang, et al, then we can talk about static type systems. But for the algol languages, I don't think they have all the benefits of a strong, static type system that outweigh the productivity of dynamism.
If you use docstrings, doctests, etc, you can find quite a nice enterprise coding language in there. Remember dynamic are more expressive (by definition almost)than static languages per line of code. So that 20k Line java/C# program is something like 3-13k lines of python or 2-9k lines of ruby.
The dominant reason to use static languages usually has little to do with their static nature with regards to errors: They are often faster at execution time. Also, they tend to have cheaper programmers. Both of these often dominate any code quality issues.
I personally advocate mixed language situations (like python weave) for most situations like that so inner loops can get the speed of C, and the rest gets the development speed of a dynamic language.
The problem I have with Ayende's statement is that it can seem like advice, and there are a huge number of inexperienced developers for which that advice is incredibly dangerous outside of the proper context.
Dynamic typing wins on prototypes and small projects, but for a project of more than 2000 lines, I'd be hard-pressed to see a case where dynamic typing wins.
Of course, I'll add the caveat that good dynamic typing is essential. Most people think dynamic pwns static because they're most familiar with Java and Python, but Java has shitty static typing (you have to declare types, and moreover end up needing to subvert the type system to do anything interesting) whereas ML and Haskell have awesome type systems.
Would "the game-world model for a MUD/MOO" be a cheap shot?
Large projects are certainly doable in dynamically-typed languages. I just think that static typing becomes a win once you have:
* enough code that internal interfaces become important,
* enough code that at least 30% of time is spent debugging, and:
* more than one programmer on the project by necessity.
Where would you say that the trade-off is between dynamic and static if you stick with widely used languages? (From your comments about Java I think you'd choose Python.)
Where would you say that the trade-off is between dynamic and static if both are represented by best of breed representatives? Something like Erlang or your favorite Lisp?
How does it change things if we take into account the widespread availability of useful libraries for some languages but not others? (The infamous "CPAN argument" for Perl being an extreme example.)
Lisp is a great language, but I still think static typing wins at a certain point for large projects. Type signatures (which are automatically inferred) provide a certain amount of documentation, and static typing also makes for cleaner and inherently more comprehensible interfaces.
When you have multiple programmers, when a lot of time is spent debugging, and when internal APIs become essential, static typing begins to show its strength. People can't just change interfaces willy-nilly and expect everyone else to adapt; the compiler catches this.