The causality in a functional language is far from obvious. Consider Haskell, a language that is both purely functional and lazy, and is considered somewhat of a beautiful poster child of the functional approach. Say you write a program and it has a bug—it’s not doing what it’s supposed to. How would you debug it? Some alternatives:
(a) Use a debugger to step through a program until it does something it’s not supposed to (this entails dealing with a causal order of evaluation of statements—something Haskell as a lazy and functional language is explicitly hiding from you until you start a debugger).
(b) Use good ol’ print statements. These will appear in a very strange order because of lazy evaluation. Again, Haskell hides the true order—the true order has nothing to do with the way the code appears on the page. This makes it difficult to build a causal model of what’s going on in your program. A causal model is what you need if you want to use print statements to debug.
(c) Intervene in a program by changing some intermediate runtime value to see what would happen to the output. As a functional language, Haskell does not allow you to change state (ignoring monads which are a very complicated beast, and at any rate would not support a straightforward value change while debugging anyways).
My claim is that causality is so central to how human beings think about complex computer programs that it is not possible to write and debug large programs written in functional style without either building a causal model of your program (something most functional language will fight with you about to the extent that they are functional), or mostly sticking to an imperative “causal” style, and only use simple functional idioms that you know work and that do not require further thinking (like map and reduce, and simple closure use). Note that even Haskell, a language committed to “wearing the hair shirt” (http://research.microsoft.com/en-us/um/people/simonpj/papers/haskell-retrospective/haskellretrospective.pdf) of functional programming, has given concessions to the imperative/causal writing style by providing the “do” shorthand.
Personally, I love functional idioms, and I think functional code with heavy recursion use is often quite beautiful. But I don’t think the functional approach is well suited to writing complex code precisely because it is violating a principal rule of computer science—make things easy for the human at the expense of the machine.
Laziness can muddy the waters, but it’s also optional in functional programming. People using haskell in a practical setting usually avoid it and are coming up with new language extensions to make strict evaluation the default (like in records for example).
What you’re really saying is the causal link between assembly and the language is less obvious, which is certainly true as it is a very high level language. However, if we’re talking about the causality of the language itself, then functional languages enforce a more transparent causal structure of the code itself.
You can be certain that a function that isn’t tainted by IO in haskell, for example, isn’t going to involve dozens of different causal structures. An imperative function like AnimalFactory.create(“dog”) could involve dozens of different dependencies (e.g. through singletons or dependency injection) making the dependency graph (and causal structure) obfuscated. This lack of transparent guarantees about state and dependencies in imperative languages makes concurrent/parallelprogramming (and even plain code) very difficult to reason about and test.
Moreover, the concessions that haskell has given way to are probably temporary. Haskell is a research language and functional solutions to problems like IO and event driven programs have been put forward but are not yet widely accepted. And even ignoring these solutions, you still have a basic paradigm where you have top level imperative style code with everything else being functional.
And while it can be more difficult to debug functional programs, they’re easier to test, and they’re less prone to runtime bugs. And really, the debugging problem is one of laziness and difficult to use debuggers. Debugging F# with visual studio’s debugger isn’t that difficult.
(Note: that when I talk about functional programming, I’m talking about a paradigm that avoids mutable state and data rather than idiomatic approaches to container manipulation)
The causality in a functional language is far from obvious. Consider Haskell, a language that is both purely functional and lazy, and is considered somewhat of a beautiful poster child of the functional approach. Say you write a program and it has a bug—it’s not doing what it’s supposed to. How would you debug it? Some alternatives:
(a) Use a debugger to step through a program until it does something it’s not supposed to (this entails dealing with a causal order of evaluation of statements—something Haskell as a lazy and functional language is explicitly hiding from you until you start a debugger).
(b) Use good ol’ print statements. These will appear in a very strange order because of lazy evaluation. Again, Haskell hides the true order—the true order has nothing to do with the way the code appears on the page. This makes it difficult to build a causal model of what’s going on in your program. A causal model is what you need if you want to use print statements to debug.
(c) Intervene in a program by changing some intermediate runtime value to see what would happen to the output. As a functional language, Haskell does not allow you to change state (ignoring monads which are a very complicated beast, and at any rate would not support a straightforward value change while debugging anyways).
My claim is that causality is so central to how human beings think about complex computer programs that it is not possible to write and debug large programs written in functional style without either building a causal model of your program (something most functional language will fight with you about to the extent that they are functional), or mostly sticking to an imperative “causal” style, and only use simple functional idioms that you know work and that do not require further thinking (like map and reduce, and simple closure use). Note that even Haskell, a language committed to “wearing the hair shirt” (http://research.microsoft.com/en-us/um/people/simonpj/papers/haskell-retrospective/haskellretrospective.pdf) of functional programming, has given concessions to the imperative/causal writing style by providing the “do” shorthand.
Personally, I love functional idioms, and I think functional code with heavy recursion use is often quite beautiful. But I don’t think the functional approach is well suited to writing complex code precisely because it is violating a principal rule of computer science—make things easy for the human at the expense of the machine.
Laziness can muddy the waters, but it’s also optional in functional programming. People using haskell in a practical setting usually avoid it and are coming up with new language extensions to make strict evaluation the default (like in records for example).
What you’re really saying is the causal link between assembly and the language is less obvious, which is certainly true as it is a very high level language. However, if we’re talking about the causality of the language itself, then functional languages enforce a more transparent causal structure of the code itself.
You can be certain that a function that isn’t tainted by IO in haskell, for example, isn’t going to involve dozens of different causal structures. An imperative function like AnimalFactory.create(“dog”) could involve dozens of different dependencies (e.g. through singletons or dependency injection) making the dependency graph (and causal structure) obfuscated. This lack of transparent guarantees about state and dependencies in imperative languages makes concurrent/parallelprogramming (and even plain code) very difficult to reason about and test.
Moreover, the concessions that haskell has given way to are probably temporary. Haskell is a research language and functional solutions to problems like IO and event driven programs have been put forward but are not yet widely accepted. And even ignoring these solutions, you still have a basic paradigm where you have top level imperative style code with everything else being functional.
And while it can be more difficult to debug functional programs, they’re easier to test, and they’re less prone to runtime bugs. And really, the debugging problem is one of laziness and difficult to use debuggers. Debugging F# with visual studio’s debugger isn’t that difficult.
(Note: that when I talk about functional programming, I’m talking about a paradigm that avoids mutable state and data rather than idiomatic approaches to container manipulation)