I recently learned enough Standard ML to be dangerous, and I gained enough of an appreciation of ML-style static / type inferred languages to see where the author is coming from. I even agree to some level.
But, in practice, some dynamic languages just don't seem to suffer from hidden type bugs. One of those is Clojure. Instead of packing everything into complicated types, you are mostly working with maps and sequences of maps--just raw data--with higher order functions. There isn't a lot of room for type bugs. Additionally, without any input from the developer, Clojure does some type-inference all on its own. You can enable warn-on-reflection such that the Clojure compiler will tell you anywhere in the code where it's going to have to use reflection to figure out how to invoke a Java method or property on an object. You can fix them with type hints such that you get performance on par with statically typed Java.
Clojure protects you from name typos you might see in other dynamic languages and accidentally clobbering existing variables through the fact that most variables are really not variable at all. You bind a value once in a lexical scope and it can't be mutated. That's more of a benefit of functional languages and lexically scoped languages, than statically typed ones.
It may boil down to a matter of taste, but I don't see the need for fancy refactoring tools like "method extraction" in a language like Clojure. The methods already don't live in classes, so they hardly need to be extracted from them :). If you rename a method in Clojure, the compiler _will_ complain in all the places where you are trying to call a method that no longer exists.
I spent much of a week debugging an issue in a Clojure multiplayer game where the same action was being handled differently on different machines. Somehow, the "true" branch of an if-statement was being taken even though the condition was "false."
The reason? When the action was sent over the network, the boolean was converted to a Boolean.
On the same project, I would also frequently suffer from name-typoes -- in structure keys (deftype and defrecord didn't exist yet).
A dynamically-typed language is still dynamically typed. A lot of basic analyses are still undecidable. Lexical scoping doesn't fix that.
Yes, it's definitely by degrees. Some dynamic languages provide a great deal more help than others for common issues listed in the original article. And using the print-str and read-string (with read-eval off) is a fantastic way to serialize data between Clojure processes.
I like the point. And not being able to use if(0) (as in C) has caught tons of bugs (like writing "if(x=0)" instead of "if(x==0)"). There are ton of things that sounded convenient in the early history of programming languages that are really anti-patterns.
For example:
Confusing ints with booleans.
Confusing ints with pointers (no longer done, thank goodness).
allowing assignments to have a non-void type ("x=1.0" should be of type void, not type double).
Poor choice of assignment and test equals (would be nice not to use "=" but to force ":=" and "==").
Pverly aggressive type coercion and autoboxing.
Integer division using the same symbol as floating point (many end-users want 5/2 to be 2.5 as the answer, mostly you want it to be 3 if you are doing loop-foo).
Making "else if" and if-braces mere convention.
Allowing null way too many places.
Automatic variable decleration.
I find that when working with maps and sequences of maps with higher-order functions, types are especially critical to ensure that e.g. you didn't get your arguments backwards in your fold, or forget that your map values are sequences, not single elements. So much can be accomplished with a chain of higher-order invocations, and so much can go wrong (especially if you have Perl-style type coercions), but types are great at catching the errors.
At the same time, I think that programing with complex generic structures is a functional anti-pattern anyway: if your type is "Map (Foo, Bar) (Map Int (Set String))", and it's not somehow encapsulated (as in a class or ADT), good luck. In Python you would hide this in a class, but what is the Clojure solution (genuine question)?
The Clojure solution is a map of maps. There are facilities for creating types (notably, defrecord), but it's just a way to optimize your code -- the interface it offers is exactly the same as a map, to the point that if you call assoc on it (as in, add a key-value pair) you get a normal map back.
This really doesn't appear to be as much of a problem as it was for me in Python and Haskell. Perhaps it's because the accessor syntax "feels right" so that manipulating even complex structures of maps and vectors seems natural, and the code won't look too convoluted.
(In Clojure, maps are functions of their keys, vectors are functions of their indexes. So:
(def a-map {:a 1 :b 2})
(a-map :b)
=> 2
also, when using keywords as keys, the reverse also works:
(:b a-map)
=> 2
This basically makes normal operations on maps look much like using accessors on Haskell ADTs.)
That doesn't sound like encapsulation though. If I have some complex map of maps with invariants I need to maintain, is there any way to ensure that client code doesn't mess with it?
The Clojure ethos is quite anti-encapsulation. Generally, you maintain complex invariants by defining the functions that manage your data and not updating it except through them. (This is easier than it sounds, because when all data structures are immutable, drive-by-mutation just doesn't happen.)
Yep. Same here - I had always thought I agreed with all the arguments mentioned in the post, and then I found Clojure. I'd like to add that I don't know if there are any existing refactoring tools for Clojure, but there certainly could be. All the things that you might want to rename safely - say functions and keywords (used for hashmap access, and serve as sort of Clojure class fields) are proper symbols, well defined in a namespace. You could have a tool that renames them safely. In Scala, OTOH, a statically typed language, you can't even rename methods safely because they're not always referenced through their defining types, as they could be called through an injected so called "structural type". So nowadays, some statically typed langiuage lose, through cleverness, many of the advantages of static typing, while dynamically typed languages, through disciplined restraint, manage to gain them.
EDIT: actually, the one thing I miss most from statically typed languages is the immediate error highliting in the IDE. The REPL alleviates the need for that, but it was still cool immediately seeing files and lines highlighted in red as soon as I type in some error. I wish Clojure could do that.
No, what Scala is giving you is the choice: use structural types, and sacrifice some of the benefits of nominal types, or don't. Note that if you perform your renaming and now class C no longer matches structural type S, any part of the code that depended on that typing will no longer compile: safety is preserved. Structural types therefore only make renaming slightly less automatic. (And they're rarely used anyway.)
I'm also skeptical of your claim about Clojure symbols: can you not select a field using a runtime-computed values, thereby making perfect static refactoring intractable?
Like I said, I do miss instant IDE error highlighting.
You could select fields using computed values in any language using reflection, but no, 99 times out of a hundred you use explicit symbols ("keywords") in Clojure.
Looking into keywords a bit, it seems that auto-rename is only going to work for namespace-qualified keywords, and even then, it's going to be hard if the ::keyword syntax is used, because the current namespace is determined dynamically.
> I don't know if there are any existing refactoring tools for Clojure, but there certainly could be.
Maybe, but they would be imprecise and they would require developer supervision. When the tool doesn't have any types, it can't reason about the code accurately.
Here is an article that explains why in more depth:
Indeed. They refute the claim in the post that search-and-replace was "the best [Smalltalk] could do". The author ought to have corrected his post, but instead just ignored this.
The claim that "Debugging is one of the most expensive steps in software development- so you want incur less of it" is something we can all agree with, but "even if it is at the expense of more typing" doesn't follow.
Having programmed in both kinds of languages, I have to admit I love not thinking about the type. I don't buy the claim that static typing saves much in the long run (in terms of debugging or writing better code). In 15ish years of using both static and dynamic typed languages, I think I've been burned two or three times at most by not having static types—it's just not a big problem.
Using static types creates a self-fulfilling environment in which getting the right type is crucial (and thank goodness we have a modern IDE to help with that).
I think the issue is that there are good statically typed languages and there are bad statically typed languages but only the latter are popular.
I've done a good bit of programming in Java and that was painful, with relatively little benefit. In fact, for a pretty long time, Java made my an adherent of dynamic typing.
Then I tried Haskell and realized that a good static typing system is very useful. It not only catches mistakes--I've worked on very language interpreters in both Python and Haskell, and I constantly made preventable mistakes in Python--but actually makes your code more expressive. Take the read function, for example. (It is basically the opposite of toString.) I can't think of any way of writing it in a dynamically typed language because it has to know what type it's supposed to be to work--it's polymorphic on its return type. And this is very useful: if you have a string, you can just read it in and it will be of whatever type you need (as long as you've defined how its read).
Haskell has some other advantages over Java (e.g. much more lightweight types and no messing about with classes and subclasses), but I think the important point is that static typing done right is not only safer but also sometimes more expressive than dynamic typing.
It is my experience with large teams that just about every issue starts to appear frequently as the overall quality on large teams is harder to maintain than on small teams where coordination is easier and you can keep the skill level of the average developer higher.
I've fallen in love with hybrid systems like Common Lisp. SBCL is dynamically typed, but type inferred and I have the option of adding type declarations to help the compiler out. It's a different way of thinking, you're working on a living system, instead of treating your program as a monolithic block that the compiler has to prove satisfies some static constrain, you work on chunks(functions, classes, etc.) that you compile and test immediately while your program is running, and if you screw up, the system is there to help you figure out what went wrong and fix it, without killing your process. Wetter or not this type of incremental development is better or not for some definition of better, I feel it fits the way I think better. Maybe somebody with a different way of thinking will actually perform worse in such an environment, thats my theory at least. I know I'm faster(and much less annoyed at my compiler) this way.
- Statically checked and removed at compile-time if possible
- Checked at runtime otherwise
- Not checked at all if you compile with high speed and low safety, yielding close-to-C level of performance (complete with memory faults and weird bugs when things go wrong)
Furthermore, I can turn on this high speed/low safety mode in specific portions of my code (inner loops), while compiling the rest with high safety.
SBCL + SLIME + Quicklisp is a seriously awesome combination.
Very good points. I agree 100%. However, I think you can and should mix the two. I write mission critical code in C++ with all compiler warnings enabled and lots of unit testing. I do less critical code in dynamic languages (such as Python) where I can 'import critical_cpp_module' and use it from within the dynamic environment. This approach allows for the best of both worlds. I get the benefit of quick development time, high-level Python libraries and the ability to be sloppy without being punished while at the same time I have strict, very strongly typed and tested C++ code for the things that just have to be right no matter what. When you combine the two approaches, you're more flexible. I suggest Boost Python for those who wish to expose C++ functions, classes, etc. to Python.
Edit: Software such as pylint are nice too if you're going to go dynamic only. That helps catch lots of bugs that you'd not otherwise notice.
I learned to program in dynamic languages (that's not quite true, my first couple of weeks programming were in Pascal), primarily javascript but also some ruby and PHP. I continued using mostly javascript for about 2 years, and I was strongly opposed to static typing (largely because of my exposure to Pascal, but also because I did a whole lot of crazy stuff in javascript that was completely dependent upon weak dynamic typing).
Then I learned Haskell, which I was more open to because I had heard I didn't need to write down any type information. With Haskell I learned that static typing was great because it created a documentation for your code that was self enforcing, and as Haskellers love saying, if the code typechecked, it probably works as expected.
Now I prefer working in a statically typed environment. But for none of the reasons presented in this article, which I think is just presenting information that those used to dynamic typing will turn their head at.
There are actually some really good points here. I was bracing myself for the typical anti-dynamic-typing article that merely confuses dynamic typing and weak typing, but no, these are good points.
I will say, however, that weak typing can be used to overcome the first complaint. For example, in Python, if you were to do apply a function that resulted in integer overflow, you would get an automatic conversion to long integers. (The actual example doesn't really apply in this case, since to make that function work at all in Python, you'd have to to make `n` a float, which would fix the problem anyway).
Edit: And yes, I know that Python is a strongly typed language, but implicit conversion to long integers is still an example of weak typing.
> implicit conversion to long integers is still an example of weak typing
No, it's an example of a properly defined integer type. Letting "integers" silently overflow is weak typing. If you explicitly want wraparound arithmetic (by proving bounds for performance or wanting implicit modulo), then explicitly specify int16/int32/int64.
> Letting "integers" silently overflow is weak typing.
"Weak typing" is a broad concept, but that's not one of the ideas that it covers. In any case, Python's conversion of integers to long integers is not transparent. It is an actual change to a different type, which is an example of weak typing.
Python never converts an int to a long. Instead, it may return a long from the result of an operation on an int if that result wouldn't find in an int.
The term "weak typing" does not really mean anything and confuses a lot of discussions about programming languages. Here you are clearly talking about implicit conversions, but weak typing is also used to refer to completely unrelated things like casts in C (which can be unsound). Python having both "strong" and "weak" typing illustrates this: these terms are ill-defined.
Edit: mindslight's comment adds another possible interpretation of "weak typing" (integers which overflow), further illustrating my point.
Komodo relies on getting type info from JSDoc notation and Sublime Text barely does autocomplete at all. For instance type this into Sublime and you won't get a list of String methods when you type the last dot:
var x = "Hi!";
x.
I'm not sure how Komodo does with that one without any JSDoc notation. I'm know that there are editors that do a slightly better job (for instance, Visual Studio probably has the best Javascript autocomplete I've ever seen) but still, it really pales in comparison to how well it works with statically typed languages.
In visual studio your example would work. You can even do it on user defined types. It's pretty accurate and actually finds every place where it seems reasonable where it could have the information.
But it doesn't go much further than that. For example parameters inside functions don't have intellisense and return values from functions rarely do, but sometimes.
You're right about parameters inside of functions, but wrong about return values. It does work for return values as long as visual studio can deduce the type by running the code.
For parameters, you have to help visual studio out by documenting what the parameter does using XML code comments (e.g. /// <param name="arg1" type="String">Arg1 description</param>).
Don't know what to tell you: the difference between the reliability and accuracy of statically typed autocomplete and dynamically typed autocomplete is night and day, by their nature.
A best case scenario for dynamic typing is either global inference analysis, which is slow and painful, or relying on test/runtime information, which is unstable and incomplete.
Yes it can. The language isn't non-deterministic, rope has the ability to do both basic static analysis (which catches the common cases 95% of the time), or it can do dynamic analysis to determine what went where.
Please don't comment on what Rope can or cannot do unless you actually understand how it, or Python for that matter, works.
Static typing has it's place, but it's been massively oversold. I'm interested in static languages that have excellent type inference or like dart with it's optional static types,however 90% of the time in strictly static languages like C# and Java it gets in the way more than it ever helps.
Complaints like "refactoring" or compiler error checking however are the oldest FUD in the book.
> Until real software engineering is developed, the next best practice is to develop with a dynamic system that has extreme late binding in all aspects.
For the sake of context, the original source of that quote is essentially an advertisement for Squeak. It raises some interesting ideas, and Smalltalk is definitely a system to know.
But as an argument for one paradigm vs. another it doesn't really stand up, because the essay takes its own conclusion (that large systems would be easier to maintain if they were more like Squeak) as a major premise. Him being one of the inventors of the platform, I don't think we can just take his word for it. At a minimum, what we'd really want to see is a large successful enterprise system built on Smalltalk to serve as an instructive example. To my knowledge no such system exists, so we can't really take the paper, insightful as it is, as much more than hopeful musing.
That said, the "bind really late" approach has seen a lot of success. Just not quite so pervasively as the "in all aspects" that Kay advocates in the paper. Nowadays, the standard way to build large systems is to build completely independent modules and couple them on fluid interfaces. Text streams in Unix, REST APIs, and even SOAP are clear examples. What we don't see, though, is a whole lot of reason to think that the languages and run-times on which these modules run must also support late binding and hot-swapping of code at the micro scale, or that we're really suffering for lack of it.
> a large successful enterprise system built on Smalltalk... To my knowledge no such system exists
"a fully-integrated, model-driven, automated silicon wafer fabrication facility (fab) ... ControlWorks managed fab saved TI the equivalent of 1.2 fab lines last year. What does a fab cost? roughly $1B!"
...what we'd really want to see is a large successful enterprise system built on Smalltalk to serve as an instructive example. To my knowledge no such system exists...
Does such a system exist to anybody's knowledge? I've heard that there's a good bit of Smalltalk on Wall Street, but companies don't like to advertise it because it's a competitive advantage.
You've started with a fallacy here. No one is getting paid to push smalltalk. Even if that was the case, my association with a thing, doesn't make my support of it wrong by definition. Smalltalk obviously should be expected to embodies Alan's opinion on this. That it does, should come as no surprise, and one can safely assume he would have the same opinion regardless of having written a language based on it or not.
To be honest it doesn't seem that you are arguing against this concept that strongly, you're just not overly sold on it. I agree it's good to be sceptical, just as I'm sceptical there is all that much value in static typing in most cases. Having used a statically typed language for years, I've personally found the value to be little and the cognitive friction to be high.
> You've started with a fallacy here. No one is getting paid to push smalltalk.
I wasn't using the word 'advertise' to suggest he was getting paid to push it. I was using it in very much the same sense in which you use the word 'sell' here:
> To be honest it doesn't seem that you are arguing against this concept that strongly, you're just not overly sold on it.
To which I respond: Correct. I'd even go so far as to say I'm not arguing against the concept at all.
Considering that all sorts of approaches to software development continue to be extremely popular, including among very smart people, it just seems crazy to me that people get so acrimonious about such issues. Programming is a very wide and varied field. If two developers try something and come away with differing opinions of it, isn't it just possible that those different opinions are both informed, but informed by different experience resulting from working in different problem domains? I'd submit that developers don't give each other nearly enough credit when they offhandedly dismiss each other's sharing of their own practical experience as "FUD".
To bring it back to Kay's paper: He's got some very interesting ideas, but the specific examples he talks about are problems that just don't cause me any stress in my day job. Yet he implies that people in my problem domain are suffering for not using his preferred programming paradigm. Now I don't want to accuse him of attacking me, and I certainly don't want to attack him because I believe that paper represents learned speculation and not a stake in the ground. But it remains true that he's not necessarily coming from the same place that everyone else is, and his experience of what works well does not necessarily translate into a universal best practice.
> Complaints like "refactoring" or compiler error checking however are the oldest FUD in the book.
What makes you say that?
First, I happen to have worked with this fellow before [Hi, John! Long time no see. Fun to come across your post here.] and I'm sure he's quite sincere.
Second, having spent a bunch of time lately in dynamic languages (Ruby and JS mostly), I do indeed miss the automated refactoring tools that existed for Java. It's really nice to be able to rename a method everywhere across the code base without worrying that something else got mangled. I also miss the magic documentation that a type-aware IDE can provide; I spend a lot more time digging through layers of code to figure out what a particular parameter is expected to be.
Not that I want to switch back; I agree that a language with strong type inference is the way to go. But man, I miss the tools.
Not saying he isn't sincere I'm saying it's a common misunderstanding that the improved refactoring and compiler errors are that valuable, that they alone can support a strong argument for static vs. dynamic.
There are generally very few situations where you have a single method from a single class sprinkled across an entire codebase for a good reason. That always indicates high coupling and design problems. There are even fewer reasons to rename a well designed method. Constant renaming and high-coupling are bad habits that seem to be commonplace when working in strictly static languages and yet they are known to be bad habits even there. At least dynamic languages discourage these anti-patterns. Not to belittle your friend, I found myself falling into these traps constantly when working in C#.
Honestly, what I'm saying is that these problems just are not nearly as significant in say, Ruby, as they are often made out to be by static typing proponents. It's a bit disingenuous to point to these trivialities as reasons why static > dynamic. If it were true then there would be little benefit in dynamic languages. Clearly there is.
After 10 years of programming almost exclusively in dynamic languages I'm very much enjoying the refactoring abilities of modern IDEs when I'm doing native mobile development. I rarely have to change a method across an entire code base, but the ability to rename a method or package name in half a dozen files with a single key press is indisputably not only a productivity boost but an encouragement to more extensive refactoring.
More generally, the ability of modern IDEs to manipulate code structurally, and not just texturally, has been eye opening for me.
I agree that high coupling is bad. I strongly disagree about renaming, though.
The theory that you can get good design up front depends on having both a stable problem and a stable solution. Most places don't have stable solutions, because creation of software is usually an exploration of the solution space. (If you don't need to explore the solution space, that often means you should just buy something off the shelf.) Quite a number don't have stable problems, either. Some people have innovative competitors; others are doing startup-ish things.
As Keynes said, "When the facts change, I change my mind. What do you do, sir?" I think that applies to design as well, method names included.
I am reserving most of my replies for the comment section of my own blog, but I couldn't resist say hi to you. I am in fact sick of typing in type labels (so strong type inference is great, it is just finally becoming available to us masses).
Also I would like try defend refactoring. Those that don't think it is important have not seen it used well (there are a lot of trivial uses of refactoring). I remember pair programming with Brian Slesinsky (when the two of you were coaching). We wanted to change some deep functionality of the project. Brian slowed the moment down for a bit and then said something like: "we call this refactor method, then introduce this error here, fix it here and then call this last refactor to clean it up." He was definitely not only doing what the IDE supplied but thinking in a very deep way how to trick the IDE into correctly performing a big change it was not designed for.
Not "unwilling to discuss it here," just trying not to post everything twice. Now as regard to that objection- I have also read that the empirical studies of defect rate seem to show that defect rate does not correlate with anything once you control for size (leading to the argument that you should decrease size at any cost). I haven't examined the studies or the nature of the code in them. My personal observation is that not all code bases are the same (even those with the same number of defects). Some code bases seem to have a brittle behavior where there is an unlimited reserved of subtle bugs that depend on particular circumstances to happen (so likely undercounted, so called mysterious crashes) and other code bases do not (they are rock solid). I don't think this is captured by just counting tracked defects (as a "mysterious crash" can one bug or many interacting). But, yes current industrial consensus is size is most important.
Thanks for replying. "Current industrial consensus" isn't that size is most important - far from it, approximately nobody pays any attention to that. Programs are routinely built by slathering code and people are proud of how much they write instead of how little.
Yet it seems to be what the evidence is telling us.
Ugh, `dynamic` doesn't provide the benefits of a dynamically typed language by long, long way, and yes I've used it extensively. But thanks for the downvote all the same.
In case you plan on skimming that and missing the details the tl;dr is that Rob, pushes `dynamic` as far as he can and it still comes up far short of the extensibility and expressiveness of Ruby constructs.
Upvoted. Spot on. It's nowhere near as dynamic as a basic dynamic language. So much so that I just don't bother with dynamic in C# (I also don't bother with nullable types and a load of other features as well but that's another story).
However, I don't find that a problem. I tend to prefer the correctness of statically typed languages these days.
That variance example seems contrived. I'm expected to believe that the programmer explicitly asks for integers in a dynamically-typed language and then is surprised by integer overflow?
"in a statically typed language the language would force.. coercion"
But R just did an implicit coercion, right there. You gave it ints, it gave you back 1e12. I don't know R well enough to understand what the int() call does, but there's no reason why a dynamic language can't perform implicit coercion. Python does:
>>> type(1000000)
<type 'int'>
[define sumXX]
>>> sumXX([1000000,2000000,3000000,4000000,5000000])
55000000000000L # <--- type long
I could wrap the elements of the list in int() calls but that would change nothing.
The whole point of dynamic languages is that you don't have to think about the types of your numbers. They're just numbers. If you have a use case that requires "4-byte integers, goddammit!" (and there are many valid ones), why yes, you should use a static language and think about overflow. If you don't want to think about overflow, the languages that go to the greatest lengths to shield you from it (lisps) are all dynamically typed.
Although many static languages do force you to choose a width for your integers, there is no inherent connection between static type systems and fixed-width integers. For instance, Haskell is one of the most strongly static languages out there and it has an unbounded Integer type. The connection between static typing and integer overflows is an artifact of older statically typed languages like C whose type systems were designed to model the hardware.
Ah, good point. So the whole notion of unboxed integers seems increasingly orthogonal to dynamic typing. And now so do explicit variable declarations (http://news.ycombinator.com/item?id=3633520).
The author's second and third points are either disingenuousness or ignorance.
* The omission of variable declarations has nothing to do with dynamic typing. Some dynamically-typed languages certainly do this (Ruby and Python). Some dynamically-typed languages make variable declarations optional or optionally-required (JavaScript and Perl). Some dynamically-typed languages require variable declarations (the Lisp family), and so do not suffer from the identifier misspelling problem.
* In my experience, Eclipse and other IDEs are not completely reliable when it comes to refactoring and variable renaming. Yes, they are good at it, but at the same time, they all offer preview modes and encourage the user to double-check the IDE's changes. I have seen Eclipse fail in renaming identifiers in Java code. For Lisps, I have found http://brian.mastenbrook.net/display/26 to be just about as effective as Eclispe's Java renaming.
As for the first point, the misuse of the word "macro" makes the entire argument rather difficult to address. It seems to boil down to taste.
> In my experience, Eclipse and other IDEs are not completely reliable when it comes to refactoring and variable renaming. Yes, they are good at it, but at the same time, they all offer preview modes and encourage the user to double-check the IDE's changes. I have seen Eclipse fail in renaming identifiers in Java code.
True, but an IDE will refactor accurately much, much more often on a statically type language than a dynamically one. Refactoring dynamically typed languages is pretty much impossible to do automatically when you don't have type information:
At work we have a Java backend and a very heavy/complicated JavaScript frontend. The frontend was born more recently while the Java backend has been around for years. Due to this situation, IntelliJ is the company-wide recommended development environment and most often used. IntelliJ is largely considered to be a quality IDE. And when using it for Java, it is. When using it for JavaScript, it's not only borderline useless but often downright counterproductive. We also have quite a bit of tooling written in Ruby, and most also use IntelliJ for that as well. Again IntelliJ falls on its face with Ruby too.
IntelliJ's ability to refactor Ruby and JavaScript is a joke and really just amounts to global searches and replaces, and wildly inaccurate results. I typically get out a plain text editor and ditch IntelliJ altogether.
> IntelliJ's ability to refactor Ruby and JavaScript is a joke and really just amounts to global searches and replaces, and wildly inaccurate results. I typically get out a plain text editor and ditch IntelliJ altogether.
Like I said in my comment above, this has nothing to do with IDEA: dynamically typed languages are just technically impossible to refactor automatically without the developer's supervision.
> dynamically typed languages are just technically impossible to refactor automatically without the developer's supervision
For sake of argument, let's say that's true and then ask - How much does that actually matter in practice? We have unit tests don't we?
Here's an example -
A very large Smalltalk application was developed at Cargill to support the operation of grain elevators and the associated commodity trading activities. The Smalltalk client application has 385 windows and over 5,000 classes. About 2,000 classes in this application interacted with an early (circa 1993) data access framework. The framework dynamically performed a mapping of object attributes to data table columns.
Analysis showed that although dynamic look up consumed 40% of the client execution time, it was unnecessary.
A new data layer interface was developed that required the business class to provide the object attribute to column mapping in an explicitly coded method. Testing showed that this interface was orders of magnitude faster. The issue was how to change the 2,100 business class users of the data layer.
A large application under development cannot freeze code while a transformation of an interface is constructed and tested. We had to construct and test the transformations in a parallel branch of the code repository from the main development stream. When the transformation was fully tested, then it was applied to the main code stream in a single operation.
Less than 35 bugs were found in the 17,100 changes. All of the bugs were quickly resolved in a three-week period.
If the changes were done manually we estimate that it would have taken 8,500 hours, compared with 235 hours to develop the transformation rules.
The task was completed in 3% of the expected time by using Rewrite Rules. This is an improvement by a factor of 36.
from “Transformation of an application data layer” Will Loew-Blosser OOPSLA 2002
That's my point though. I'm not ragging on IDEA. I'm just using it as an example because I see the reality of dynamic versus static languages every day.
However, I do feel though that IDEA made some poor choices and tried to get refactoring and autocomplete capabilities in JavaScript when they probably should have backed off, and IntelliJ's performance suffers (sometimes greatly) because of it. As a simple example: if you use ExtJS and have it loaded in your IntelliJ solution, try to rename a local variable named ownerct. IntelliJ will completely lock up for about 15 minutes. Why? Because ExtJs uses the variable "ownerct" throughout, and IntelliJ is mindlessly sucking in all those references in. Of course you can set up your IntelliJ to avoid this situation, it's just an example.
If dynamic refactoring so easy they were doing it 30 years ago, but one of the best IDE makers in the world can't generalize it for the masses today, doesn't that tell you something?
I use IntelliJ's Ruby editor every day, and I think you're being too harsh. It definitely is much weaker than it is in Java, but it does a good job with simpler refactorings local renames, variable extraction and inlining, and method extraction and inlining.
"The omission of variable declarations has nothing to do with dynamic typing."
I was inclined to say this as well when I was critiquing the first point (http://news.ycombinator.com/item?id=3633351). But on further thought, just about the only dynamic language that flags assignment to a typo'd variable name is scheme[1]. So it seems like a significant correlation.
JavaScript with "use strict"--which is basically the modern version of JavaScript, but has to be enabled explicitly because of backwards compatibility--also warns you about assigning a variable that hasn't been declared. Just shows that JavaScript really was influenced by Scheme :).
In both languages, the use of a variable is almost always preceded by a binding of that variable to a symbol or var using the "let" form. This accounts for most uses of variables, and attempting to access a misspelled variable will flag an error at compile-time.
With regard to destructive assignment, most Common Lisp compilers warn on setf calls when executed against unbound symbols. Clojure doesn't have even have assignment in the sense discussed here (refs are bound before use, and the compiler enforces this).
I learned to program with Ruby and used it pretty much exclusively for 2 years.
However, after I learned Java and C++. I found that I really preferred static typing, and the error checking you get from the compiler.
I've been learning Clojure, but I'm thinking of switching over to Haskell for my next project mainly because it has static typing (also because I'm intrigued by QuickCheck).
Am I the only one who thinks in types? Whether I'm coding Ruby, C, or Haskell, I'm always conscious of what type signature the function I'm writing has.
"Okay, this function will take two arguments, one will be an integer, and one will be a string, and it will return a list of strings."
It helps me reason about my code since I'm never going to want to, say, add an integer to a string, without some intentional type conversion in there.
I'm diving into haskell at the moment and really liking it. I always notated to myself what types a function took and returned, even if it was just in a comment, but with haskell there's a notation for it, and the compiler is pretty sophisticated about making sure that type-wise my function is doing what I'm expecting it to be doing.
Very good points. I was initially enamored by the glamor of dynamic typing and to some extent I still like the concept of not having to declare anything. It's like driving on the Autobahn without any speed limitations. However, since I hav en't done large scale software development using dynamic typing, I haven't faced bugs that are mentioned in this article. It takes a lot of discipline to reduce debugging costs in a dynamic typed language and I agree to the author's point that why deal with enforcing such discipline when you can simply use a statically typed language.
This argument is silly. Some things require more discipline than others. Using tools that inherently enforce discipline is easier than enforcing it through self-discipline, convention and constant reviewing.
They won't do so reliably if you're working in a larger or longer-lived project. Then there's much more chance for a mental disconnect between the writing of a procedure and the usage of the procedure. (Either in the form of each being done by a different person, or the same person but with some 'forgetting time' in between.) There's also much more chance for the source to start to acquire a large collection but almost-but-not-quite-the-same types.
The unit tests that were written with the original procedure are likely to only cover usage scenarios that are expected by the person who just wrote the procedure. Someone who's not in that headspace can easily be a lot more "creative" about coming up with surprising ways to try and use the code. So easily that it might even happen by accident. Like, say, as a result of a simple refactor. So in a reasonably-sized project, unit tests for basic functionality end up being a rather short Maginot Line for type errors.
The integer overflow bug? It depends on the quality of the test. It makes sense to test with the same types you intend to use in production, and to cover a wide range of values within each type (the array [1,2,3] for example would not be a good test for 64-bit integers.)
And static typing would not necessarily catch this bug anyway. A C++ template or a polymorphic Haskell function would fail in exactly the same way. What is called for here is argument conversion (A C function or java method declared with double arguments would convert its arguments automatically, but no reason you cannot convert explicitly in a dynamic language.)
The OP argues that although programs written in dynamic languages are smaller, they have more bugs. But empirical studies find the opposite: bug counts grow superlinearly with program length irrespective of language. In fact, program length is the best predictor of error rate we know. So his argument contradicts the evidence.
Chapter 27 of Code Complete (2nd ed.) discusses this. So does http://news.ycombinator.com/item?id=3037293 among other HN threads. Also, if anyone has access to Capers Jones' "The Impact of Program Size" from Programming Productivity (1986) and is feeling generous, please cite the relevant findings. I can't find it online.
The empirical evidence is as you say. It just hasn't been my experience that code bases with the same number of tracked defects are all the same. Some are brittle and littered with rare lurking bugs (rare in the test environment, but guaranteed to be hit by many in production) and other code bases are much more stable. I think this depends on the style, the rigor of design and the discipline seen throughout development. So even if my experience with static typed code bases has been more pleasant, dome of that could be due to the type of developer that uses them.
Thanks for the comments, I am learning some things. I have a longer comment in my article now- but I would like to point out the two bugs in question actually happened. The first one never made it into production because I deliberately write in small modular pieces and then use Emacs incremental search to search on important variables (causing all correct spellings to highlight so incorrect spellings become obvious in the deliberately regular format I write in) before checking in. The int problem did in fact happen. I didn't add a stupid int-cast (as in the example) but an R ODBC driver promoted a result to int based on something it secretly learned from the DB schema.
I think autocompletion as a selling point for static language is underrated. With autocompletion I feel I type less than half the characters that my code requires.
It is not just about completing a word. The completion can be a pattern of several lines of code with placeholders where the IDE jumps and waits for you to enter one or two characters that it completes again.
The resulting experience is that things just flow. The IDE frees your mind from details like name spelling, api method list, exact language syntax and usual idioms.
And it is probably just the beginning. The completion is still pretty basic when you think about it. At some point maybe, IDE will switch to a rule engine to manage thousands of completion rules.
Autocompletion tools are getting pretty good as it is. IntelliJ for Java and Resharper for C# are unnervingly good about predicting exactly what I would have typed even for stuff like declaring method names.
ActiveState's Komodo has autocomplete for some dynamic languages like Python. Obviously its not possible to be as accurate as static type languages, but it helps a lot.
I work on a team of about 10 people with Python on a pretty complex project. We have not experienced any typing issues in deployed code. I think this can be attributed to a combination of strong unit tests and type enforcement when constructing objects that are eventually serialized to the database. Python is stronger than some other dynamic languages at type enforcement too.
I believe I would prefer compile time type checking, but when working with a lot of dynamic data structures (say, JSON or XML) there is an ease that dynamic language toolkits can bring that a lot of the time evade more formal languages.
His example for the second point is horrible and contrived (not to mention stringly typed, which is an anti-pattern in every language). If anything, the "unhandled" should be assigned in and "else" block, not by implicit "fall-through". That would (probably, I don't know R) cause a failure for an undefined variable when trying to return it - which is exactly the same as having the compile complain about an undeclared variable.
In general, I just disagree. Strictly typed just isn't superior to dynamic typing at all. It's a different approach with its own pitfalls, and more than enough of them.
The final else block is a good point. In R this would cause an error during the attempt to return a non-existent variable (so at least R doesn't form a new variable on reference, just on assignment). This better is you now can see the error if it occurs. But it isn't everything as you may see this first in production (if you have insufficient testing). I still like the bug to not be at all possible. For example in Java we could declare the variable final at the top of the block and then the compiler would only accept code where the variable is assigned exactly once on all code paths (no matter how unlikely the path). This would catch a bunch of other popular errors (like forgetting to type "else" before one of the chained "if"s (as "else if" is unfortunately just a convention).
The three main points in the argument are wrong. I present as evidence this flower and a pack of cards.
Dynamic languages DO have refactoring tools. It's just that not many people use them.
That typo error WOULD have been caught with a static analyzer or with unit tests.
Yes, if you supply a function with a type you haven't tested for it might not work. Solution: test for that type, and validate your inputs.
The fourth argument that debugging is more expensive is not substantiated. I find dynamically typed systems easier to debug, especially when they don't have four hour compile times like some statically typed code bases do. Being able to more quickly change, and rerun code in dynamically typed systems gives them a big debugging advantage. Many dynamically typed systems even let you easily change code at run time (which I know is possible with statically typed systems too).
I think more major reasons would be dependent on the types of problems people are solving.
But, in practice, some dynamic languages just don't seem to suffer from hidden type bugs. One of those is Clojure. Instead of packing everything into complicated types, you are mostly working with maps and sequences of maps--just raw data--with higher order functions. There isn't a lot of room for type bugs. Additionally, without any input from the developer, Clojure does some type-inference all on its own. You can enable warn-on-reflection such that the Clojure compiler will tell you anywhere in the code where it's going to have to use reflection to figure out how to invoke a Java method or property on an object. You can fix them with type hints such that you get performance on par with statically typed Java.
Clojure protects you from name typos you might see in other dynamic languages and accidentally clobbering existing variables through the fact that most variables are really not variable at all. You bind a value once in a lexical scope and it can't be mutated. That's more of a benefit of functional languages and lexically scoped languages, than statically typed ones.
It may boil down to a matter of taste, but I don't see the need for fancy refactoring tools like "method extraction" in a language like Clojure. The methods already don't live in classes, so they hardly need to be extracted from them :). If you rename a method in Clojure, the compiler _will_ complain in all the places where you are trying to call a method that no longer exists.