Pages

Monday, March 10, 2008

Static vs 'dynamic typing', part 2: The personality-factor.

After talking about some of the basic nomenclature I will look now into the pros & cons of static typing. But instead of going into pure technical details I want to look at it from a less common angle. What if it's not really a technical but more of a psychological matter?

Let's start by looking at my personal experience: I've used languages without static typing often enough to know that they don't fit into my style of development. I've used Lisps, Ruby, Javascript and played in various other languages without static typing. But it never felt good. And I never was really productive. Sure, when I started a project, everything went fine and I enjoyed the freedom I had without thinking about static types. But as the project grew bigger and bigger, I started to miss static typing more and more and I also needed more and more time for rewrites and 'relearning' my own creation. With static typing I have no problem creating and maintaining programs 10000s of lines in size, but without it seems that above 1000 lines my productivity is dropping dramatically. Like I got stuck in a tar pit.

Of course this is only my personal experience. It may differ for other people but I suspect I'm also not alone with this kind of experience. So what if the question it's not simply a pure technical thing but depends much on the personality of the programmer? That there are personalities which can work well without static typing while others struggle?

Static typing creates some kind of 'safety-net'. It protects you from yourself, forcing you into thoroughly thinking about your design. Without it, it's very tempting to use short cuts instead of solid design (which seems to have always have some overhead in line-count and artificial complexity, at least in the beginning). I am a lazy person and because of this I often take those short cuts - and regret it later.

Now this is also true to a certain degree if I use a statically typed language. If I enter new territory and come across a problem I've never had before, I try to get a working version as fast as possible to try out things and learn more about the problem. It's rather uncommon that I'm able to create something solid in the first try. I could try to work things out on paper, but in the end I've always overlooked crucial things which tend to pop up only in a real implementation. In most cases my first implementation kind of works but is full of bad hacks that it will be rewritten or at least heavily reworked later until it it ready to stay.

But what has this to do with static typing? Static types create global constraints which describes your program in a non-local way. This creates a skeleton you have to fit your program into. If I have to solve a sub-problem which is new territory for me, the rest of my program creates a solid framework in the background which can lead the process while the IDE detects mismatches instantly. From my experiences with languages without static-typing this typed-framework combined with instant-feedback is the thing I'm missing most and which cost most of my productivity.

Static typing also helps a lot if you're refactoring your code. I'm not only talking about automatic refactorings, but also the good old cut&paste-refactorings. Just cut some code out and paste it somewhere else. And the IDE instantly highlights the unresolved names, type-errors etc. which shows where the code has to be changed to fit into the new place. I'm refactoring my code constantly. I rename, change signatures, cut code into pieces to assemble it new elsewhere, extract methods etc. I always 'sculpt' my code, forming it in place. As I write new code I always refactor old code. Because of static typing this nearly always works without introducing new errors and can often even be done semi-automatically by the IDE. If I write code in languages without static-typing I refactor much less. Now every refactoring is a risk, may corrupt my code without noticing it. So it takes more time because I have to be more careful. And even if a mistake shows up by a failing test later I still have to find the source of the error instead of simply checking-off one error-highlight after the other.

The funny thing is that people tend to think that static types hinder exploratory programming. But with my style of work I can explore things quite easily with the abilities to restructure my code again and again, instead of first thinking about how to write for example a macro abstracting things out. So static typing makes exploratory programming even easier for me (but only thanks to the IDE - without it doing all those things by hand would be quite cumbersome) and in the moment I have to work without, I feel bounded and forced to think much more ahead instead of simply exploring things.

Many people praise the 'REPL' ('read-eval-print-loop', a way to execute code immediately in many non-static-typed languages) because of it's way to instantly try out things. But a IDE with instant error-highlighting is even faster in pointing out mistakes - and works with all of your code, even if you don't suspect a mistake.

REPLs are nice and fine, but they need to be used explicitly be the programmer. This requires to enter and write additional code and it also works only if the code kind of works. Instant checks based on static types work automatically and even if the code is still incomplete. So instant checks based on static types can be much more immediate and faster, while 'dynamic checks' like tests (done in the REPL or with testing frameworks) only work later if you're already finished a piece of code.

This leads to the disadvantages of TDD ('test-driven-development'): Tests must be written explicitly and thoroughly or they aren't of much use. This takes time. And if you change your design you also have to rewrite all your tests. And that's is my problem with real TDD: It's again my laziness. Writing and maintaining those lots of tests is essential for this method to succeed - and I'm simply to lazy to do this.

