Python is soup-in-a-cup

24Jan07

Introduction: we’ve had smarter language wars than this

Judging from the blogs — at least from Reddit, which is where I get my daily dose of blog — functional programming is frankly on the rise. Part of it probably is due to OOP’s failure to materalize its promise of easy-to-maintain, flexible, reusable code. Part of it is also the rise of FP-centered languages around relatively recent developments in theoretical computer science — like the MLs and Haskell, a little further away from academia, Erlang. There’s finally the visible hand of Moore’s Law making high-level languages ever more practical.

Also judging from the blogs there’s a distinct zeitgeist of rejecting “static” languages in favor of “dynamic” ones. This is partly an artifact of fast computers finally making serious work in dynamic languages feasible, but also a rejection of the Big Architecture discipline of the C ++/Java weltanschauung.

Being as easy as it is to make noise with a blog, this has led to a lot of uninformed ranting. Even Steve Yegge, who’s supposed to be a smart programmer with a track record that shames mine, has done his share of pointless rambling on how “static” languages compile to ‘hardware’, to ‘Pinocchio’-like wooden boys that aren’t “real”, while dynamic languages yield “living” software somehow.

This, of course, makes absolutely no sense.

The Either/Or approach to classifying computer languages

First of all, this dichotomy mixes up type-checking strategies with evaluation models. Dynamically-typed languages check types at runtime, while statically-typed languages do some form of type-checking that ensures no type error will arise once a program manages to compile. On the other hand, statically-compiled languages yield an executable that does what it needs to do, while dynamic languages are either interpreted — that is, require a separate runtime environment that parses source in text form — or allow some form of pseudocompiling that mangles source into some form of object code and bundle a runtime environment.

This distinction deserves repetition.

type-checking
at compile-time at run-time
compilation to a ‘native’ executable “static” languages (C, C++, Haskell) ???*
to object code for some runtime “VM” static languages (Java) “Dynamic” languages

(* To the best of my knowledge, that can’t be done. How would you do runtime type checking without a runtime environment?)

Evidently, there’s some empirical correlation here — enough to cause massive conceptual confusion in Steve Yegge’s brain. The table above nevertheless represents the standard wisdom about conceptual dimensions to be emphasized; this is supposed to be a technical choice based on pros and cons of each approach:

static typing dynamic typing
static compilation PRO: computationally most efficient; amenable to engineering at all levels. Safe safe safe!

CON: major loss of flexibility. Slower development time. Broken type systems in most languages have no concept of inference — and worse yet, often allow type casts.

PRO: designing one is likely to get you big bucks, a McArthur Genius Award or even beatification by the Roman Catholic church.

CON: Probably doesn’t exist.

Interpreted/bundled runtime PRO: portability without sacrificing safety.

CON: Interpreted languages are generally inefficient; bundled runtimes are generally humongous; ultimately no control of what the virtual machine/interpreter at the user’s side might choose to do.

PRO: Faster development process. In noncrippled languages of the type, metaprogramming. Maximum flexiblity! Agility! Ooh, aah!

CON: Computationally inefficient. Hard to catch bugs before they happen in the wild. Architecture is a matter of faith. Haskell hackers mock you.

Conventional wisdom holds that once you choose what to do about compilation and type-checking strategies, you’ve basically bound yourself to the above pros and cons. Fortunately, the conventional wisdom is essentially wrong.

Is type-checking really this much of an issue?

Why do computers bother to have the concept of a type? At some point, machines mangle around bytes, but even in the lowest-level user languages, some facilities to deal with numbers, strings, etc. become desirable — to the point there are even typed assembly languages.

Given that, there’s no algebraically correct method for summing a number and a string. None. The best you can do is to arbitrarily overload the “+” operator to become “concatenate . toString” so "Oh bitty box!" + 2 becomes "Oh bitty box!2", but that won’t map so easily to the rest of the ordinary operations over numbers. Worse yet, numbers no longer are a ring, nor an integrity domain, nor an abelian group; mathematicians, who know this stuff better than us, have decided we don’t want to live in a world where ordinary numbers are not an integrity domain.

