Good point, but the debate is still in the "C++ vs Python" bounds. Haskell has Hindley-Milner and Quickcheck and I think the debate is elsewhere: the cost of abstractions and tools (just read another doc/paper to understand how to do this) but also their benefits (scaffoldings and cranes while programming).
The comparison of static typing to anti-lock brakes is a bit off. Anti-lock brakes are more like the function of an optimizing compiler (JIT or otherwise). Unless you're on-top of your game all the time and have race car driver-like reflexes, you can't beat it.
Yes, this is true, the dynamic language communities have compensated for the lack of a type system with explicit type checks (or type constraints) and lots of testing. But it's still nice to have a real type system (and I don't mean C or C++ or Java; those languages also have no type system). When I compile a Haskell project successfully, I have a reasonable belief that it's going to run to completion and at least produce some sort of result. The same is not true of Perl, even with lots of tests, my programs almost always die in the middle (during development) where a runtime type check or assertion fails.
The reality of programming is that all functions and variables have types. If you add 42 to "hello world", you get garbage. In dynamically typed languages, you keep the types in your mind -- you know that the variable you are adding 42 is a number. In statically typed languages, you tell the compiler. The second way lets the computer do some of the work for you (ensuring that if you use the variable like a string, your program won't compile), the first way requires you to do everything. If you aren't careful, you get the wrong answer or a runtime error.
I think dynamic typing was a nice way for the programming language community to realize that C's type system sucked, but it isn't the ultimate answer, it's just a stopgap. It's also good that people realized they needed to test their code automatically; but that works even better with statically-typed languages.
Anyway, I am not going to give up Perl anytime soon, but I secretly wish it wasn't dynamically-typed.
Is running to completion and producing an incorrect result really preferable to dying in the middle of execution due to a runtime check or assertion? Either your program works or it doesn't.
With dynamically typed languages and implicit variable declarations, I find the most common runtime errors are, in descending order of frequency:
* Something returned null when I assumed it would return a value/object.
* I used the wrong variable.
* I made a typo in an assignment that implicitly created an unused variable
(this is easy to catch with static analysis even in dynamically typed
languages, admittedly).
The first one is by far the most common.
I'm not particularly experienced with Haskell, but my understanding is that the static typing will usually catch the second one and that more advanced static analysis (more advanced than type checking) can easily catch the third one.
Most important, though, is the case of the first and most common error. Here, the language will very explicitly require you to handle the case that "Nothing" will potentially be returned.
So, most of the sorts of errors that would have caused failed execution in a dynamically typed language will be caught at compile time. This means that when the program runs, it will usually be closer to being correct than it would be if it were written in a dynamically typed language and failed in the middle of execution.
I agree. One of the biggest benefits of a proper type system (and by saying "proper" I immediately exclude C and Java, so don't even bother mentioning them) is that you get clean semantics for both nullable and non-nullable types. Hoare called null a billion-dollar mistake, and I completely agree.
and by saying "proper" I immediately exclude C and Java, so don't even bother mentioning them
If there was a "jrockway award for explaining type systems", this statement would qualify you for one. I am glad I'm not the only person that thinks this :)
Not all dynamic languages implicitly declare variables, I'm a Smalltalker and this just isn't a problem. If I make a typo and try to save it, it'll tell me this variable isn't declared and ask me how I'd like to declare it.
Using the wrong variable, I just never seem to have that problem as well named variables make the code just look wrong when it happens and secondly because I'm writing the code while the code is running in a live environment. I see immediately when it does work because the first thing I do is execute it on a live object to make sure it does what it's supposed to do. This handles the null issue as well. Incremental live development just side steps about all of these issues, and is something static languages just don't let me do.
More often than not, I've found type systems to prevent me from expressing what I want to express even when the code would work perfectly fine simply because polymorphism isn't granular enough. In dynamic type systems, polymorphism is based on method signature alone and is not bound to any kind of class/interface/type class which gives me the most flexibility in expressing myself. I prefer that flexibility and incremental live development over any gain any static type system might give me.
Most important, though, is the case of the first and most common error. Here, the language will very explicitly require you to handle the case that "Nothing" will potentially be returned.
Well, actually no. Consider:
divide :: Int -> Int -> Maybe Int
divide x 0 = Nothing
divide x y = Just $ x / y
printResult :: Maybe Int -> IO ()
printResult (Just x) = print x
Now, run:
main = do
putStr "Type a number: "
x <- readLn
divide 42 x >>= printResult
This will still die if you type 0, because there is no pattern to match the Nothing case. Sure, you can run Catch to see that you made a mistake, but the compiler doesn't care. (At least you can run Catch to determine that you messed up, though. If you are using Java or Ruby or something, you are just hosed.)
Anyway, my usual error is not null or using the wrong variable or typoing something. It's usually failing type checks because I forgot a coercion, or just plain wrong code. Exceptions prevent null returns, and I am lucky with respect to the other two.
Interesting. I assumed Haskell would at least warn you. But it's probably only a matter of time before Catch is built in to the compiler, at least as an option.
: kragen@inexorable:~/mail ; ocaml
Objective Caml version 3.10.2
# let divide x = function 0 -> None | y -> Some (x / y) ;;
val divide : int -> int -> int option = <fun>
# let printResult = function Some x -> print_int x ;;
Warning P: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
None
val printResult : int option -> unit = <fun>
# printResult (divide 9 3) ;;
3- : unit = ()
# printResult (divide 10 0) ;;
Exception: Match_failure ("", 1, 18).
#
It needs to be fixed up a bit. You can somewhat resolve this issue by compiling with -Wall, at which point it will warn you that your pattern matching is non-exhaustive.
import System.IO
divide :: Int -> Int -> Maybe Int
divide x 0 = Nothing
divide x y = Just $ x `div` y
printResult :: Maybe Int -> IO ()
printResult (Just x) = print x
main = do
putStr "Type a number: "
hFlush stdout
x <- readLn
printResult $ divide 42 x
An incorrect answer gives me feedback on why my thought process was wrong. A program that dies in the middle just means my typing is not so good.
Remember, most of your time is spent with a broken program rather than with a working program. Once it works, you don't think about it anymore. Getting to that working state is what programming is.
Good dynamic systems don't just die, they halt on the error and fix them on the fly in the debugger. Most of my time is spent on a working program, not on a broken one.
Social networking web sites? Yes. Video games? Definitely.
Business applications (or anything dealing w/money)? You sure had better fail and roll back any transactions.
But I think your point stands when you consider that the cases where it's ok to keep going after something wrong happens are the exception, and not the rule (most software is automating boring business practices).
In that case, why not let the computer tell you whether or not the types are reasonably self-evident? That's what static typing is.
(Remember, don't mash up "Java and C++" with "static typing". Java and C++ are not worth mentioning when discussing type systems; they have type systems that get in the way and make it harder to code.)
I've only every programmed in Java/C++, and dynamic languages, so I don't know what you mean.
Do you know of any good resource to read up about a better type system and its benefits, but without having to learn a whole new language? (I don't have time to learn a new language, but this sounds important enough to read up on).
A lot of the fun-to-program statically typed languages use type inference to determine types automatically. In these languages, as long as you make your types reasonably clear the compiler can go through and do type checking for you without you ever having to explicitly type a single expression.
The big problem with statically typed languages isn't having to say "int" constantly. It's having to decide on a type hierarchy.
What I mean is, when I write a function to do something with files, I don't want to have to decide whether I'm going to accept a "FILE" class or not. This isn't laziness: it's realizing that I will one day want to use a different class which looks like a file, but isn't part of the "file" hierarchy.
This situation comes up far more often than one might think, and it saves a lot of headaches up-front (designing complicated hierarchies to prevent future use-cases you'll want).
From what I've read, Type inference doesn't help with that. (But I haven't read much, so I could be wrong).
You can't learn this stuff by reading about it, you have to experience it. Some of your counter-arguments are outright confusing to those who have programmed in languages with type inference (hint: they don't necessarily have classes).
Experience may help you change your mind. Reading papers and advocacy from people who've used it should provide limited influence.
As an aside, there are solutions to programming problems I would've never come up with if it weren't for experience in other programming languages, so I read "don't have time to learn a new programming language" as "don't have time because I haven't learned a new programming language."
I think dynamic typing was a nice way for the programming language community to realize that C's type system sucked....
Dynamic typing is an answer to the problem that getting types right in a program is as difficult as getting anything else right in a program, especially given the limitations of type systems. I can write a trivial Haskell program which will not halt (due to trivial mathematical properties of negative numbers) but which the type checker will allow by allowing the compiler to infer the wrong type.
This is true for "average drivers". The math is totally different with, say, racecar drivers.
I think there's a strong parallel with programmers here, and whether you're optimizing for average-case job throughput (or hobby throughput) or whether you're optimizing something specific where safety or reliability is specifically important.
I ride a bicycle in the city with a helmet, knowing that I do it in order to ride a little more dangerously. It gets me where I'm going faster. When I'm without a helmet in the city I ride very defensively.
I use Python and I'm very slowly getting into Haskell, I don't have much experience with static typing, but I think that the analogy will apply when I get used to it. I enjoy the safety net so that I can, in fact, be a little more reckless in my coding. Experiment a bit more, get things done a little faster, etc.
Static typing is like a helmet that forces a speed limit, too. It won't necessarily help you experiment faster-- the common wisdom is that it does the opposite, in Java/etc. style static typing.
Which is why we need to work on dispelling the myth that static typing requires explicit typing. Type inference gives you many of the advantages of type checking without having to explicitly type every variable.
I worked on a 20 KLOC app of mine yesterday that has neither static type decls or unit tests and yet is AFAIK bug free. This has been the rule and not the exception in my experience. So I prefer to not have them since in the general case they are not needed and just add clutter and drag.
Good perspective, thanks. And yes, many users other than me. Another way of looking at it though is to say if a bug has never been encountered that arguably it is not actually a bug and/or it doesn't matter. Anti-Heisenbug? :)
Bugs can exist within a coding session. I consider that a normal part of the development effort. You think of some change you want to make. You make it. If you discover you made a typo or overlooked some issue and thus it has caused a bug, during your dev-testing, you fix it. Once the desired change is done, working as intended, with no bugs, that change-sprint is done and commited to your VCS. Then you end the dev session or advance to the next desired change. The key is that when your dev session ends, and when you are just wearing the user's hat, there should be no bugs. In my experience, bugs can be found usually quickly and easily within the same coding session that introduced a new feature, while the code and your recent change are all fresh in mind, and you are in The Zone.
That was a good heuristic you suggested, but I think it applies to situations where a bug survives beyond the end of a development session, accidentally.
bugs can be found usually quickly and easily within the same coding session that introduced a new feature, while the code and your recent change are all fresh in mind, and you are in The Zone
That's probably true for 99% of bugs. But every time you say "no bugs" I'm inclined to interrupt and say "you mean, no known bugs!"
There are probably large codebases without bugs; some or all of: qmail, some implementations of cryptographic algorithms, seL4, and some versions of the Space Shuttle in-flight software. But they're very rare. Maybe at some point every single one of the above codebases will have a bug found in it.
Regarding the anecdote rather than the meat of the post, I drove for years under all conditions without anti-lock brakes. These days, I still find myself occasionally fighting them. I know very well how to control the car, including through controlled skids, and that's worked its way deep into my reactions. In extreme situations, the anti-lock brakes still sometimes throw me off and slow my overall reactions, as I have to pause and adjust to their presence. I also simply cannot perform some -- often useful -- maneuvers with them that I can on a vehicle not having anti-lock brakes.
So... I can imagine some of the cab drivers, with years of driving experience, having some difficulty adjusting to the difference that anti-lock brakes make, with a corresponding increase in effective reaction time and with perhaps a surprising inability to negotiate risks as they have previously learned to and expect to.
37 comments
[ 3.2 ms ] story [ 103 ms ] threadThe reality of programming is that all functions and variables have types. If you add 42 to "hello world", you get garbage. In dynamically typed languages, you keep the types in your mind -- you know that the variable you are adding 42 is a number. In statically typed languages, you tell the compiler. The second way lets the computer do some of the work for you (ensuring that if you use the variable like a string, your program won't compile), the first way requires you to do everything. If you aren't careful, you get the wrong answer or a runtime error.
I think dynamic typing was a nice way for the programming language community to realize that C's type system sucked, but it isn't the ultimate answer, it's just a stopgap. It's also good that people realized they needed to test their code automatically; but that works even better with statically-typed languages.
Anyway, I am not going to give up Perl anytime soon, but I secretly wish it wasn't dynamically-typed.
I'm not particularly experienced with Haskell, but my understanding is that the static typing will usually catch the second one and that more advanced static analysis (more advanced than type checking) can easily catch the third one.
Most important, though, is the case of the first and most common error. Here, the language will very explicitly require you to handle the case that "Nothing" will potentially be returned.
So, most of the sorts of errors that would have caused failed execution in a dynamically typed language will be caught at compile time. This means that when the program runs, it will usually be closer to being correct than it would be if it were written in a dynamically typed language and failed in the middle of execution.
If there was a "jrockway award for explaining type systems", this statement would qualify you for one. I am glad I'm not the only person that thinks this :)
Using the wrong variable, I just never seem to have that problem as well named variables make the code just look wrong when it happens and secondly because I'm writing the code while the code is running in a live environment. I see immediately when it does work because the first thing I do is execute it on a live object to make sure it does what it's supposed to do. This handles the null issue as well. Incremental live development just side steps about all of these issues, and is something static languages just don't let me do.
More often than not, I've found type systems to prevent me from expressing what I want to express even when the code would work perfectly fine simply because polymorphism isn't granular enough. In dynamic type systems, polymorphism is based on method signature alone and is not bound to any kind of class/interface/type class which gives me the most flexibility in expressing myself. I prefer that flexibility and incremental live development over any gain any static type system might give me.
Well, actually no. Consider:
Now, run: This will still die if you type 0, because there is no pattern to match the Nothing case. Sure, you can run Catch to see that you made a mistake, but the compiler doesn't care. (At least you can run Catch to determine that you messed up, though. If you are using Java or Ruby or something, you are just hosed.)Anyway, my usual error is not null or using the wrong variable or typoing something. It's usually failing type checks because I forgot a coercion, or just plain wrong code. Exceptions prevent null returns, and I am lucky with respect to the other two.
Remember, most of your time is spent with a broken program rather than with a working program. Once it works, you don't think about it anymore. Getting to that working state is what programming is.
Social networking web sites? Yes. Video games? Definitely.
Business applications (or anything dealing w/money)? You sure had better fail and roll back any transactions.
But I think your point stands when you consider that the cases where it's ok to keep going after something wrong happens are the exception, and not the rule (most software is automating boring business practices).
That's one way, or you could write clear unambiguous code such that the types are reasonably self evident.
(Remember, don't mash up "Java and C++" with "static typing". Java and C++ are not worth mentioning when discussing type systems; they have type systems that get in the way and make it harder to code.)
Do you know of any good resource to read up about a better type system and its benefits, but without having to learn a whole new language? (I don't have time to learn a new language, but this sounds important enough to read up on).
http://en.wikipedia.org/wiki/Type_inference
What I mean is, when I write a function to do something with files, I don't want to have to decide whether I'm going to accept a "FILE" class or not. This isn't laziness: it's realizing that I will one day want to use a different class which looks like a file, but isn't part of the "file" hierarchy.
This situation comes up far more often than one might think, and it saves a lot of headaches up-front (designing complicated hierarchies to prevent future use-cases you'll want).
From what I've read, Type inference doesn't help with that. (But I haven't read much, so I could be wrong).
Experience may help you change your mind. Reading papers and advocacy from people who've used it should provide limited influence.
As an aside, there are solutions to programming problems I would've never come up with if it weren't for experience in other programming languages, so I read "don't have time to learn a new programming language" as "don't have time because I haven't learned a new programming language."
Dynamic typing is an answer to the problem that getting types right in a program is as difficult as getting anything else right in a program, especially given the limitations of type systems. I can write a trivial Haskell program which will not halt (due to trivial mathematical properties of negative numbers) but which the type checker will allow by allowing the compiler to infer the wrong type.
I think there's a strong parallel with programmers here, and whether you're optimizing for average-case job throughput (or hobby throughput) or whether you're optimizing something specific where safety or reliability is specifically important.
I use Python and I'm very slowly getting into Haskell, I don't have much experience with static typing, but I think that the analogy will apply when I get used to it. I enjoy the safety net so that I can, in fact, be a little more reckless in my coding. Experiment a bit more, get things done a little faster, etc.
http://en.wikipedia.org/wiki/Type_inference
http://www.gizmag.com/go/7255/
"Drivers were as much as twice as likely to get particularly close to the bicycle when [the cyclist] was wearing the helmet.
Across the board, drivers passed an average of 8.5 cm (3 1/3 inches) closer with the helmet than without.
The research has been accepted for publication in the journal Accident Analysis & Prevention."
20/20 video: http://www.youtube.com/watch?v=YdoE2YCvwdM
That was a good heuristic you suggested, but I think it applies to situations where a bug survives beyond the end of a development session, accidentally.
That's probably true for 99% of bugs. But every time you say "no bugs" I'm inclined to interrupt and say "you mean, no known bugs!"
There are probably large codebases without bugs; some or all of: qmail, some implementations of cryptographic algorithms, seL4, and some versions of the Space Shuttle in-flight software. But they're very rare. Maybe at some point every single one of the above codebases will have a bug found in it.
So... I can imagine some of the cab drivers, with years of driving experience, having some difficulty adjusting to the difference that anti-lock brakes make, with a corresponding increase in effective reaction time and with perhaps a surprising inability to negotiate risks as they have previously learned to and expect to.