I always let my tests stay behind instead of leading my designs, testing only the most important things instead of doing the kind of fine-grained testing which is necessary to make this methodology work. I always think "hey, this code is simple, why test it, I shouldn't have made any mistakes" - and later I find a typo or something similar stupid. But with a static type-system which let the IDE quickly point out where I made mistakes this happens far less. And it scales well with the size of the program. In small ones the overhead of searching for typos or simple stupidities isn't that big. But as bigger the code-base grows the longer it takes to find those little mistakes. So without static typing I've lost my most important tool to write new working code and it's not wonder why I'm not very successful without it.

The same is true for documentation. Static types in combination with expressive names are often already enough documentation. Sure, for more complex parts of the code, writing additional documentation is mandatory. But again the problem is to hold it up to date with code changes. With static typing I use the types as part of documentation and write only documentation which explains the non-trivial parts of a method or class (like algorithms, how-to-use etc). So static types are an excellent tool for compiler checkable documentation which never gets out of sync with your code. Without static typing the documentation is optional and lazy people as I am tend to omit it to often for their own good.

I think here is also the reason why there are people who can work well without static typing: If you're very disciplined (or work under strict project management which enforces the rules without exceptions), TDD can probably be a feasible alternative to static typing. And if you don't refactor that much as I do (because I am often to lazy to create the right names and interfaces in the first try and have to change it later if the code stays), the advantages of safe refactorings are quite useless for you.

But for lazy people as I am, discipline has to be enforced by the language. And this is what static typing does. It's not perfect because it can't find all kinds of errors. But it forces me to create clean designs and solid interfaces. It points out flaws in my designs and forces me to think about them. It gives me immediate feedback and it is also useful as documentation. It makes refactorings easy and let me clean up my code more often. And the things it can check are not only checked, they are even proved, so I can rely on them (compared with TDD which can check a wider range of things but with much less guarantees because I never know if the tests were correct. I can remember spending hours on 'broken code' discovering later that the code was correct but one of my tests was faulty).

So the prime reason why people can't stop to argue about the topic is probably that people are different and have different ways to work. If you're a disciplined person who writes as much code for tests as for the 'real' program, you will benefit from the freedom a language without static typing gives you, without getting stuck at a certain complexity as I do. If you tend to write working code in the first try, you won't refactor that much and rely more on building different abstractions using the more dynamic nature of a language without static typing. But if you see comprehensive tests as a distraction from the 'real work', while at the same time you're not having problems with thinking more about overall design, creating interfaces and specifications, then static-typing is the way to go because it fits much better to your way to work. Even if it requires additional code and stands sometimes in the way of creating a better abstraction.

And I think that's also the reason why most 'enterprise-programming' is done in static typed languages: With many different people creating and maintaining a project often over decades, it it much to risky to depend on the personality of the individual programmer. And the situation is also asymmetric: People who can work well without static typing can also do productive work in a static typed language. But a programmer who works better with static-typing can totally mess-up a program written in a non-static-typed language. So the risks are much smaller if the language used has static typing. And I'm also quite sure, that the number of programmers who can work well without static-typing is much smaller than vice versa because people in general and programmers in particular are more on the lazy than on the disciplined side.

So if you argue next times over the question 'what is better', please keep in mind that people have different ways to work and to think and that there may be no 'one right way'. To make people most productive, it's important to acknowledge their way to work. Static typing is an additional tool which is very helpful for some people but may also be to restrictive for others. Try out both and see what fits you best. As programmers we all rely on tools. Nobody would be productive today by entering machine-code with switches as in the early days of computing. So there is no disgrace in relying on a compiler or an IDE to do your job. Find the tools which help you most without feeling forced to adopt a certain style which is 'hip' in the moment. It's important to learn new things and try out alternatives constantly - but in the end, all what really matters is to get the job done.

17 comments:

Sammy Larbi said...

Great post. I too wondered if some people are just more dynamically minded than others a little while back, but I didn't go near as in depth as you have, merely mentioning the idea.

In any case, I think you are right on the ball. If it were purely or perhaps even mostly technical, I don't think we'd be seeing good programmers feeling more comfortable in one style than another - on both sides of the equation.