What I’m trying to tell you here is that computers choke at machine type mismatches, and that’s a fact of life. All the available dynamic languages won’t help you the least when you try to divide a floating point number by a string. To be feasible, a computation must first make sense. When you see things from this point of view, does it make sense not to try and detect all possible machine type clashes at compile-time so nothing sad happens at compile-time? It does if the type model your language offers you is inconvenient enough.

First of all, digital computers being discrete, finite machines, they’re not good at handling numbers in the sense we’ve accustomed ourselves to deal with in real analysis at all. At the machine level, computers can’t handle the real continuum, or abitrarily large numbers as a type because they’re supposed to enclose values in fixed-sized boxes so memory can be managed. That is, computers don’t deal with “numbers” in the flexible sense we use — they deal with variations of floating point numbers and bounded integers.

When computer languages reflect this fact — and it’s often useful that they do; declaring all numbers to be the largest, most precise floating-point type would lead to imprecisions and waste memory space — you often find out you can’t add two numbers! int a = 2 can’t be summed with float b=2.0. That has to be a drag, which makes run-time type checking more attractive: just let code compile, and let’s decide at runtime what machine type this “2” will correspond to. This, in guidospeak, is “duck typing”: if it walks like a duck and quacks like a duck, then it’s a duck. 2.0 + 2 will still fail to run, though — implicit type casts are evil.

Second of all, in the type model available to most languages, you have to explicitly declare every type. This is very, very boring. This is the stuff that makes for entire lives spent in quiet desperation.

Does static typing have to be like this? Not at all; none of these problems relate directly to the fact that machine types are checked at compile-time.

The first problem has to do with the fact that most languages have no concept of “type classes” built around common operations. In a language with a good type model, I can just say


double x = 2*x

The type of this will be (Num a)=>a->a, which means this function is correct for every type that implements the operations of Num.

The second problem has to do with compilers for static languages being bone-headed (or actually, the typing models not being smart enough to allow for smart behaviour). We’ve stated the type of double above; does anyone really think that applies to a string? Why can’t compilers know this? Well, this is 1958 1969 1978 1985 1987 1998 2006 already: they can!

Given this, the only advantage of dynamic typing is that it lets you compile massively broken code, only to see it break when people attempt to run it. I can only attribute the fact that people prefer to catch bugs at runtime rather than at compile-time to the fact that they’ve learned that static typing means boring, repetitive, soul-crushing grunt-work. Except there has been a mathematical theory of type-inference since the late 50s, it’s been perfected for decades to the point of a great piece of art — and most programmers don’t know about this! Instead, they rely on unit tests like an engineer who, for lack of knowing that differential calculus does half of his physics for him, runs extensive simulations for months on end before trusting something.

How many roads must a program construction calculus walk down before you call it a language?

Python’s signature war cry is “batteries included”. That’s supposed to mean that it includes many nice libraries with common operations for everything from scientific computing to web development. Python goes further than that, though: Python — as many other like-minded dynamic languages — is soup-in-a-cup.

The word “Python” refers to (a) a programming language standardized in Neuron #385978 inside Guido van Rossum’s brain — that is, a formal system one can specify computations in — (b) a system for manipulating programs in Python — which makes it a dynamic language in the sense of not being statically compiled (c) an object system over Python and (d) an interpreter.

You can’t have some of it. Your soup-in-a-cup comes with carrots, deal with it. You can’t decide you don’t need the benefits of metaprogramming and would rather have a faster program. You can’t decide to use an alternative interpreter, because no one really knows what Python is supposed to work like except the core development group. You can’t even decide to compile it because metaprogramming is bolted-on: every compiled program written in Python would have to include an interpreter because you might want to use “eval” at some point.

In comparison, a Haskell environment of comparable power is a tower of separable pieces. First, you have the core language — not defined in a fanboy mailing list, but in a congress of top academics evaluating the contributions made to the language by recent research. Second, there are Haskell interpreters and compilers around that formal specification — possibly including extensions that are enable if you politely ask it to use -fglasgow-exts or something equivalent to that. Third, if you want metaprogramming — if you want to write programs that manipulate Haskell programs — you have Template Haskell, which basically converts between standard, meant-for-humans Haskell syntax and an Abstract Syntax Tree, not too different from what’s available in the Lisp world, which you can manipulate at will to spit out Haskell source. Fourth, if you want that generated code to be dynamically-loaded — if you want “eval” — you have hs-plugins, which is roughly equivalent to the “bundle an entire interpreter with the program” strategy of many Lisp and Scheme pseudocompilers.

