34 comments

[ 4.1 ms ] story [ 60.6 ms ] thread
I find it rather risky to change something and expect compilers to catch all the errors.

Anyway, the non-typed languages I use tell me the line of code where the error occurred. Isn't that equivalent in this case?

No; The line has to be executed for the error to show up in a dynamic language.

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.

Of course it has to be executed, but it's either something that you are going to execute yourself (as in pushing a button) or at least you are making an easy way to trigger this function. If you are testing such code you make sure to run it.

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.

> If you are testing such code you make sure to run it.

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.

>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.

I work with systems with hundreds of thousands of lines of code.

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.

"No; The line has to be executed for the error to show up in a dynamic language."

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.

That's a form of static typing (via type inference)
>No; The line has to be executed for the error to show up in a dynamic language.

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.

It's not about catching errors, but showing which locations to be modified. It's an aid to code editing, not a proof of correctness.
If you rest on the premise that merely because your program compiles it is also correct then you will face a career full of frustration and mediocrity.
That's not the premise he's making. He's saying it's more convenient. Also, I would think twice before referencing ayende and using the phrase "...[ayende] will face a career full of frustration and mediocrity."
I don't doubt that this is just one tiny aspect of Ayende's development practices, and in that context it's not necessarily even bad practice.

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.

I'm glad I actually read the article, because while I too like strong typing and compilation errors, but I don't want to be associated with programmers who do things like this:

>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.

It is interesting to observe that TDD (or our intelligent IDE) actually makes programmers never think. They tend to develop in more ad-hoc way: Google some example code snippet, change the order and play around with some parameters, fix the Eclipse "syntax error" hint and try it out until it works.

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.

Hogwash.

No good developer does what you describe, no matter what tools they use.

That is my point. But it trained many incompetent programmers who fancy they can do the job "just right" in the market.
Good? No. But making a living writing code, maybe even important code? Far more likely than you'd like to think.
> But making a living writing code, maybe even important code? Far more likely than you'd like to think.

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 type system is a unit test of expected inputs and outputs - it ensures that values are bounded to their domains, and aren't manipulated incorrectly.

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.).

I think that you, InclinedPlane, and BoppreH are missing the point, while cautioning against a strawman. It is elementary that successful compilation does not imply correct code; but that is not what Ayende is talking about at all.

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.

Strong static typing is great. You can cut your runtime bug count by 70-80% easily if you know what you're doing, which provides an enormous increase in productivity. Also, type discipline seems to lead naturally to better architecture. You have to build your data structures and functions from the ground up, which tends to kill a lot of the sloppy thinking and nonexistent architecture that you sometimes see in the dynamically-typed world (although this can't be blamed on the languages, because a lot of users of such languages write great code).

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.

> but for a project of more than 2000 lines, I'd be hard-pressed to see a case where dynamic typing wins

Would "the game-world model for a MUD/MOO" be a cheap shot?

Explain.

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.

In the world model popular in a http://en.wikipedia.org/wiki/MOO, almost every object has its own unique set of "verbs" (methods), which each destructively update the world in manifold unpredictable ways. There are no real "classes" of objects, per se, as having more than one of a world object that acts in exactly the same way ruins its "mystique" or "sense of fun"; instead, objects are created first through prototypical inheritance (cloning), and then by defining singleton methods (and adding mix-ins composed thereof), each of which may have originated within any other object. Objects survive mostly by memetic transmission (see an object you like, retain a clone for yourself) rather than some sort of unified architecture.
Interesting. You say that after 2000 lines, people are better off with static typing. But then you make it clear that this is only true if, instead of comparing widely used static and dynamic languages, you compare widely used static languages with best of breed statically typed languages that are not actually in wide use.

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.)

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?

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.

People tend to pick one side or the other of this issue and then argue for it. It's rare for anyone to change their mind, and even when they do, it's usually just a boolean toggle to the opposite position. Most people believe (even if they don't say it) that the opposite camp is, basically, stupider.

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.

That the subject is remarkably evidence-free reflects on the evolution of computing technology that's cascaded all the way up to the way we program at the highest level. We're so adept at noticing the differences between static typing and dynamic typing, but evaluating them much more objectively inside the entire computing environment context is like comparing two heaps of random trash and trying to decide which pile makes the best art piece.

How is that for an evidence-free assessment? :)

The problem is obtaining this evidence is really hard. So many considerations are involved during the coding of a non-trivial system. It's hard to isolate all factors involved. I remember code complete presenting some evidence that 100-200 line functions are the best size but how true is that really? What consideration does language have on that? Most people these days would see that functions that size as way too long.
I don't really have a strong preference either way. The bulk of my professional work is in JavaScript with a fair amount of Python and C++ thrown in. I dislike the C++, but that's probably because the type system is brain dead rather than because the type system is static. I do most of my hobby hacking in Haskell, which has about the strongest type system imaginable. I'm hacking on a programming language of my own design, which will have static typing, but not to catch errors. I want static type inferencing because it lets the compiler generate usable, machine-checked, cross-referenced, auto-updating documentation from my source code, without having to update this myself.

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.

I don't see the need for the invasive change. Given the problem that he had I would have written a new function that returned the more complex data structure, converted the old function into a wrapper around the new one, then called the new function in my new code.

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.

If you're refactoring a lot then your method leaves you with huge amounts of extra code, which makes maintainability much harder, because you've got multiple levels of indirection.
If even your internal modules are clearly separated into public interfaces and internal stuff, then most refactorings won't need that level of care.