Where Python falls down

Paul Prescod recently published an article entitled Why I Promote Python. He deals the obvious telling blows against C, C++, and Perl. For myself, though, I prefer Java, Haskell, or (of course) Nickle to Python for writing most programs these days…

The main reason I prefer these languages is that they are safer than Python. Most importantly, they check the basic sanity of things typed at them statically rather than waiting until runtime (i.e., too late) to expose defects. The most important static check is strong type checking, but the checks are pretty sophisticated these days, and also catch things like uninitialized variables. With Haskell's type inference, one needn't even give explicit types to get static type safety.

In particular, module interfaces need to be statically typed. Narrow module interfaces are what make large programs possible; static typing provides the most basic kind of enforced contract for modules. One of the difficulties with Object Oriented Programming is that it tends to be difficult to get the module interfaces right—they are much easier to design in the functional/imperative world, where interfaces can be described just by function signatures.

Other features of these languages are also safer. For example, Haskell has a well-defined formal semantics; I can formally verify a Haskell program. The English specifications of the other languages make this difficult at best.

Haskell and Java are mature, stable languages. There's little question what they will look like in 6 months. Even Nickle hasn't changed a lot in the last few years. Python, like Perl, is still continuously evolving. Evolving languages are bad for writing large, safe programs.

Consider Common Lisp as an alternative to Python. In what way does Common Lisp meet Prescod's stated criterion for language goodness less well than Python does? Prescod states that

My primary criteria [sic] for decency—whether in programming languages, markup languages, or graphical systems—is scalability. By scalability, I mean two things: the ability to scale from easy to difficult problems and the ability for beginners and experts to be comfortable.
Perhaps Java is tough for beginners (although I find it no tougher than Python). Otherwise, I think Java, Haskell, and Nickle meet this primary criterion quite well, and so does Common Lisp. Prescod rejects Lisp as "dismissed by programmers". I'm not sure why that's a criterion: surely there's no point in just picking the most popular language to advocate for.

Let me be clear that I think Python is pretty neat. I've seen some good solutions put together with it, and even helped to do so myself. I just think that if you have to pick a silver bullet, there are better choices. Even if C, C++, Perl, and TCL aren't four of them.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

About semantics

Other features of these languages [Java, Haskell and Nickle] are also safer. For example, Haskell has a well-defined formal semantics; I can formally verify a Haskell program. The English specifications of the other languages make this difficult at best.

