Pinderkent

Pain and glory from the trenches of the IT world.

Static typing saves developer time. Dynamic typing often wastes it.

Posted on Thursday, January 22, 2009 at 12:53 AM.

I just finished reading this account of a developer who spent over an hour trying to track down an unexpected application crash that was eventually determined to be due to a tuple being passed to a function that was expecting a list. This incident happened while using Erlang. For certain types of systems, Erlang is a fantastic language and platform to use. In the coming years, it is likely that its model for handling concurrency will become familiar to most developers. Unfortunately, for all of its benefits, Erlang does have some drawbacks. One of the most significant is that it is a dynamically typed language.

The main problem with dynamic typing is that it allows incidents like the one described by Travis. A simple typo, in his case involving only two characters, can quickly become an hour or more of wasted time. Were static typing, such as that offered by OCaml or Haskell, being used, the problem would have been detected immediately.

Using OCaml (via its interpreter) as an example, we'll define a function that takes a list of strings, and prints each one to stdout:
    $ ocaml
        Objective Caml version 3.11.0

    # let f = List.iter (Printf.printf "%s\n");;
    val f : string list -> unit = <fun>

Next, let's see what happens when we accidentally call it with a tuple of strings:
    # f ("test", "test", "test");;
    Error: This expression has type string * string * string
        but is here used with type string list

It clearly has detected the mistake, and lets us know right away that there's a problem. Better yet, it tells us exactly what the problem is: it expects the argument to that function to be a list of strings, rather than the tuple of three strings that we passed it. Now that we have so quickly identified our mistake, we can go ahead and fix it. Passing in a list of strings has the expected result:
    # f ["test"; "test"; "test"];;
    test
    test
    test
    - : unit = ()
    #

We would have gotten similar results were we using the OCaml compiler rather than the interpreter.

Using a language that offers static typing is clearly the better route to take when we care about saving developer time, writing quality software, and detecting potential bugs and problems as soon as possible. It's unfortunate that Erlang is a dynamically typed language, as static typing would help increase the quality of code written using it even further. And it would've caught a minor mistake that has caught at least one developer over an hour of time.

Permalink: http://pinderkent.phumblog.com/post/2009/01/static_typing_saves_developer_time_dynamic_typing_often_wastes_it
Share:

A small example of the hidden dangers of dynamically typed programming languages.

Posted on Sunday, February 17, 2008 at 9:09 AM.

Several days back I wrote about how unit testing is not a substitute for static typing. A comment posted to that article by James asked for more clarification regarding what I was talking about. James wrote, "I can't recall the last time I had Ruby code break because I tried to act on an object of the "wrong" type." Well, I will give a simple example of how such problems arise, and how different languages deal with them. The languages in question will be Ruby, Python, OCaml and Haskell.

Our example program will be simple. It will consist of two functions. One will be called 'test', it will take two integers, and it will return the arithmetic sum of them. The other function will be called 'main', and it will invoke the 'test' function in two different ways, depending on the size of the array or list containing the command line arguments passed to the program. If there are over three command line arguments, the result of the 'test' function applied to the values 1 and the function 'test' is returned. Otherwise, the value of 'test' applied to values 1 and 2 is returned. Thus the program should have a return value of 3 up until the array or list storing the command line parameters has more than 3 elements.

Since James mentioned Ruby, let's start with the Ruby version of the code:

def test(a, b)
  a + b
end

def main()
  if ARGV.length > 3
    test(1, test)
  else
    test(1, 2)
  end
end

Process.exit(main())

And now we'll run it, with warnings enabled and set at the most verbose setting:

$ ruby -w -W2 t.rb; echo $?
3
$ ruby -w -W2 t.rb 0; echo $?
3
$ ruby -w -W2 t.rb 0 1; echo $?
3
$ ruby -w -W2 t.rb 0 1 2; echo $?
3
$ ruby -w -W2 t.rb 0 1 2 3; echo $?
t.rb:7:in `test': wrong number of arguments (0 for 2) (ArgumentError)
        from t.rb:7:in `main'
        from t.rb:13
1
$

As is expected from a dynamically typed language like Ruby, the error wasn't detected until runtime. Not only that, but the program ran quite successfully before then, without giving any indication that a hidden problem might arise were too many command line arguments given. Even were unit tests to be used, it is quite possible that duplicating such a scenario would be missed, and a perplexed user would be faced with an error such as the one above.

Python doesn't fare much better. Here is the code we'll use for it:

"""docstring"""
import sys

def test(first_arg, second_arg):
    """docstring"""
    return first_arg + second_arg

def main():
    """docstring"""
    if len(sys.argv) > 3:
        return test(1, test)
    else:
        return test(1, 2)

sys.exit(main())

Just to be safe, that code was run through the pylint utility, which rates the above code as "10.00/10". So an unsuspecting programmer may believe they have written high-quality Python code, when they surely have not, as we will soon see when we go to run the code:

$ python -W error t.py; echo $?
3
$ python -W error t.py 0; echo $?
3
$ python -W error t.py 0 1; echo $?
3
$ python -W error t.py 0 1 2; echo $?
Traceback (most recent call last):
  File "t.py", line 15, in ?
    sys.exit(main())
  File "t.py", line 11, in main
    return test(1, test)
  File "t.py", line 6, in test
    return first_arg + second_arg
