August 18, 2013

The Human Consequences of Dynamic Typing

I agree with Jay McCarthy that static typing is anti-human. I think I can give a slightly more elaborate example of a program that every static type checker must reject, but that is nevertheless correct. Everything below is valid Common Lisp:

(defclass person ()
  ((name :initarg :name
         :accessor person-name)))
(defmethod display ((p person))
  (format t "Person: Name ~S, address ~S."
            (person-name p)
            (person-address p)))

A static type checker must reject this program because there is no address field defined in the class person that person-address presumably refers to. However, below is a run of the program in a Lisp listener that runs to a correct completion without fatal (!) errors:

CL-USER 1 > (defvar *p* (make-instance 'person :name "Pascal"))
*P*
CL-USER 2 > (display *p*)
Error: Undefined function PERSON-ADDRESS called with arguments
(#<PERSON 4020059AB3>).
  1 (continue) Try invoking PERSON-ADDRESS again.
  2 Return some values from the call to PERSON-ADDRESS.
  3 Try invoking something other than PERSON-ADDRESS with the
    same arguments.
  4 Set the symbol-function of PERSON-ADDRESS to another
    function.
  5 (abort) Return to level 0.
  6 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for
other options.
CL-USER 3 : 1 > (defclass person ()
                  ((name    :initarg :name
                            :accessor person-name)
                   (address :initarg :address
                            :accessor person-address)))
#<STANDARD-CLASS PERSON 4130371F6B>
CL-USER 4 : 1 > :c 1
Error: The slot ADDRESS is unbound in the object
#<PERSON 41303DD973> (an instance of class
#<STANDARD-CLASS PERSON 4130371F6B>).
  1 (continue) Try reading slot ADDRESS again.
  2 Specify a value to use this time for slot ADDRESS.
  3 Specify a value to set slot ADDRESS to.
  4 (abort) Return to level 0.
  5 Return to top loop level 0.
Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for
other options.
CL-USER 5 : 1 > :c 3
Enter a form to be evaluated: "Belgium"
Person: Name "Pascal", address "Belgium".
NIL
CL-USER 6 > (display *p*)
Person: Name "Pascal", address "Belgium".
NIL

The "trick" is that the Lisp listener allows for interactive modification of a program while it is running. No static type checker can anticipate what modifications will be performed at runtime.

This is not a toy feature of Common Lisp, but something that many Lisp developers rely on. For example, my own ContextL library crucially relies on the very class redefinition feature I demonstrate above. Joe Marshall provides another account how such features can solve real-world problems.

11 Comments:

Blogger Smarter said...

"A static type checker must reject this program because there is no address field defined in the class person that person-address presumably refers to."
Sure, but it should be trivial to define one, in Haskell for example:
person-address = undefined
The program will build, and blow up at runtime, like yours. And if you're running it in a repl(like GHCi), you can then actually write the function.

06:12  
Blogger Pascal Costanza said...

The point of this example is that you don't have to provide a definition for person-address at all to run this program, not even an "undefined" one. I know it's hard to imagine that this could be crucial given just such a small example, but trust me, it is. ;)

07:47  
Blogger Pipo Prvy said...

Is not this just an example of differences in how Lisp REPL is used compared to e.g. Haskell. I understand in Common Lisp "runtime programming" seems almost idiomatic. Haskell is more conservative following "WCL" (write-compile loop).

Now imagine this happens in production. The static typed version is more likely to catch this type of errors where Lisp version works fine just until untested path is reached and results in a runtime error and a call to support.

Is there a way in CL to check all the paths are fine but to run them (which is in most cases at least not feasible?) I am not aware of one.

On the other hand in Haskell one can use error (or todo wrapper below) anywhere so it is easy to grep and implement later for such paths. I am not aware if there is a way to replace such implementation at runtime.

todo :: String -> a
todo s = error ("Not implemented. "++s)

+1 for Static typing.

15:50  
Blogger Pascal Costanza said...

Pipo,

No, this is not just a REPL example, this is part of the semantics of the Common Lisp object system which you can rely on in any program.

This feature, and similar others, are actually taken advantage of in production systems. The Common Lisp condition / exception system is designed in such a way that you can both interactively and programmatically change and re-execute relevant parts of your program.

Good Common Lisp compilers give warnings for undefined functions, type mismatches, and similar issues. Unlike most statically typed languages, however, they don't keep you from running a program even in the presence of such warnings. This is a crucial advantage.

There is no point in having to manually insert stubs in your program just to make it compile and run. Please also check out Joe Marshall's blog post for a more comprehensive example that illustrates this.

-1 for you. ;)

21:34  
Blogger Smarter said...

"Good Common Lisp compilers give warnings for undefined functions, type mismatches, and similar issues."

GHC 7.6 can defer type errors until runtime too: http://www.haskell.org/ghc/docs/latest/html/users_guide/defer-type-errors.html :).

03:16  
Blogger Pascal Costanza said...

It's nice to see that GHC provides such a feature. However, I suspect that this is considered merely an aid during software development, not a feature for deployed software. What I show in my blog post is that you can react to the exception / condition you get when calling an undefined function, or accessing an undefined field, provide the definition after the fact, and then continue the execution without restarting the whole program. You can make this work without requiring user interaction. This is part of the Common LIsp language specification, not an extension of a particular compiler. As I already stated in my blog post, for example my own ContextL library relies on this feature in its implementation, which is portable across eight different Common Lisp implementations.

With GHC, can you react to such type errors at runtime, change the involved definitions, and then continue running? Is this a feature you can rely to consistently work in well-specified ways?

17:29  
Blogger Pascal Costanza said...

Daniel Herring sent me the following comment and gave me permission to reproduce it here:

"There are many reasons to patch running code. Live upgrades and security patches are two examples. Interactive debugging is another use case, especially when bugs are hard to trigger or understand, algorithms are ill-defined, etc. By their post-facto nature, these examples preclude full knowledge when the code is first compiled.

CLOS provides excellent support for the object changes that are often required. Factor tracks filesystem changes to simplify world updates, and it (at least used to) also tracks and re-applies changes to inlined functions.

While language support for dynamic changes (including typing) is helpful, it is not strictly required. Language runtimes usually provide powerful debug hooks. Coupled with an intimate knowledge of the runtime, these make hot patching possible. For example, patches may be applied by atomically inserting jumps to new code. Large structural changes may require inserting breakpoints to guard the change.

In fact, Ksplice has made a business of patching live Linux kernels."

17:32  
Blogger Big Rick said...

This sort of wide-open system would be easier to exploit by the black-hats. And it could be harder to test.

I don't have any real experience with such dynamical systems, though I've coded lisp and Ruby. And I have great respect for the ability of people to adapt and manage in the face of the unexpected. But it sounds like a formula for unreliability to me.

Think about a computerized space shot, surgery, Mars robot, traffic controller, driverless car, smart bomb, or your bank account.

Some kinds of systems are by definition flexible in the face of dropped data and failures: Craigslist, Google search, restaurant selection. Others are more demanding.

18:11  
Blogger Pascal Costanza said...

Big Rick,

The issues you are concerned with are orthogonal to the issues being discussed here. If somebody can gain unauthorized access to a system, they can affect reliability in negative ways, no matter whether the system is dynamically or statically typed, or whether it allows for runtime changes or not. You need good security and protection in any case.

However, dynamically adaptable systems can help with reliability. For example, it was possible to fix the flash memory management anomaly that occurred with the Mars rover Spirit while the system was deployed because it provided ways to dynamically adapt it, not in spite of this. Since it is hard to anticipate what potential problems might occur, it's better to provide as many adaptation opportunities as possible, to ensure (eventual) reliability.

19:06  
Blogger Big Rick said...

This is good to hear. But I'm still concerned that the language processor would not warn the programmer about referencing undefined fields before the software goes into production.

I don't use common lisp so I don't know if would provide that warning, at "compile time".

It is good engineering practice in general to eliminate sources of variability as much as possible; So that the bits that run in production are the bits that you tested before hand.

In the case of the Mars software this was true, they ran it on a ground "satellite" first.

But think of Microsoft's problem with "DLL Hell". The bits the customer was running were different than the bits that were tested. The production software was too "dynamic".

13:25  
Blogger Pascal Costanza said...

Big Rick,

You have now stated two times that you don't have real experience with the kinds of systems under discussion. Maybe it's worthwhile to change that, if you are interested and find the time to do so.

Keep in mind that the example I have given in my blog post is just for illustration. Just like programs that print "Hello, World!", or compute Fibonacci numbers recursively, are examples that nobody actually really wants to run, this example also just shows off a feature of a programming language that can be a building block for more relevant applications, but not necessarily exactly in the way I have shown. I actually already hinted at that in my blog post.

Consider this a starting point for people who are interested to dive deeper and get a more complete view of what is possible, and what the real advantages and disadvantages are. If this doesn't work for you, then I apologize. If this doesn't work for anybody, then I guess I failed, but I'm optimistic that this isn't the case.

13:54  

Post a Comment

Links to this post:

Create a Link

<< Home