I may be more at home with dynamic languages, but I can agree with your statically typed brain on this issue. =)

Chonger said...

This is one of the better posts about static vs dynamic typing.

I enjoyed reading your personal traits and how it affects you make your decisions when it comes to choosing between static and dynamic languages.

CruxOp said...

You do know that refactoring can be done in dynamically typed languages?

Smalltalk invented automatic refactoring.

Karsten Wagner said...

Of course refactoring is possible in languages without static typing. But Smalltalks automatic refactoring uses runtime type sampling to learn which methods should be refactored. This isn't as sound as automatic refactorings in statically typed languages because runtime types are only as good as the coverage at sampling time. And without a comprehensive test-suite (which visits all parts of the program, giving type infos in turn) it is not really practical (otherwise you have to execute and 'test-drive' your program everytime you want to do automatic refactorings).

I also haven't only talked about automatic refactorings but also about manual ones ('cut&paste'). Those kinds of refactorings are quite common and often require to change names (if the pasted name already exists in the code), create new locals or method parameters etc. Having static types and mandantory declarations makes this much more easy and safe to do because in general the IDE shows exactly where things have to be corrected. Of course it still can't guarantee to find every possible mistake, but compared to languages without static typing its a huge factor better.

Also those tools simply don't exit for most current languages. I have to work in Javascript now and I have no such tool at hand. Even if by IDE (IDEA) has relatively good support for Javascript, its a joke compared to the abilities I have in Java. The same is true for most other 'dynamic' languages.

PS: Please read my previous post ('part 1') why the term 'dynamic typing' isn't really appropriate here (hint: many static typed languages are dynamically and statically typed at the same time).

Federico Builes said...

Karsten: Many of the advantages you mention in static typed languages are of the kind "the IDE/Editor/Compiler tells me if I'm wrong before runtime". You're forgetting about the real issue when debugging code: the logic of the applications.

Static typing will surely help you with syntactic/semantic errors when writing the code, but at least on my personal case, those are not the kind of bugs that really matter. The real problems (again, for me), are the "How will I do this?" or "Why is this doing X instead of Y" and no type system will help you with that. These "real" problems are the ones covered by the tests, the ones that matter the most.

An example to this is your opinion of the REPL, which is wrong according to what I understood. The real nice thing of the command line interpreters are the fact that you can go in at once and test some part of the code, making sure it actually does what you want. The IDE (which according to you is faster correcting the errors) will barely tell you if the program will compile. As we all know, this doesn't ensure that it'll do what we want it to do (in most of the languages), or that it will even run (C, for example).

Karsten Wagner said...

Federico: You're right, I'm talking about tools which help me to do my job. This is something very concrete and 'unabstract'. It's something I use at every line of code I write, modify and read. And without it I am not as productive as I am with it. For me the difference is big enought to dwarf the disadvantages of static typing.

Yes, static typing is no 'silver buller'. It doesn't writes my code for me, it doesn't makes my code bug-free, it doensn't do the thinking for me. It's just a tool which makes my life easier.

I still need to test and debug my programs. But I need less time for it compared to when I use a language without static typing. Because of this I am more productive.

Static types are themselfs a kind of test-framework: They allow to express properties which the compiler can check statically. And for all those things the compiler can prove, I don't have to write tests for anymore.

From MY experience most mistakes I make are not the 'big logical ones', but simply typos or that I overlooked something small and easy. Those mistakes are in most cases easy enough to be catched by the compiler/IDE instantly. Having to care about those manually (by tests or by more thorough thinking) is simply to boring for me. So I gladly pay the little price of writing declarations to be free of doing this kind of micromanagement myself.

Other people will see this differently, but I (and probably lots of other people) value the benefits of static typing high enough to sacrifice a little bit of expressive freedom otherwise. It a question of priorites and those are often a very personal choice.

Another example: If I programmed in Lisp for I've always created to many ad-hoc data-structures based on lists. It's so quick so easy and so tempting. But after some time my program starts to drown in cars and cdrs and I got more and more problems to understand and to change things. So I had to refactor my code to use accessor-functions or objects instead. But to do this I first have to find all the places in the code which should be replaced. This would be a piece of cake in Java, but in Lisp it takes it time until I found every usage. And then I can't even be sure, if I haven't overlooked one. I probably have - and after I change my data-structure a bit, modifying the accessors in turn, suddenly some tests fail because there was still some code which accessed the data directly. And because I was to lasy to write real unit-tests, I have to do lots of searching to find the real source of the error.