TypeError: unsupported operand type(s) for +: 'int' and 'function'
1
$

Python behaves similarly to Ruby. The error isn't caught until runtime, and there's a good chance that unit testing would not have caught it, as well.

Let us try OCaml, a statically typed language. Compiling:

let test a b =
  a + b;;

let main _ =
  if (Array.length Sys.argv) > 3 then
    test 1 test
  else
    test 1 2;;

exit (main ());;
gives the following error:
$ ocamlopt -w A t.ml 
File "t.ml", line 6, characters 11-15:
This expression has type int -> int -> int but is here used with type int
$
The OCaml interpreter also catches the error, even without the code being executed:
$ ocaml -w A t.ml 
File "t.ml", line 6, characters 11-15:
This expression has type int -> int -> int but is here used with type int
$ 

Unlike when using Python or Ruby, the OCaml compiler and interpreter catch the error before the code begins to execute. And note that this is done without any source-level type annotations. This compile-time failure forces the programmer to deal with the error, rather than the user. Thus we end up with a more reliable program. Not only that, but the error was caught without the developer having to write even a single unit test. Now instead of writing unit tests to check if his or her code types correct, the developer can write unit tests to check the actual functionality of his or her software.

We can write a similar program using Haskell, another statically typed language, and it will also catch the error during compilation:

import System(getArgs)
import System.Exit

test :: Int -> Int -> Int
test a b = a + b

main :: IO ()
main = do
  args <- getArgs
  if (length args) > 3
    then exitWith (ExitFailure (test 1 test))
    else exitWith (ExitFailure (test 1 2))

When we go to compile the above program using GHC, we get:

$ ghc -Wall t.hs 

t.hs:11:39:
    Couldn't match expected type `Int'
           against inferred type `Int -> Int -> Int'
    In the second argument of `test', namely `test'
    In the first argument of `ExitFailure', namely `(test 1 test)'
    In the first argument of `exitWith', namely
        `(ExitFailure (test 1 test))'
$
The same error is given by ghci, the interactive REPL of GHC. As with OCaml, the problem is caught at compile time, rather than runtime. Thus the developer must actively deal with it for his or her program to just compile, let alone execute. This helps increase the program's reliability.

As we have clearly seen above, dynamically typed languages like Ruby and Python can allow for some flawed code to be written with ease. But more dangerously, it is possible for the code to run just fine, until a certain context arises upon which a runtime error occurs. Even when running the interpreter with warnings enabled, or after using a code-checking tool like pylint, such problems go completely undetected. This deception is dangerous. Some developers think that unit testing for such situations is appropriate, but there's a very good chance that such typing errors won't be detected by the unit tests, either.

It can be said that the code above is unrealistic. That's true. But the scenario it simplifies is very real. Based on my own experience, I have seen far too many problems arise with dynamically typed languages, where rarely-executed code contains a type-related error, and the execution of this code causes a runtime error (usually at a most inopportune time).

Thankfully, statically typed languages provide a very natural way of avoiding such problems at runtime, instead having them be caught at compile-time. When using languages like Haskell and OCaml, that support type inference, it's not even necessary for the developer to specify the types in the source code. So such developers get the convenience of languages like Python and Ruby, but without the inherent runtime danger of those languages, and without the inconvenience of having to write unit tests to handle checking that a proper compiler can do automatically, and more rigorously than a human could.

Permalink: http://pinderkent.phumblog.com/post/2008/02/a_small_example_of_the_hidden_dangers_of_dynamically_typed_programming_languages
Share:

Unit testing is not a substitute for static typing.

Posted on Saturday, February 09, 2008 at 6:29 PM.

Users of of dynamic languages such as Python, Perl and PHP, are often faced with the additional task of ensuring that type-related errors do not occur in their programs at runtime. Some authors [1, 2] claim that this can be done effectively by the use of extensive automated unit tests. However, practice has shown this to just not be the case.

Automated unit tests have their place. But they should not be used to check for typing errors. Software typechecking is a mechanical task, and thus is just the sort of thing that should be delegated to a computer. And this has been done for quite some time now, in the form of statically typed programming languages. Users of such languages, such as Haskell, OCaml and Standard ML are freed from having to worry about such menial and tedious tasks. The rigorous nature of the compilers for those languages often do a far better job at checking for type errors than a human typically could.

Proponents of dynamic languages often suggest that such languages bring an increase in developer productivity. But if the developer ends up spending so much time writing unit tests to perform a task that is readily done automatically, we really don't see the supposed productivity boost. And if the developer doesn't write such unit tests, the chances of a runtime type error crashing the application are far greater. So now the developer must choose between his or her software crashing and disrupting the users' work, or spending a lot of time to write unit tests. None of the choices are reasonable, let alone optimal.

Static typing is clearly the answer. Not only does the compiler take care of the tedious task of type checking, but any inconsistencies are detected at compile-time, rather than runtime. Thus it is the developer who deals with such errors, rather than the user. Furthermore, the developer is now freed up from writing automated unit tests to check for type errors, and can instead put more emphasis on unit testing the functionality and integration of his or her code. Clearly, static typing is the only sensible, and the most efficient, route to take.

Permalink: http://pinderkent.phumblog.com/post/2008/02/unit_testing_is_not_a_substitute_for_static_typing
Share:
Feeds
  • RSS 2.0 Feed
  • Atom 2.0 Feed
Tags
Archives