And if you really want to defer type-checking to runtime, you can always use Data.Dynamic. As I’ve said before, you can go all the way from defining a type model that matches the algebraic semantics in your head — using the deep mathematics of type inference to do part of the grunt work for you — to relying on products and sums from machine types and using type checking just so there isn’t a mortal blow at runtime when the computer tries to divide a date by a string.

Haskell gives you choices: you can prepare the strict meal, with a ritual involving several plates served in order, and you can prefare the free-wheeling buffet, with a bunch of yummy edible stuff laid on a table on the corner of your party.

You can even serve them soup-in-a-cup. It’s very convenient, I know.

About these ads


13 Responses to “Python is soup-in-a-cup”

  1. 1 Chris Dutchyn

    I’ll fill in the upper-right corner for you: Scheme.

    It is type-safe (in most implementations), does latent typing without annotations. Look at Chez Scheme (the native compiler) as the prototypical example.

  2. 2 Ian

    The Dynamic type puts Haskell in the upper right corner, too.

  3. Also, Common Lisp systems tend to compile to native machine code. They usually keep the run-time environment around for ease of debugging, but the commercial systems are said to employ “tree shakers” that can bring the executable size down to something more manageable by removing many of the “environment” parts. There’s no more “you can’t know what the VM will do” about development in mature static languages than there are about “you can’t know what libc will do” about development in C (or Haskell). Please take a look at some of the mature dynamic languages (Smalltalk, Common Lisp, Scheme), you might find that they are not always inferior to static languages.

  4. Well, yes, by virtue of being semi-static.

    I’m really kind of playing “devil’s advocate” here. The virtues of dynamicity have been overstated lately; my entire point is that this is a fuzzy line — a spectrum, even.

  5. How do you define semi-static? By not permitting total redefinition of the program at run-time? Well, of course not! With a proper tree shaker, you’ll only have the parts of the runtime actually needed for your program, which is as good as it gets. You can’t have functionality without having code that implements it. If you don’t want your application to ship with a compiler in-core, then don’t write an application that requires run-time compilation (metaprogramming does not necessarily depend on this). The only difference from static languages (on this point) is that in most static languages, you won’t even have a choice – at runtime, everything will just be an opaque block of bytes.

    But yes, of course it’s somewhat fuzzy. High-quality Common Lisp compilers (and probably also Scheme) do type inference at compile-time to generate the fastest possible code, and also point out cases where it is sure that a type error will happen (such as passing a string to the numerical addition operator). This is static analysis, but it doesn’t make the type system any less dynamic. Using the adjective “static” in the description of some language or implementation facility does not automatically apply to all other facilities as well – static analysis does not prevent dynamic typing, just like you wouldn’t claim static scoping prevents dynamic typing.

    Basically, dynamic typing is just a trait of the type system, what is actually executed at run-time is an irrelevant implementation detail, as long as the proper type errors are signalled (and the generated code is otherwise correct, of course). You cannot make the type system of a language less dynamic just by writing an optimizing compiler.

  6. Now don’t get me wrong; I agree that Haskell is massively spiffy and that it has a deeply useful type system. There’s plenty of Haskell code out that I couldn’t begin to imagine writing in Python or Ruby.

    But a couple of comments:

    1) Not all metaprogramming systems are equivalent. Lisp macros, for example, give you massive syntactic flexibility, but they’re a pain in the neck if you want to implement continuations. Monads, on other hand, make continuations easy. Different metaprogramming systems have different “flavors”, and some work better for certain tasks.

    SmallTalk and Ruby permit a very subtle style of metaprogramming based on metaclass hackery. Because this style involves mutating classes at runtime, there’s no way to make it type safe. But lots of people have mastered it (Rails, for example, is about 20 different DSLs), and judging from the quality of the average Ruby DSL, it appears to be much easier to use well than Lisp macros.

    So just because you find Haskell’s “sexy types” to be glorious, doesn’t mean should you go around harshing on the dynamic language folks. :-) They’ve produced some programs of genuine beauty, and some of their most elegant techniques depend heavily on dynamism.

    2) The state of the art in optimizing SmallTalk-like languages is Self, which reached 1/4 to 1/2 the speed of C. This is about the same as GHC. So there’s no reason why Python and Ruby need to be slow.

  7. 7 D. Johnston

    > First, you have the core language — not defined in a fanboy mailing list, but in a congress of top academics evaluating the contributions made to the language by recent research.
    Whoever said academics were good at making programming languages? ;-)
    Just an observation.
    Also, don’t knock c.l.p. We’ve got smarties too!

  8. 8 occams_razor

    It’s not an “integrity domain”, it’s an “integral domain”. Also, an integral domain, or “domain” for short, is simply a ring in which you can cancel terms but not necessarily divide. (i.e. ab = ac implies b = c but 1/a doesn’t have to exist)

    Your usage also suggests that you were reaching for the term “algebraically closed” instead of the list of things you gave.

  9. Point taken by this Python coder, not to be confused with “fanboy”.

    Re: Yegge, it sounds like you’re judging him just by the Pinocchio essay, which to put it mildly is not his best work. I half expect him to follow it up with a retraction admitting that he’s not ready to express whatever that idea is yet.

  10. Not in defence of Steve Yegge, but at the beginning you complain about Steve’s “pointless ramblings”, but in your first paragraph of the section titled “The Either/Or approach to classifying computer languages”, you are far from rigorous in your language. You use phrases like “yield an executable that does what it needs to do”; and “pseudocompiling that mangles source into some form of object code”.

    Maybe you should be lenient?

    – Paddy.

  11. 11 Larry Hastings

    C++ is not as statically typed as you seem to think it is; indeed, there is an entire language facility called “Run-Time Type Information”, or RTTI. So I guess we can all look forward to the beatification of Stroustrup.

    http://en.wikipedia.org/wiki/RTTI

    Also, I could name three alternative interpreters for Python: Iron Python, Jython, and PyPy. All of them are reimplementations from the ground up in another language (C#, Java, and Python, respectively).

    Finally, needing-to-include-a-Python-interpreter does not prevent you from compiling Python. Supporting “eval” would place many inconvenient constraints on compiling Python, but it does not prohibit it outright.

    Back to my delicious soup-in-a-cup,

    /larry/

  12. As someone who’s currently returning to static languages after several years’ sojourn in the dynamic world, possibly I can offer some insight.

    The dynamic world’s view on typing was most forcefully expressed by Stuart Halloway when he said “In five years, we will view compilation as a really weak form of unit test.” Type errors, in their experience, are usually trivial things; serious bugs almost never manifest themselves as type errors (*) [My experience mirrors this, I must say]. Secondly, type errors are picked up by the kind of thorough unit testing that you have to do anyway to find the real bugs. Thirdly, they find that testing, and designing for testability, lead to better, clearer, more reliable and maintainable software. Fourthly, tests are a useful form of documentation. So, if testing gives you all these benefits, and you have to do it anyway, why submit yourself to the pain of static typing, and deny yourself the simplicity and advanced techniques that dynamic typing allows? Type inference and polymorphism greatly alleviate this pain, sure, but they don’t eliminate it entirely.

    For a nice, simple example of the kind of technique we’re talking about, have a look at the case study “Metaclass hacking in Fetchmailconf in The Art of Unix Programming. Or the Python serialization library Pickle. Or CGI.pm, which (IIRC) generates all of the basic tag-insertion functions [b(), i(), blockquote() etc] with a simple loop over acceptable keywords that inserts a closure into the symbol table. Or any of the things already listed :-)

    Fans of static typing will no doubt say that types also yield the benefit of clearer, better documented code, and a more expressive type system allows you to express more constraints on your code in the form of types (thus increasing the strength of the unit test implied by compilation). This, by the way, is the answer to those who objected to (*) – your code may not display this behaviour, because you deliberately wrote it not to. But that took work. As much work as writing the tests?

    Now, which group is right, and which technique leads to faster development of maintainable code, seems to me to be a genuinely open (and interesting!) question. Which is why I’m relearning Haskell :-)

  13. A refereshing change to find a website with useful information


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: