Seeking thoughtful criticisms of functional programming and languages

I'm having zero luck with my Google and CiteSeerX searches.

I'm not looking for some particular language advocate's uninformed criticisms. Instead I'm looking for some at least potentially well grounded critiques based on, just for instance: implementation state of the art, performance or memory footprint, issues regarding separate compilation and link-times and so on for large, multi-developer programs, problem domain applicability, OS integration mismatch, feature/library immaturity of language designs to date, library interoperability difficulties, pedagogical issues (especially training of working programmers), debugging or other programming pragmatics issues, language implementation difficulty related issues, critique of one or more particular features commonly found in (some) functional languages and so forth, awkwardness of implementing/handling certain data structures or very common programming idioms/sub-problems, blah blah blah.

Just some ideas. I can't find much of anything so far.

Quality experience reports along the same lines would also be very welcome, perhaps even more interesting than critiques on more general principles and judgments.

Mucho thanks!

Scott

Comment viewing options

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

Confusing Referential Transparency vs. Purity

Referentially transparent code is, by definition, not really doing anything; it's an inert definition, determining the actual value of which is an implementation detail.

I suspect you confuse purity (mathematical function mapping local inputs to local outputs) with referential transparency (semantics of expression independent of referential context).

You can have impure RT code that 'does something' or 'observes something' - and this isn't so unusual in various logic programming, concurrent constraint programming, declarative concurrency, and some reactive programming models. In these cases, removal of the expression can affect the behavior of the whole system, even if there is no observable 'value' associated with the particular expression.

You can also have pure code that isn't RT, as when using dynamic scoping for pure variables.

Definition?

I suspect you confuse purity (mathematical function mapping local inputs to local outputs) with referential transparency (semantics of expression independent of referential context).

Hm, my working definition is "referential transparency" meaning something like "expressions are indistinguishable from the value they reduce to". If a function application is indistinguishable from what it evaluates to, that's a pretty strong constraint on being able to "do" anything.

If I'm incorrect, I'd appreciate a reference to a proper definition. I fully admit that I've picked up a lot of these ideas in an informal, ad-hoc manner, and may be confused...

Referential Transparency Def

A few papers: [1] [2]

RT has a really muddy meaning, going back to when Quine originally defined it for natural language in 1960. Strachey applied it to programming languages in 1967. RT is is about whether a given expression (reference, symbol) will have the same semantics (referent, effect) in a 'slightly' different context - such as after moving the symbol relative to another, or copying the expression. There is no absolute definition for RT because there is no clear definition for 'same referent', nor an absolute definition for 'slightly different context'.

Rather, you might measure RT in degrees, in terms of which transforms you can perform without destroying reference relationships of expressions. Can you move the expression in space (commute it within a larger expression? code distribution? location transparency?) Can you move it in time (as for laziness and concurrency)? Can you replicate the expression without changing its semantics? Can you lift a duplicate to a single call in a 'let' expression?

The "expressions indistinguishable from the values to which they reduce" definition is pretty much purity with no hidden parameters, or purity + RT. It can be weakened greatly and still retain useful properties:

If you allow dynamic scoping and hidden arguments, and monadic syntactic sugar, you can still have 'purity' in the sense of no effects other than a mapping from inputs to outputs. You can replace the expression with the value to which it reduces. However, because of the hidden inputs, you cannot move or commute the expression with the same freedom as you may move or commute the resulting value. You can distinguish the expression from the resulting value by placing each into various alternative contexts, and noting that the expression's value varies based upon hidden inputs, whereas the normal-form value does not change. Thus, purity does not imply RT. The advantages of purity remain, though, such as support for laziness, partial-evaluation, parallelism, determinism.

Similarly, you can allow effects other than reduction to a value, support hidden outputs and hidden inputs, and still maintain RT. This is made clear when working with various designs for declarative concurrency, including use of future values and promises (as in Oz or Alice), concurrent constraint programming, and even reactive programming where one can 'reduce expressions to behaviors' rather than to values. In many of these cases, despite having hidden outputs and inputs, one is entirely free to commute expressions, to replicate them in space or eliminate replicas (repeating a constraint has no additional effect), even to move them in time by introducing concurrency (even to the extent of making every expression concurrent). All the relevant aspects of RT are maintained, despite the fact that one cannot reduce the expression to a value.

Imperative expressions are neither pure (cannot be reduced to a value) nor RT (cannot be moved or replicated in space or time). They are all tied together by weaving the hidden outputs of one expression (time, state) as hidden inputs to another. Declarative expressions can do the same, as when weaving monadically, but generally do so to a lesser degree and offer greater control. If you compare pure RT functional code only against imperative effects, you're trapping yourself with a false dichotomy and you're missing out on many rich classes of effects that lie between.