If I use a language like Lisp, this happens again and again and again. I'm simply to lazy for this. I want a compiler who helps me as much as possible and who even protects me from my own lazyness by not providing ad-hoc-data-structures in the first hand. And I'm willing to pay a certain price for this.

It's like garbage-collection: It costs performance and still can't solve all memory-allocation-problems for you. But who cares, if it makes the job that much easier?

Robert Fischer said...

Karsten -- I agree that most errors I make aren't logical errors, but typos and API errors. "Oh, that's right, this uses #size, not #length." That kind of stuff. And the ability to check that stuff immediately is invaluable to me, because I seem Hell-bent on typing "legnth" and "fasle" at every possible opportunity. This is just part of the reason I'm a big static typing fan.

One of the things I will note is that more and more static languages are generating REPL. And static languages are moving away from a lot of the noise of explicit typing while still retaining the safety net. In both cases, see Ocaml (toplevel and implied static typing). This lets you have your cake (succinctness) and eat it, too.

The other thing is that the kinds of bugs covered by static typing are growing each and every day. Monads (and their weak knock-off, inversion-of-control) provide type safety around making sure that you open files, have an open file, and close it when you're done. Ocaml checks for unvalued variables for you (OMG! Coding without fear of nil/null!). Static analysis from tools like Checkstyle and Findbugs can catch double-checked locking and other logical bugs that are inaccessible to dynamic languages.

Quite frankly, I don't see the argument for dynamic typing. I do see the argument with not being pleased with Java's type system. But turning away from static typing seems like throwing the baby out with the bathwater.

Stephen said...

I have a [possibly dumb] question;

In any language with a class system, does that effectively give me a static typing mechanism? (that I have to specify myself via class definitions)

If I use abstract data typed for a representation task; string, array, associative array, etc., am I implementing dynamic typing in my program?

I'm sort of getting the feeling that this distinction holds only for the most disciplined of programmers. I don't think I've ever read any code that is that disciplined. (I'd love to see examples in any language)

Federico Builes said...

The advantages of not using a static type system are not limited to "I don't
have to define a type for everything, that way, I'm more effiecient", that
might be true but you'll also get:

* Code reuse: When an object "responds to the method X"instead of "is a Y" you can avoid a lot of code duplication (in some languages as Ruby you can process a String, a File and an Array with the exact same code -- XML processing is an example of this).

* Code is smaller. In comparation to Java or C++, you avoid typing a lot of
code just to say an object's class (Map m <Foo, Map <Bar, Bla>> >> =
new...). In some languages as Haskell you have type inference which helps
with this, but the system's not pefect (using parser combinator's a great
example of this -- you end up defining most of the types since the
inference system is not capable of doing it for yourself.)

* Compilation doesn't mean error-free. This is false even for Haskell where
the myth "If it compiles, it works" is common. I agree that it's "safety
net" will help you, but it'll also make you feel safe when you're really
not. A great example of this is not writing tests: they should not be
avoided
and I don't care if you're lazy or you have a compiler checking
types. I'm sorry if this sounds harsh to some of you.

* Workflow: Not having to run make, ant or w/e every time I do a small change
is a *huge* time saver. You have to do it just to realize by how much your
productivity will increase.

* Obviously, it has it's problems too, but in the end, it's all about the
tradeoffs and what you wanna get. I'll keep all of these things even if I
have to write more tests (which is not always bad) or actually check that
things are working, thank you :)

>> It's like garbage-collection: It costs performance and still can't solve
all memory-allocation-problems for you. But who cares, if it makes the job
that much easier?

Karsten: This is an awful analogy, what you get by not using a garbage
collector is too specific to compare it to something so wide as a type
system. And again, the advantages are _huge_ compared to what you get by
using static typing. In other words, in the "modern" languages GC is almost a
given, I don't remember seeing any language besides the C/C++/D(?) family
needing you to allocate/free your memory. Static type systems are not a given
in today's world (or at least, that's how I see it).

Robert Fischer said...

@federico

The same succinctness and reuse you get in implied static typed languages (Haskell, OCaml). They just also give you the capability of not shooting yourself in the foot by passing in a list when you should be passing in an element.