Well, you can build any number of "well-defined" formal semantics for any programming language, and you can use one or more of them to prove that some analysis on the source code is correct (with respect to the semantics you've used, at least).

The real problem is that the precision of the analysis is usually inversely proportional to the complexity of the semantic. Let's make an example...

Suppose I want to use a control flow analysis in order to know which functions may be called at each program point. I want to check that no function with the wrong signature ever gets called (thus causing a run-time exception). The semantic of the programming language defines how a symbol could be bound to a function, and the analysis uses this information to place constraints on the possible values ("this symbol could be bound to this and this function, but not to that one"). More constraints mean more accurate analysis.

If the semantic of the language allows these bindings to happen dynamically in run-time (e.g. through conditional inclusion of modules and stuff like that), then the analysis will be very approximate ("more than 1 billion functions could be called here, I give up!").

If, on the other hand, if everything is known in advance (e.g. if the bindings are "freezed" when the program is compiled), then the analysis will be able to produce more constraints, and give more accurate results ("warning! this symbol may be bound to functions A and B, but B has an incompatible signature!").

Python is more like the first kind of languages, while Java and Haskell are more like the second (and it's not a coincidence that they're both compiled).

Apart from that, there's also the syntactic suckability of the languages. Java features both very verbose source code and very weak static analysis: why am I forced to put types in front of every function parameter? Why doesn't the compiler infer them, like OCaml and Haskell do? Why should I perform hours of find'n'replace every time a class is renamed?...

When productivity goes down because one has got to type ten lines of code just to iterate over a sequence, the weak static checks performed by the Java compiler become a lot less interesting. And it becomes quite convenient to switch to languages like Python, eventually reserving some of the time saved with the switch for doing more debugging and verification.

static and dynamic together; PyPy and RPython

If the semantic of the language allows these bindings to happen dynamically in run-time (e.g. through conditional inclusion of modules and stuff like that), then the analysis will be very approximate ("more than 1 billion functions could be called here, I give up!").

True; even if you added some form of static typing to Python, you'd still need a dynamic type system for cases where static typing simply can't help you. While Nickle has fewer of those cases, it still has a dynamic type system which is as complete as the static system; if you globally replace every type in a Nickle program with "poly", the program will still run, and any static type errors become dynamic type errors.

However, you should take a look at PyPy and their "RPython"; essentially, they do type inference and static type checking (as a means of specialization for subsequent compilation to efficient native code) on a substantial subset of Python.

Interesting thoughts

I'm not quite sure what you're getting at with your thoughts about semantic complexity and analysis accuracy. I agree that providing more information statically means more opportunities for more accurate analysis, and that this makes Java and Haskell more amenable to accurate analysis. Is this what you were trying to say?

I agree that you can build formal semantics for any programming language; it just is more useful if everyone agrees on the same formal semantics for a given language. Thus, I wish language designers, including me, would try a little harder. Clinger's Scheme denotational semantics is a great example of a job of back-fitting a formal semantics onto an existing language with reasonable effort.

Syntactic "suckability" is very much a matter of taste; I wrote some stuff about Python's and Haskell's use of the offside rule in an earlier draft, then removed it. Note that the reason that Java doesn't statically infer types is that it's really hard; at the time Java was first specified I don't think anyone really knew how to do it with an imperative OO language. Note that its close cousin C# does do static type inference, though, if that's what you're looking for.

"When productivity goes down because one has got to type ten lines of code just to iterate over a sequence…" Speaking just for myself, I rarely find my programming speed limited by the amount of code I have to type. Although I would be happy to hear of some, I know of zero empirical evidence relating language verbosity to productivity, and little relating it to any other software engineering measure. My honest guess is that the relationship is correlative rather than causal: well-designed languages both improve productivity and are not unecessarily verbose. I think both Knuth's Web and Iverson's APL provide cogent qualititative arguments that the extremes aren't too good a place to be. Beyond that, I don't think I'm willing to venture.

Re: Interesting thoughts

I'm not quite sure what you're getting at with your thoughts about semantic complexity and analysis accuracy.

Hmm, you're right, I've been quite unclear in the basic point of my comment.

The basic point is: the semantic of a dynamic, multi-paradigm programming language (like Python, that suppports functional, procedural and object-oriented programming) will be more complicated than the one of a static single-paradigm language (like Haskell/functional or Java/OO). It means that the static analysis will also be generally weaker, and the number of possible run-time errors will increase.

On the other hand, the high number of tools offered by a dynamic multi-paradigm programming language may become practically very interesting, allowing to solve problems with a few lines of code (while keeping readability, I'll add). But am I (as a developer) going to trade some "safeness" with this power? It depends. I wouldn't use Python for developing the security system of a nuclear plant. But IMHO Java can't even compete in this battle, because it doesn't counter-balance the lack of expressivity with an advanced static analysis of the source code.

(As a side note: OCaml is a multi-paradigm language that also features extremely powerful static analysis --- in fact it has been developed to be a reference for teaching static analysis techniques)

I agree that you can build formal semantics for any programming language; it just is more useful if everyone agrees on the same formal semantics for a given language.

Well, different semantics emphasize different aspects of a programming language. Some may stress what the programming language statements do (e.g. denotational semantics), or how they do it (e.g. operational semantics), with different levels of granularity (big-step vs. small-step semantics). More in general, they describe how a program will transform values during execution. Everyone may "agree" on some particular semantics, but it may be needed to develop yet another one in order to emphasize some properties of a program and allow more advanced analysis techniques with higher precision. In fact this is what happens continuously.

Note that the reason that Java doesn't statically infer types is that it's really hard; at the time Java was first specified I don't think anyone really knew how to do it with an imperative OO language.

I agree, but of course it doesn't excuse the Java shortcomings today (see the power vs. safeness choice above).

Speaking just for myself, I rarely find my programming speed limited by the amount of code I have to type.

I'm constantly experiencing the opposite issue.

Being able to pass closures around without having to use anonymous classes saves a *lot* of code and time, expecially when developing the internals of some modules (well, if you start passing closures around the whole application, then you could have a serious design problem).

List comprehension (or even better generator expressions, that avoid wasting memory) in Python allow to process sets of data using simple one-liner functional directives. More importantly, they allow to directly write what you want to do in declarative terms, rather than forcing disassemblation into loops, assignments, etc. It's definitely a high-level vs. low-level languages issue.

I may continue...

Errata

If the semantic of the language allows these bindings to happen dynamically in run-time (e.g. through conditional inclusion of modules and stuff like that), then the analysis will be very approximate ("more than 1 billion functions could be called here, I give up!").

Just a clarification: the main source of uncertainty is not related to bindings being resolved in run-time, but in bindings being made with functions that are unknown in compile-time. That's why I wrote about conditional importing of modules during execution time.

Where programmers fall down

I've met programmers who refused to use dynamic memory allocation because it was "confusing, unpredictable, and susceptible to errors". Simply, it was too "unsafe" from their perspective. They're all mostly extinct now.

There are places where both static and dynamic typing are appropriate, but to simply see the situation as "more safe" and "less safe" hints that you've never really tried to exploit the benefits of a dynamically typed system. People don't use dynamically typed languages just to be wreckless. You're only seeing the upside of static typing, and the downside dynamic typing.

There are trade-offs in type systems. Static typing is a price you pay for efficient compilation, and marginal protection against a specific class of defects. With that, you lose a large amount of flexibility and "genericity". I think this price/benefit ratio is unbalanced. It's not as if you can stop writing tests if you're using a statically-typed language.

You'll notice that many statically typed languages grow features to subvert it. C++ got templates (which was a reasonable feature, considering its goals), and Java was recently affixed with "generics", in addition to the (unsafe, yet often used) casts it's had all along.

As an aside, these generics, in addition to the new for-loops, are much more significant syntax changes than I've seen in either Perl or Python in the last 5 years. Who knows where it'll be 10 years from now?

I don't mean to flame here; I've been reading your blog regularly, and wouldn't imply that you don't "get it", but I'm reminded of times when I've written-off techniques and tools before I attained the underlying the "zen".

(From someone who is normally making the best of being trapped in a statically typed language.)

Static vs dynamic typing, generics, etc...

"To simply see the situation as 'more safe' and 'less safe' hints that you've never really tried to exploit the benefits of a dynamically typed system." I think I've given it the ol' college try—Keith Packard and I designed our first language together, Kalypso, in the mid-80s. It was dynamically typed. Heck, it was dynamically scoped (and yes, that was a bad idea). I'm trying to remember whether there was some good reason why it couldn't easily have had a static type system like Nickle's; if so, I think it would have been the macros, which were really cool, BTW, and I should blog them sometime after I understand them again.

What I've found in 20+ years' dynamic language experience is that I'm willing to pay a lot to have at least some of my programming errors revealed at compile time rather than run time. If anyone besides the developer is going to use the program, run time is too late. It's surely better to have the program throw an exception due to a dynamic type violation than just go on computing wrong answers, but the result is still horrid from the users' point of view: their program stops working. I want my users (including me) to have that experience as rarely as I can manage, which is why Nickle has a mantra of "fail as early as (reasonably) possible."

"Static typing is a price you pay for efficient compilation…" I'm with you—we needn't go there. "…and marginal protection against a specific class of defects." I disagree. First of all, the specific class of defects caught by static typing is one that becomes much more prevalent and dangerous in large (1M LOC or more) programs. This makes it hard to reason about their prevalence and severity by the use of toy examples. Secondly, the protection against this specific class of defects is not marginal; it is typically absolute. As you say, one still must unit test and even system test large programs written in statically typed languages. But without static interface types, the chances of being able to pass a system test are pretty small. As interface contracts, static types are pretty pathetic, but they're the best most of us have in 2006; I'm loathe to give them up.

"You'll notice that many statically typed languages grow features to subvert it." Unfortunately, your examples involving parametric polymorphism illustrate an opposing point: languages' static type systems are adapted to avoid the need to subvert them. The whole point of Java's generics (I've programmed with them quite a bit and they seem pretty nice to me) and C++'s templates is to get rid of the constant casts, and re-enter the world of strong static typing.

You should check out Nickle's static type system. It has some properties you might like. First, it's "polite": it promises never to statically reject a program unless it can prove that the program would fail with a type error at runtime. Second, it has full subtyping, so you can be as unspecific as you want about the static type of an object, all the way to omitting it altogether. The net result of these features is that if you're determined to ignore the type system because you "know" your code is OK, you can. But if you choose to let the compiler and future readers of the code in on why it is OK, it's usually pretty convenient to do that also.

I guess I think part of the problem is an idea of what static typing is derived from Pascal and its notional descendants. I'm a software engineer, and so I'll program in Pascal's type system if it improves the quality of my code even a little. Fortunately, modern programming language tech lets me do a lot better than that. Check out Haskell, Nickle, or at least the latest turns of C# and see how much prettier static typing can be.

Act III

You should check out Nickle's static type system. It has some properties you might like. First, it's "polite": it promises never to statically reject a program unless it can prove that the program would fail with a type error at runtime. Second, it has full subtyping, so you can be as unspecific as you want about the static type of an object, all the way to omitting it altogether.

That is really cool.

I guess I think part of the problem is an idea of what static typing is derived from Pascal and its notional descendants.

Yes, modern languages with decent type inference are much less painful. Boo is a recent example of accomplishing this in a fashion more palatable to cranky people like me.

Yet, the syntax overhead remains only half the issue. I brought up templates and generics earlier because they solve a problem created by static typing. Consider the physical labor and "pre-architecture" required to express "a list of objects which will respond to message :execute" in the two camps, considering that LoC aren't without cost, even once written.

Many people balk at the idea that such a structure would even be desirable, and I think it illustrates the schism between the styles nicely. How suspect one is of this technique largely defines whether they see the value of static typing.

I appreciate the clarification/extension of your perspective; I would've had much less urge to rant having read your comment before it existed.

Regards

Re: "dynamic" typing and dispatch

"Consider the physical labor and 'pre-architecture' required to express 'a list of objects which will respond to message :execute' in the two camps, considering that LoC aren't without cost, even once written." I'll still claim that I don't believe that it's the LOC that cost, within reason, but the underlying semantics. If the barrier to successful programming is "physical labor", we've won. I wish.

In Java, you'd express your constraint using interface types, and I'm sure there's some corresponding mechanism in C++ (MI?). As you correctly note, however, parametric polymorphism doesn't help here, and it's a commonly-needed case.

I think what you want is "disjoint union" types, which Haskell and Nickle have. (Ironically, so does Pascal. C unions were a huge step backward.) Nickle's syntax is admittedly painful, and we would welcome suggestions for changing it. In Haskell, you do something like this:

--- Apply a list of functions to
--- a list of data of corresponding type,
--- returning a new list of data.

data Execute = ExInt (Int -> Int) | ExString (String -> String)

data Arg = ArgInt (Int) | ArgString (String) deriving Show

step :: [ Execute ] -> [ Arg ] -> [ Arg ]
step [] _ = []
step _ [] = []
step ((ExInt f) : exs) ((ArgInt a) : as) =
    (ArgInt (f a)) : (step exs as)
step ((ExString f) : exs) ((ArgString a) : as) =
    (ArgString (f a)) : (step exs as)

egexs = [ ExInt (\x -> x + 1), ExString (\s -> 'a' : s) ]
egdat = [ ArgInt 0, ArgString "" ]

main =
    print egdat''
    where
      egdat' = step egexs egdat
      egdat'' = step egexs egdat'
Things to say about this code:
  • Because Haskell does type inference, the type declaration (lines with ::) is optional, inserted as a mental note as to what's going on and for the compiler to check.
  • I actually introduced a type bug in the example, through cutting and pasting too fast. I wrote
    egdat = [ ArgInt 0, ArgString 1 ]
    
    It was really nice to have the compiler catch this statically for me.
  • I also had another bug in the example. (Great programmer, eh?) I wrote
         egdat'' = step egexs egdat''
    
    which caused an infinite loop. Type systems certainly don't solve everything.
  • Haskell is not an object oriented language, so I'm using first-class functions as a surrogate for methods.
  • This code can exhibit a run-time type error! Consider
    egexs = [ ExInt (\x -> x + 1) ]
    egdat = [ ArgString "" ]
    
    This passes the static type system, but fails at runtime. This is part of why advocates of strong typing claim no loss of "flexibility"; the static type system doesn't tie one down so much as capture some common cases exceedingly well. (If one makes step "exhaustive", then the function becomes safe, but at the price of figuring out what to do in cases like the above. Choices are good.)

"objects which will respond to message :execute"

Consider the physical labor and "pre-architecture" required to express "a list of objects which will respond to message :execute" in the two camps
I think what you want is "disjoint union" types, which Haskell and Nickle have.

Personally, I think the best solution for "objects which will respond to message :execute" is Haskell's typeclasses:

class Executable t where
    execute :: t -> Integer

data StackInst = Inc | Dbl deriving Show
data StackProg = StackProg [StackInst] deriving Show

unprog :: StackProg -> [StackInst]
unprog (StackProg l) = l

instance Executable StackProg where
    execute = (foldl (flip execute_inst) 0) . unprog
        where execute_inst :: StackInst -> Integer -> Integer
              execute_inst Inc = (+1)
              execute_inst Dbl = (*2)

data Expr = Mul Expr Expr | Const Integer deriving Show

instance Executable Expr where
    execute (Mul e1 e2) = (execute e1)*(execute e2)
    execute (Const n)   = n

main :: IO ()
main = do let prog1 = StackProg [Inc, Dbl, Dbl, Inc, Dbl, Dbl, Inc, Dbl]
              prog2 = Mul (Const 6) (Const 7)
          print prog1
          print $ execute prog1
          print prog2
          print $ execute prog2

(The extra StackProg wrapper around a list of StackInst exists solely to avoid using a GHC extension to allow a parameterised type like [StackInst] in an instance declaration; with that extension, you could drop the unprog function and the StackProg wrapper.)

This program produces the result:

[Inc,Dbl,Dbl,Inc,Dbl,Dbl,Inc,Dbl]
42
Mul (Const 6) (Const 7)
42

If you want a "list of objects which can respond to message :execute", then you'll need to use the Haskell extension allowing existential types. With it, you could declare a type which can contain anything in the typeclass "Executable", and then manipulate a list of them.

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor to prevent automated spam submissions.
Image CAPTCHA
Copy the characters (respecting upper/lower case) from the image.
Syndicate content