That isn't really the focus of functional languages

Pure functional languages generally focus more on expressing what a program does rather than the how it is going to do it. How is an implementation detail that you leave to the compiler which handles the messy part of mapping what you want to do onto the stateful machine that is going to execute it.

Your debug information isn't really part of what the program does, it is really concerned with how it is done so there is a bit of a mismatch. I think it would be very awkward to do what you asked in a pure functional language.

That isn't really the focus of functional languages

Pure functional languages generally focus more on expressing what a program does rather than the how it is going to do it. How is an implementation detail that you leave to the compiler which handles the messy part of mapping what you want to do onto the stateful machine that is going to execute it.

Your debug information isn't really part of what the program does, it is really concerned with how it is done so there is a bit of a mismatch. I think it would be very awkward to do what you asked in a pure functional language.

How rather than What

I disagree. Only the goal-based disciplines that leave search up to the runtime (such as constraint programming, logic programming, certain uses of modeling languages) can really be said to focus on 'what rather than how'.

Rather, functional programs express a particular strategy or algorithm for performing a calculation. The same darn function can be expressed many different ways, and functional programming forces you to pick one of them: the How, rather than the What.

For a pure language the compiler has considerably more freedom to reorder this algorithm, perform partial-evaluation, lift expressions to avoid a multiple-evaluation, etc. than it would for an imperative expression of the same program (especially in presence of modularity). Similarly, the programmer has a great deal more freedom to do the same, and to introduce arbitrary abstractions, without fear of changing program semantics. These are not inconsiderable advantages.

But, despite being relatively declarative, expressions within a functional programming language are still quite mechanical.

Even constraint and logic

Even constraint and logic programming usually have a computation model in mind. If you really want to focus on the 'what' rather than the 'how', you produce a specification as a proposition to satisfied by a solution, and leave the implementation with an intractable search problem.

Constraint programming can

Constraint programming can do that for you very easily. Constraint satisfaction problems are among the easiest to express, and at least a few constraint programming implementations support structured data well enough that one could express constraint satisfaction problems that request source-code inputs (or at least ASTs) that will meet certain invariant properties under a given evaluation function. :-)

And you are correct that "logic programming" as commonly implemented (horn clauses and such) would not be able to do so. I was thinking of the more general paradigm: logic programs are reversible. One can provide the output and ask to browse the inputs that lead to it. This can also give you a nice, intractable search problem, and can also be used to produce source code as the input to produce outputs meeting certain invariants.

The application of using such (in-the-large intractable) searches to produce code from a specification has been called 'declarative metaprogramming' in the literature. One can also apply these during runtime, balancing the nigh-intractable nature of the searches against the specialization advantages that come from extra information available at runtime.

perhaps I was unclear since you misunderstood

The comment system seems to have messed up, that was actually a reply to an earlier comment so it isn't clear if you realized that or not.

Functional languages, relative to imperative languages, generally have a much more narrow focus on defining what the result of the computation will be while specifying fewer details (how parameters are passed, how memory is managed etc.) that commonly need to be handled in lots of imperative languages. By "what" I mean the core computation that is the raison d'être of the program and by "how" I mean all the details about how the core computation is mapped onto the machine (or machines) that are going to execute it.

The distinction you make between what and how seems to be of little practical interest.

Programming at higher

Programming at higher abstraction is achieved by use of abstraction primitives (such as variables, functions, macros, dynamic scope, implicit typing, templates/generics) and domain-specific libraries.