As for compilation not being perfect: this basically boils down to saying that some safety might give people a false sense of security, and therefore any level of safety is to be avoided. I heard this argument back when C++ people were griping against automatic garbage collection ("It's still creating memory leaks, and providing the impression that references/pointers don't matter!"). It wasn't convincing then, and it's not convincing now.

Also, the amount of correctness being guarantied by static typing is increasing by leaps and bounds in recent years, particularly when you're in the world of functional languages. So the number of problems that can "slip through the cracks" are dwindling.

And the workflow issue solved by the use of a good IDE, unit tests, continuous integration server, and/or REPL, depending on the particular nature of the problem.

You're totally right that writing tests is not a bad thing. But the point is how much effort you have to put into writing your tests. Do you want to have to test for what happens when you pass in nil, or pass in something that doesn't implement your interface? If you make a change to your library, do you want to have to hope that your users unit tests catch all the places they called the change method? I'm with Karsten -- I'm simply too lazy for that.

Mike Stone said...

You pose a very interesting question that maybe dynamic languages are more attractive to certain personalities, but I would have to disagree on a few points.

Federico I think is dead on that tests should not be avoided. I wrote a little dominoes game a while ago in Java (before I discovered Ruby), and things were going fine until I was getting close to done. At that point, some logical errors just started killing the game, and I eventually bored of the project because of it. A big part of the reason? No automated tests. I am rewriting the game in Ruby now from scratch, focusing on TDD. I don't know how well TDD will actually save me from that kind of outcome again, but I have some confidence that it will go a bit better.

I think the biggest benefit of TDD is finding logical bugs faster. The common syntactic bugs you speak of will be common in any language, and easy to fix in any language. As soon as you try to run your Ruby program with a syntactic bug, it will become very obvious very quickly, and pinpoint the line of problem, just like an IDE. Granted, those syntactic bugs get a faster turnaround in a rich IDE that static languages provide, but the fact that those bugs are quick to fix in most languages makes them less of an issue. The first time you hit the code, you will be pointed to what is wrong. The longer a bug is in the system, the more costly it becomes, and the logical ones will always be the ones that slip through the cracks easier and stay in longer. Static languages won't help you there, and neither will dynamic languages. TDD has to be there in either case to help with those bugs.

I fully agree that TDD has to be maintained strictly to be useful, I just don't see it as something that should be skipped out of laziness (though I have suffered from the laziness too, but I don't think it's fair to pretend that it was a good idea, or something that is ok to leave out).

One advantage dynamic languages like Ruby has is that TDD becomes significantly easier, particularly in the way of mocking objects. In Ruby, let's say I want to test a method which uses some kind of connection object to read data, and the connection object has the methods "connect" "read_line" and "disconnect". The following Ruby code will mock this object:

mock = Object.new
def mock.connect
# mock a connection
end
def mock.read_line
# mock reading a line
end
def mock.disconnect
# mock disconnect
end

Now compare this with how you would mock an object in something like Java, and I think you will notice it is a lot simpler in Ruby (at least that has been my experience). Furthermore, with this technique you can mock only the methods you need, which is not possible in Java (if you are mocking an interface anyways). Singleton methods + duck typing in Ruby (the features that make the above work) I think are such a vast improvement for unit testing.

Now, this of course won't matter if you are abandoning TDD, but again, I think the danger of abandoning TDD is much higher than any safety you may get from a static language. This is coming from a very lazy guy too (I don't always use TDD, and I regret it if what I'm doing grows to a certain point). Also you comment about fixing bugs in tests... this can easily be a problem, but one way to reduce it is to write the tests before the code, as TDD requests. Then, you write the code to match the test, and if it doesn't work, it is more obvious to look at both the test and the code, rather than just the code. This won't stop bugs in the test, but I think it is very important to see the TRANSITION between a failing test because of no implementation, and a passing test because you just wrote the code. If you are testing code that already exists, it is a lot easier to let a bug in a test get by, which is why it is so important to write your tests (incrementally) before you even begin the code.

Another use of REPL that I frequently use, that I think you missed, is not to test my existing code, but to gain an understanding of some library, standard library class, or some language construct. This doesn't require working code so much... I can for example test how the different uses of array indexing works (in Ruby you can use 1 index, 2 indices, negative numbers, and more) without writing a simple app, and testing it this way. This makes some exploration a lot easier, and prototyping a lot quicker.

