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