Level of abstraction (distance between developer and hardware) seems much closer to what you mean with regards to describing computations with few low-level details. It is perfectly feasible to program imperative languages in a very high level - if your abstraction facilities are adequate to the task (and don't leave you fretting about performance).

I'm certain you could point to several imperative languages whose abstraction facilities are inadequate, but anyone else could point in turn to Forth and Lisp.

Where the imperative model fails me has much more to do with problems of the model itself: poor handling of domain concurrency, poor local reasoning about or recovery from partial failures, reduced flexibility for abstractions (since you must often maintain order of expressions), reasoning about anything at all in the face of modularity, performance in distributed systems vs. complexities of explicit caching, inefficient expression for reactive flows (operating based on changing state of environment), poor support for separation of concerns: 'thinking' from 'acting' (or calculation from IO), and so on.

Of course, variations on the model fix some problems. For example, object capability model (memory safety + eradicate global mutable state) greatly simplifies reasoning about programs in the face of modularity. And process calculi of various sorts can help reason about concurrency.

Don't have the time to write THAT much :-)

There's several cross-connected issue which are just about equally lethal.

I’ll start with the lack of a stable, universally accepted syntax, caused in part by the fact that functional languages are by and large based on rather simple actual parsers which allows way too many people to go releasing "one more language". Another part of the problem is that functional languages still get designed by and for people doing research an publishing papers and part of their thinking is that you don't really need anything better since you are already "in the field".

Historical reminder - the explosion of software productivity happened when C came to fore and languages that didn't respected core C syntax vanished. Well "functional languages" don't even have anything close to core common syntax and it won't have it as long as you keep having groupie reactions instead os a rational debate. Just check how many people replied with "ML rulez" equivalent - to a request for criticism :-)

So a related issue is egos galore. Doesn't matter if you can lern Haskell in 2 days on a web and can't learn ML in 2 months with a book if you have a lot of people perpetuating ML-something either because that's what they learned in college when enduring pain was easier. Reminds me of the times when academic world was pushing Pascal, then Modula, then ...

Lack of remotely good IDE and general debugability and code discoverability for development process. Part of this is the lack of professional tools, part is the notion that it's somehow not needed and part is the lack of understanding how to actually present as code that's like 100 levels deep in mutually recursive execution "stack" which is actually a tree. Part of this is design issue that has never been thoroughly thought through.

Too high dependency on "just trust my engine as a black box". For any serious programming a person doing it has to know at least one layer bellow in order to be effective. So people who are doing functional languages need to get in touch with reality a bit and give up on that utopian "you are supposed to just express what the program should do". First because it's flawed at the core (impossible in principle), second because it's self-defeating (turns everyone into "spec writers").

Refusal to be practical and offer tangible, practical service. Part of it is ego trips again, but not a small part is historical inertia and outdated thinking.

Example (imagined of course :-)Imagine a C# variant with full power templates which will by the nature of the beast form a small functional language - not designed to take over the universe but to provide tangible service of modifying complier without losing perf. Imagine such a language also getting a control of whether to use gc (we are talking deep interference not) or some other memory and resource management tactics. Imagine such a language also being able to provide moderate context sensitive parsing and operator modification (not just precedence but composition and optimization as well). I'm sure a few more aspects and opportunities would surface up. Say, finally a new, modern lexer/parser generator pushing both performance and flexibility edge.

Without any loss of performance of course (compiler fully aware of the template-functional language and it's transformational semantics), without excuse to "be functional just because" and without needing half a year collge course to learn it.

That would provide a lot of concrete and immediately useful objectives, much more seamless integration with existing language and a venue where a functional language is indeed needed and would rock. Now list all the things that would need to change in the way that academic world at large thinks, works, applies for grants and values it's own results and you'll have all other criticisms of functional languages that I forgot or didn't have the time to write about.

No one needs another "category type algebra with my own slightly different functional language doing nothing" or "see mom I can write web server in this language as well". Real world needs real, practical integration - code still needs to land as a machine code eventually and you still have to be able to follow it in a debugger and if it's doing transofrmations for a compiler then you need to be able to debug in the middle fo the pattern matching engine as well.

Without a shift towards a practical focus it's always going to remain terribly fragmented fringe field where people will at best learn one language for fun and curiosity and at worst will remain stuck with whatever their collge professor liked.

Who hurt you?

Who hurt you, Johnny? Was it a functional programmer?

Oh, I dunno...

Zarko could be a sockpuppet for Erik Meijer... ;)

Hmm,

Never had a chance to meet Erik, maybe when he snaps out of the VB phase (VB being one of few languages I'll never touch).

In case you guys haven't noticed the subject asks for criticism and criticism is not an easy reading for a devotee.

If the guy who asked for it actually has some use for such info (like that he's starting or thinking about a project) then spelling out what you don't like may help him. Don't take it personally - imagine if GM was asking, and listening, what people didn't like about their cars ...

No hard feelings, I hope

I was only kidding. And of course, you make some good points, particularly regarding the availability of industrial-strength tools and focus on debuggers and other peripheral programming support.

a while ago, I wrote a brief on Scheme vs. Common Lisp.

As I figured, somebody copied it onto the web. It can be found at:

http://community.schemewiki.org/?scheme-vs-common-lisp

At this point it's a bit dated; the Scheme it refers to is R4RS or the very early days of R5RS before widespread implementation.

Isn't the memory footprint impractical ?

http://www.haskell.org/haskellwiki/GHC/Memory_Management

"Haskell computations produce a lot of memory garbage - much more than conventional imperative languages. It's because data are immutable so the only way to store every next operation's result is to create new values. In particular, every iteration of a recursive computation creates a new value. ... "

That alone sort of does it in terms of practicality on real computers. I would be interested to hear other opinions.