As for clean designs... I don't think static typing really helps much there, unless I am missing your point (some examples might help). For example, a procedural programmer can design procedurally in an OO language, despite being statically typed.

Overall, an interesting read, and I think your conclusion might be accurate, though I just don't think some of your arguments are QUITE strong enough to support it. You do make some very strong points though, especially with the instant feedback from an IDE. Thanks for writing it all the same :-)

Karsten Wagner said...

First: Thank you all for your comments, I really appreachiate any kind of feedback.

Now @ Mike and Frederico:

I totally agree, that tests are necessary. But there are differences how to use tests. I use tests mainly to detect regressions. After a program reached a certain size, regressions are IMO the main problem which slows progress. But detecting regressions is often possible with a relatively small test-suite which works on the high level of the system testing everything at this level and below. I run (and check-in) my code often and at each run the test-suite detects if something is wrong. Because I know what I've worked on lately (changes are always visible in the IDE based on the version-control) I know where the error must be. So I find it generally quick and and my code stays solid. But all this is done quite sparsely, I only write tests for code which is already relatively stable (because otherwise I would have to change the test quite often which I'm to lasy for). So the total size of my tests is quite small. And for creating new code I totally rely on the type-system to find mistakes.

Now after doing a bigger refactoring I have lots of changes all over my code and my way of testing would be quite useless (because of the huge number of possible sources of errors). But commonly there simply are no errors, because the type-system prevents them (refactoring generally don't introduces logic errors but only simple name-conflicts or type mismatches which show up instantly). This is a totally different way compared to unit-testing in a language without static typing. It's very dynamic because I refactor very often and tests are mainly to detect that there is an error, not giving to many informations about the reason of the error.

So my workflow is: Writing code, testing it (mostly) manually, refactoring, writing more, testing, changing, etc. And only after some code is solid I write tests to validate the code to detect regressions later.

It may true, that testing can be more easy in more dynamic languages. You can simply monkey-patch some tests or mock-code right into your program which is impossible in more static languages. But this is also risky because it can create instablities in your tests. And with the amount of test-code which is generally necessary to test fine-grained enough, this can be a risk too. Of course it's not impossible, but again: Testing isn't simple, too. You have to invest brainpower and you need dicipline. And the amount of necessary test-code is often as big as the type-annotations I have to write (and the IDE helps a lot here, too. If you look at a Java-program you may thing 'wow, what a lot of useless code. Booooring'. But in fact most of this code is generated by the IDE. It's often like type-inference, only that you see what the infered types are).

About the REPL: It can be quite nice, but my problem is often that I write some code in it, play with it, write something else and discover later that I want to reuse some code ealier. But it's gone because I haven't saved the REPL-history. Or I'm to lasy to search the last 200 entries in the history. If I want to test something, I hit two keys in the IDE, create a new class with a new 'main-method' and code away. Not exactly as fast as a REPL, but also not much slower too. And the code remains existant unless I remove it explicitly.

Static typing can enforce more solid designs. For example in Javascript I often add some property if I need it. In the moment I do it, I want to test something quick, so I simply do it. And afterwards I often forget to document it or encapsulate it into a more solid structure. In Lisp I often use ad-hoc structures based on lists. Quite easy in the beginning, but a pain after your program grows. Those things aren't possible with static typing (ok, partially with tuples - but those contain at least type-infos and have to be declared somewhere) and so I create a 'real' data-structure right on which is later easily refactored into something more stable (for example by adding getters/setters, renaming it or putting it into the right class hierachy).

It's a very different way to work, that's for sure. And it's important to notice that 'my' way to work is quite dynamic, too. I do much explorative programming and I also change my code quite often. People who think that 'static typing' means 'static programming' simply don't know what's possible with a modern IDEs. But most things are possible with more dynamic languages too. It simply works totally different there: Instead of doing lots of refactorings, you would create a more dynamic data-structure from the beginning to make refactorings less necessary. And instead of relying on static types and restrictive declarations you would test write and execute much more tests early and often. And the latter gives more freedom of choice but also more freedom to mess things up.

Robert Fischer said...

@Everyone

Please note that TDD and REPL are not dynamic language specific. So they cannot be used as an argument against statically typed languages.

TDD was invented in Java, and (I argue) is actually easier in a statically-typed language, because there are fewer possible cases to test -- the compiler can prevent a lot of bugs which you otherwise have to test and manually handle (list.legnth being the classic example).

As for REPL, there is nothing fancy about them which can't be done by a statically typed language:
http://caml.inria.fr/pub/docs/manual-ocaml/manual023.html

Gabriel C. said...

@Mike
I fail to see the difference between the Ruby code and this java code:
Object mock = new Object() {
void connect() {}
void readLine() {}
void disconnect() {}
};

The difference is in a "dynamic language" like Ruby you can actually receive an "unknown" message (with missing_method IIRC ), and that is very powerful.
One thing

Personally I like static typing, and having explored Scala (sort of hybrid between ML and Java, but very powerful) I saw how a powerful type inferencing system could work (you can even have type-safe "duck typing" if you want)
One thing that helps in static languages is you have more information for static code checking and the compiler can perform code optimization giving better performance.
I'm pretty much bored with the "Static vs dynamic" when actually boils down to Java vs. the-dynamic-language-I-like.

Mike Stone said...

@gabriel c

The key difference is in duck-typing. Because of Java's type system, you HAVE to be a descendant of the given type, even if the type behaves the same. Thus, if you want to use a mock of an object for a test, in Java you must construct a descendant of that type, either as an anonymous class like you proposed or an actual named class. Sometimes an anonymous class will effectively do what the equivalent Ruby version would be, but also oftentimes you have to jump through some hoops.

For example, if you are mocking a Java interface, you have to define each method, even if you do nothing in them (because you know your test won't hit those methods). Other times you may have to have some real data for the base constructors, and without the data the class may not work, so then you may even have to mock those arguments (you could argue this as bad design, but sometimes you won't have direct control over the design of those objects, and I have definitely run into such cases in C#). Even further examples of trouble are if the class or methods you want to mock are final. What do you do then?

As a concrete example, let's say that our connection object we want to mock requires a network interface object that cannot be null to instantiate it. So in Java, you would have to do something like this:

Interface interfaceMock = new Interface() {
// mock as needed.
};
Connection mock = new Connection(interfaceMock) {
public void connect() {}
public String readLine() {}
public void disconnect() {}
};

All that extra stuff just to mock a few methods of the Connection, and if Connection defines some further abstract methods, we would have to add those as well. Because of duck-typing, the above in Ruby is entirely unnecessary, because we can merely pass in an object with a few singleton methods defined for the methods we KNOW will be used in our test. In other words: the same example I gave in my previous comment will still work.

You may be willing to accept that additional overhead, but I recently read a very good essay from Paul Graham entitled Succinctness is Power. His argument basically boils down to: the more succinct the code you can create with a given language, the more powerful that language is for you.

Type inference can definitely increase the power (succinctness) of a static language, as I have seen in Haskell (I have not yet explored Scala), but I would still speculate that in general a dynamic language is more succinct because it imposes less restrictions and boilerplate requirements on your code.

I will agree with you that there is a lot of extra stuff you get with static languages that are difficult, maybe impossible in dynamic languages (such as IDE refactoring and intellisense), but speed and compilation optimization is not something I think we should focus on. Most people nowadays consider Java reasonably fast, but there were serious concerns over its speed in the early days. An improved JIT compiler and other enhancements, plus Moore's Law have made them irrelevant. Moore's Law (or multiple cores if Moore's Law is hitting a ceiling) will continue to make speed less of an issue. There may be improvements in how dynamic languages are run that make the difference less of an issue, and anyways you can already drop down to a faster language in most dynamic languages, so you can implement performance critical sections in a language more appropriate for speed (C for Ruby, Java for JRuby, I know these because Ruby is the dynamic language I know, but I wouldn't doubt that every major dynamic language can interface with some other largely standard and fast language). I would like to try Lisp precisely because it is a dynamic language that supposedly can be as fast or faster than C (I don't know how accurate that claim is, but it seems reasonable that Lisp has benefited in performance improvements from being such an old language).

At the end of the day, it really comes down to which language fits your personality and preferences best, which is really what I got out of this article, though Paul Graham makes a persuasive case for succinctness.

xiaoliang said...

Great article and great discussion

Brian said...

@ Mike Stone

The test criticism of static typing isn't really all that compelling to me because of the existence of mock frameworks like EasyMock.

You do something like this:
Interface interfaceMock = EasyMock.createMock(Interface.class);
interfaceMock.expect(...);