30

On page 839 of the second edition, Steve McConnell is discussing all the ways that programmers can "conquer complexity" in big programs. His tips culminate with this statement:

"Object-oriented programming provides a level of abstraction that applies to algorithms and data at the same time, a kind of abstraction that functional decomposition alone didn't provide."

Coupled with his conclusion that "reducing complexity is arguably the most important key to being an effective programmer" (same page), this seems to pretty much a challenge to functional programming.

The debate between FP and OO is often framed by FP proponents around the issues of complexity that derives specifically from the challenges of concurrency or parallelization. But concurrency is certainly not the only kind of complexity software programmers need to conquer. Perhaps focusing on reducing one sort of complexity increases it greatly in other dimensions, such that for many cases, the gain is not worth the cost.

If we shifted the terms of the comparison between FP and OO from particular issues like concurrency or reusability to the management of global complexity, how would that debate look?

EDIT

The contrast I wanted to highlight is that OO seems to encapsulate and abstract away from the complexity of both data and algorithms, whereas functional programming seems encourage leaving the implementation details of data structures more "exposed" throughout the program.

See, e.g., Stuart Halloway (a Clojure FP proponent) here saying that "the over-specification of data types" is "negative consequence of idiomatic OO style" and favoring conceptualizing an AddressBook as a simple vector or map instead of a richer OO object with additional (non-vectorish & non-maplike) properties and methods. (Also, OO and Domain-Driven Design proponents may say that exposing an AddressBook as a vector or map overexposes the encapsulated data to methods that are irrelevant or even dangerous from the standpoint of the domain).

5
  • 3
    +1 despite the question has been framed rather antagonistically, it's a good question.
    – mattnz
    Commented Jan 12, 2012 at 3:36
  • 16
    As many have stated in the answers, Functional decomposition and Functional programming are two different beasts. So conclusion that "this seems to pretty much a challenge to functional programming" is plainly wrong, it has nothing to do with it. Commented Jan 12, 2012 at 10:24
  • 5
    Clearly McConnel's knowledge in the modern functional data type systems and high order first class modules is somewhat patchy. His statement is utterly nonsense, since we've got the first class modules and functors (see SML), type classes (see Haskell). It's just another example of how OO way of thinking is more a religion than a respectful design methodology. And, by the way, where did you get this thing about the concurrency? Most of the functional programmers do not care at all about the parallelism.
    – SK-logic
    Commented Jan 12, 2012 at 11:38
  • 6
    @SK-logic All McConnell said was that "functional decomposition alone" does not provide the same means of abstraction as OOP, which seems a pretty safe statement to me. Nowhere does he say that FP languages don't have means of abstractions as powerful as OOP. In fact he doesn't mention FP languages at all. That's just the OP's interpretation.
    – sepp2k
    Commented Jan 12, 2012 at 12:29
  • 2
    @sepp2k, ok, I see. But still, a very complex and well-layered system of data structures and processing abstractions can be built on top of nothing but functional decomposition for nearly pure lambda calculus - via simulating the modules behaviour. No need for the OO abstractions at all.
    – SK-logic
    Commented Jan 12, 2012 at 15:50

10 Answers 10

13

Keep in mind that the book was written over 20 years go. To professional programmers of the day, FP didn't exist - it was entirely in the realm of academics & researchers.

We need to frame "functional decomposition" in the proper context of the work. The author is not referring to functional programming. We need to tie this back to "structured programming" and the GOTO filled mess that came before it. If your point of reference is an old FORTRAN/COBOL/BASIC that didn't have functions (maybe, if you were lucky you'd get a single level of GOSUB) and all your variables are global, being able to break your program down into layers of functions is a major boon.

OOP is a further refinement on this sort of 'functional decomposition'. Not only can you bundle instructions together in functions but you can group related functions with the data they're working on. The result is a clearly defined piece of code that you can look at and understand (ideally) without having to chase all around your codebase to find what else might operate on your data.

27

I imagine functional programming proponents would argue that most FP languages provide more means of abstraction than "functional decomposition alone" and do in fact allow means of abstractions comparable in power to those of Object Oriented Languages. For example one could cite Haskell's type classes or ML's higher order modules as such means of abstractions. Thus the statement (which I'm pretty sure was about object orientation vs. procedural programming, not functional programming) doesn't apply to them.

It should also be pointed out that FP and OOP are orthogonal concepts and not mutually exclusive. So it does not make sense to compare them with each other. You could very well compare "imperative OOP" (e.g. Java) vs. "functional OOP" (e.g. Scala), but the statement you quoted would not apply to that comparison.

6
  • 10
    +1 "Functional decomposition" != "functional programming". The first relies on classic sequential coding using vanilla data structures with no (or only hand rolled) inheritance, encapsulation & polymorphism. The second expresses solutions using lambda calculus. Two completely different things. Commented Jan 12, 2012 at 9:36
  • 4
    Apologies, but the phrase "procedural programming" stubbornly refused to come to mind earlier. "Functional decomposition" to me is far more indicative of procedural programming than functional programming. Commented Jan 12, 2012 at 11:27
  • Yes, you're right. I did assume that Functional Programming favors reusable functions operating on the same simple data structures (lists, trees, maps) over and over & actually claims that this is a selling point over OO. See Stuart Halloway (a Clojure FP proponent) here saying that "the over-specification of data types" is "negative consequence of idiomatic OO style" and favoring conceptualizing an AddressBook as a vector or map instead of a richer OO object with other (non-vectorish & non-maplike) properties and methods.
    – dan
    Commented Jan 12, 2012 at 13:33
  • The link for the Stuart Halloway quote: thinkrelevance.com/blog/2009/08/12/…
    – dan
    Commented Jan 12, 2012 at 13:34
  • 2
    @dan That might be how it's done in the dynamically typed language Clojure (I don't know, I don't use Clojure), but I think it's dangerous to conclude from that, that that's how it done in FP in general. Haskell people, for example, seem to be very big on abstract types and information hiding (perhaps not as much as Java people though).
    – sepp2k
    Commented Jan 13, 2012 at 0:38
8

I find functional programming extremely helpful in managing complexity. You tend to think about complexity in a different way though, defining it as functions that act on immutable data at different levels rather than encapsulation in an OOP sense.

For example, I recently wrote a game in Clojure, and the entire state of the game was defined in a single immutable data structure:

(def starting-game-state {:map ....
                          :player ....
                          :weather ....
                          :other-stuff ....}

And the main game loop could be defined as applying some pure functions to the game state in a loop:

 (loop [initial-state starting-game-state]
   (let [user-input (get-user-input)
         game-state (update-game initial-state user-input)]
     (draw-screen game-state)
     (if-not (game-ended? game-state) (recur game-state))))

The key function called is update-game, which runs a simulation step given a previous game state and some user input, and returns the new game state.

So where's the complexity? In my view it has been managed quite well:

  • Certainly the update-game function does a lot of work, but it is itself built up by composing other functions so it's actually a pretty simple itself. Once you go down a few levels, the functions are still pretty simple, doing something like "add an object to a map tile".
  • Certainly the game state is a big data structure. But again, it's just built up by composing lower level data structures. Also it's "pure data" rather than having any methods embedded or and class definition required (you can think of it as a very efficient immutable JSON object if you like) so there is very little boilerplate.

OOP can also manage complexity through encapsulation, but if you compare this to OOP, the functional has approach some very big advantages:

  • The game state data structure is immutable, so a lot of processing can easily be done in parallel. For example, it's perfectly safe to have a rendering calling draw-screen in a different thread from the game logic - they can't possibly affect each other or see an inconsistent state. This is surprisingly difficult with a big mutable object graph......
  • You can take a snapshot of the game state at any time. Replays are trivial (any thanks to Clojure's persistent data structures, the copies take up hardly any memory since most of the data is shared). You can also run update-game to "predict the future" to help the AI evaluate different moves for example.
  • Nowhere did I have to make any difficult trade-offs to fit into the OOP paradigm, such as defining a rigid class heirarchy. In this sense the functional data structure behaves more like a flexible prototype-based system.

Finally, for people who are interested in more insights on how to manage complexity in functional vs. OOP languages, I strongly reccoomend the video of Rich Hickey's keynote speech Simple Made Easy (filmed at the Strange Loop technology conference)

11
  • 2
    I would think a game is one of the worst possible examples to demonstrate the supposed "benefits" of enforced immutability. Things are constantly moving around in a game, which means you've got to be rebuilding your game state all the time. And if everything is immutable, that means that you have to not only rebuild the game state, but everything in the graph that holds a reference to it, or that holds a reference to that, and so on recursively until you're recycling the entire program at 30+ FPS, with tons of GC churn to boot! There's no way you get good performance out of that... Commented Jan 12, 2012 at 5:51
  • 7
    Of course games are hard with immutability - that's why I chose it to demonstrate that it can still work! However you'd be surprised what persistent data structures can do - most of the game state doesn't need to be rebuilt, only the stuff that changes. And sure there is some overhead, but it's only a small constant factor. Give me a enough cores and I'll beat your single-threaded C++ game engine.....
    – mikera
    Commented Jan 12, 2012 at 6:06
  • 3
    @Mason Wheeler: Actually, it is possible to get virtually equal (as good as with mutation) performance with immutable objects, without much GC at all. The trick in Clojure is using persistent data structures: they're immutable-to-the-programmer, but actually mutable under the hood. Best of both worlds. Commented Jan 12, 2012 at 8:16
  • 4
    @quant_dev More cores are cheaper than better cores... escapistmagazine.com/news/view/…
    – deworde
    Commented Jan 12, 2012 at 11:13
  • 6
    @quant_dev - it's not an excuse, it's a mathematical and architectural fact that you are better having a incurring a constant overhead if you can make up for it by scaling your performance near-linearly with the number of cores. The reason functional languages will ultimately offer superior performance is that we have come to the end of the line for single core performance, and it will all be about concurrency and parallelism in the future. functional approaches (and immutability in particular) are important in making this work.
    – mikera
    Commented Jan 12, 2012 at 11:21
3

"Object-oriented programming provides a level of abstraction that applies to algorithms and data at the same time, a kind of abstraction that functional decomposition alone didn't provide."

Functional decomposition alone isn't enough to make any sort of algorithm or program: you need to represent the data too. I think the statement above implicitly assumes (or at least it can be understood like) that the "data" in the functional case is of the most rudimentary kind: just lists of symbols and nothing else. Programming in such a language is obviously not very convenient. However, many, especially the new and modern, functional (or multiparadigm) languages, such as Clojure, offer rich data structures: not only lists, but also strings, vectors, maps and sets, records, structs - and objects! - with metadata and polymorphism.

The huge practical success of OO abstractions can hardly be disputed. But is it the last word? As you wrote, concurrency issues are already the major pain, and the classical OO contains no idea of concurrency at all. As a result, the de facto OO solutions for dealing with concurrency are just superimposed duct tape: works, but it's easy to screw up, takes considerable amount of brain resources away from the essential task at hand, and it doesn't scale well. Maybe it's possible to take the best of many worlds. That's what modern multiparadigm languages are pursuing.

3
  • 1
    I've heard the phrase "OO in the large, FP in the small" somewhere -- I think Michael Feathers quoted it. Meaning, that FP may be good for particular parts of a big program, but in general, it should be OO.
    – dan
    Commented Jan 12, 2012 at 14:49
  • Also, instead of using Clojure for everything, even things that are expressed more cleanly in more traditional OO syntax, how about using Clojure for the data processing bits where it is cleaner, and using Java or some other OO language for the other bits. Polyglot programming instead of multiparadigm programming with the same language for all parts of the program. (Sort of like how most web applications use SQL and OO for different layers.) ?
    – dan
    Commented Jan 12, 2012 at 14:51
  • @dan: Use whatever tool fits the job best. In polyglot programming, the crucial factor is convenient communication between the languages, and Clojure and Java could hardly play better together. I believe most substantial Clojure programs use at least some bits of JDK's standard Java libraries here and there. Commented Jan 13, 2012 at 7:43
2

Mutable state is the root of most complexities and problems related to programming and software/system design.

OO embraces mutable state. FP abhors mutable state.

Both OO and FP have their uses & sweet spots. Choose wisely. And remember the adage: "Closures are poor man's objects. Objects are poor man's closure."

2
  • 3
    I'm not sure your opening assertion is true. The root of "most" of the complexities? In the programming I've done or seen, the problem is not so much mutable state as a lack of abstraction and an overabundance of detail through the code.
    – dan
    Commented Jan 12, 2012 at 14:33
  • 1
    @Dan: Interesting. I've seen a lot of the opposite, actually: Problems stem from over-use of abstraction, making it difficult to both understand and, when necessary, to fix the details of what's actually going on. Commented Jan 13, 2012 at 3:52
1

Functional Programming can have objects, but those objects tend to be immutable. Pure functions (functions without side effects) then operate on those data structures. It's possible to make immutable objects in object oriented programming languages, but they weren't designed to do it and that's not how they tend to be used. This makes it hard to reason about object oriented programs.

Let's take a very simple example. Let's say that Oracle decided that Java Strings should have a reverse method and you wrote the following code.

String x = "abc";
StringBuffer y = new StringBuffer(x);
y.reverse();
x.reverse();
x.toString().equals(y.toString());

what does the last line evaluate to? You need special knowledge of the String class to know that this would evaluate to false.

What if i made my own class WuHoString

String x = "abc";
WuHoString y = new WuHoString(x);
y.reverse();
x.reverse();
x.toString().equals(y.toString())

It's impossible to know what the last line evaluates to.

In a Functional Programming style it would be written more as follows:

String x;
equals(toString(reverse(x)), toString(reverse(WuHoString(x))))

and it should be true.

If 1 function in one of the most basic classes is so difficult to reason about then one wonders if introducing this idea of mutable objects has increased or decreased the complexity.

Obviously there are all sorts of definitions of what constitutes object oriented and what it means to be functional and what it means to have both. To me you can have a "functional programming style" in languagess that don't have things like first class functions but other languages are made for it.

7
  • 3
    It's a bit funny that you say that OO languages aren't built for immutable objects and then later use an example with strings (which are immutable in most OO-languages including Java). Also I should point out that there are OO (or rather multi-paradigm) languages that are designed with an emphasis on immutable objects (Scala for example).
    – sepp2k
    Commented Jan 12, 2012 at 5:39
  • @sepp2k: Get used to it. FP advocates are always throwing around bizarre, contrived examples that have nothing to do with real-world coding. It's the only way to make core FP concepts like enforced immutability look good. Commented Jan 12, 2012 at 5:46
  • 1
    @Mason: Huh? Wouldn't the best way to make immutability look good be to say "Java (and C#, python, etc) uses immutable strings and it works great"?
    – sepp2k
    Commented Jan 12, 2012 at 5:48
  • 1
    @sepp2k: If immutable strings work so great, why do StringBuilder/StringBuffer style classes keep showing up all over? It's just another example of an abstraction inversion getting in your way. Commented Jan 12, 2012 at 5:54
  • 2
    Many object oriented languages allow you to make immutable objects. But the concept of tying the methods to the class really discourages it from my point of view. The String example isn't really a contrived example. Whenever I call any mehtod in java, I'm taking a chance as to whether my parameters are going to be mutated within that function.
    – WuHoUnited
    Commented Jan 12, 2012 at 13:21
0

I think in most cases the classic OOP abstraction does not cover the concurrency complexity. Therefore OOP (by its original meaning) doesn't exclude FP, and that's why we see things like scala.

0

The answer depends on the language. Lisps, for example, have the really neat properly that code is data--the algorithms you write are actually just Lisp lists! You store data the same way you write the program. This abstraction is simultaneously simpler and more thorough than OOP and lets you do some really neat things (check out macros).

Haskell (and similar language, I imagine) have a completely different answer: algebraic data types. An algebraic data type is like a C struct, but with more options. These data types provide the abstraction needed to model data; functions provide the abstraction needed to model algorithms. Type classes and other advanced features provide an even higher level of abstraction over both.

For example, I am working on a programming language called TPL for fun. Algebraic data types make it really easy to represent values:

data TPLValue = Null
              | Number Integer
              | String String
              | List [TPLValue]
              | Function [TPLValue] TPLValue
              -- There's more in the real code...

What this says--in a very visual way--is that a TPLValue (any value in my language) can be a Null or a Number with an Integer value or even a Function with a list of values (the parameters) and a final value (the body).

Next I can use type classes to encode some common behavior. For example, I could make TPLValue and instance of Show which means it can be converted to a string.

Additionally, I can use my own type classes when I need to specify behavior of certain types (including ones I did not implement myself). For example, I have a Extractable type class which lets me write a function that takes a TPLValue and returns an appropriate normal value. Thus extract can convert a Number to an Integer or a String to a String as long as Integer and String are instances of Extractable.

Finally, the main logic of my program is in several functions like eval and apply. These are really the core--they take TPLValues and turn them into more TPLValues, as well as handling state and errors.

Overall, the abstractions I'm using in my Haskell code are actually more powerful than what I would have used in an OOP language.

6
  • 1
    Yeah, gotta love eval. "Hey, look at me! I don't need to write my own security holes; I've got an arbitrary code execution vulnerability built right into the programming language!" Conflating data with code is the root cause of one of the two most popular classes of security vulnerabilities of all time. Anytime you see someone get hacked because of a SQL injection attack (among many other things) it's because some programmer out there doesn't know how to properly separate data from code. Commented Jan 12, 2012 at 13:50
  • eval does not depend on Lisp's structure very much--you can have eval in languages like JavaScript and Python. The real power comes in writing macros, which are basically programs that act on programs like data and output other programs. This makes the language very flexible and creating powerful abstractions easy. Commented Jan 12, 2012 at 23:23
  • 4
    Yes, I've heard the "macros are awesome" talk many times before. But I've never seen an actual example of a Lisp macro doing something that 1) is practical and something you would actually care to do in real-world code and 2) cannot be accomplished just as easily in any language that supports functions. Commented Jan 12, 2012 at 23:34
  • 2
    @MasonWheeler short-circuiting and. short-circuiting or. let. let-rec. cond. defn. None of these can be implemented with functions in applicative order languages. for (list comprehensions). dotimes. doto.
    – user39685
    Commented Jan 30, 2013 at 21:31
  • 2
    @MattFenwick: OK, I really should have added a third point to my two above: 3) is not already built-in to any sane programming language. Because that's the only truly useful macro examples I ever see, and when you say "Hey look at me, my language is so flexible that I can implement my own short-circuit and!" I hear, "hey look at me, my language is so crippled that it doesn't even come with a short-circuiting and and I have to reinvent the wheel for everything!" Commented Jan 30, 2013 at 21:52
0

The quoted sentence does not have any validity anymore, as far as I can see.

Contemporary OO languages can not abstract over types whose kind is not *, i.e. higher kinded types are unknown. Their type system does not allow to express the idea of "some container with Int elements, that allows to map a function over the elements".

Hence, this basic function like Haskells

fmap :: Functor f => (a -> b) -> f a -> f b 

cannot be written easily in Java*), for example, at least not in a type safe way. Hence, to obtain basic functionality, you have to write lots of boilerplate, because you need

  1. a method to apply a simple function to elements of a list
  2. a method to apply the same simple function to elements of an array
  3. a method to apply the same simple function to values of a hash,
  4. .... set
  5. .... tree
  6. ... 10. unit tests for the same

And yet, those five methods are basically the same code, give or take some. In contrast, in Haskell, I'd need:

  1. A Functor instance for list, array, map, set and tree (mostly predefined, or can be derived automatically by the compiler)
  2. the simple function

Note that this is not going to change with Java 8 (just that one can apply functions more easily, but then, exactly, will the problem above materialize. As long as you do not even have higher order functions, you are most likely not even able to understand what higher kinded types are good for.)

Even new OO languages like Ceylon do not have higher kinded types. (I have asked Gavin King lately, and he told me, it was not important at this time.) Don't know about Kotlin, though.

*) To be fair, you can have an interface Functor that has a method fmap. Bad thing is, you can't say: Hey, I know how to implement fmap for library class SuperConcurrentBlockedDoublyLinkedDequeHasMap, dear compiler, please accept that from now on, all SuperConcurrentBlockedDoublyLinkedDequeHasMaps are Functors.

1
  • FTR: the Ceylon typechecker and JavaScript backend now do support higher handed types (and also higher rank types). It's considered an "experimental" feature. However, our community has struggled to find practical applications for this functionality, so it's an open question whether this will ever be an "official" part of the language. I do expect it to be supported by the Java backend at some stage.
    – Gavin King
    Commented Jul 29, 2015 at 21:02
-2

Anyone who ever programmed in dBase would know how useful single line macros were to making reusable code. Although I haven't programmed in Lisp, I have read from many others who swear by compile time macros. The idea of injecting code into your code at compile time is used in a simple form in every C program with the "include" directive. Because Lisp can do this with a Lisp program and because Lisp is highly reflective, you get much more flexible includes.

Any programmer that would just take an arbitrary text string from the web and pass it on to their database isn't a programmer. Likewise, anybody that would allow "user" data to automatically become executable code is obviously stupid. That doesn't mean that allowing programs to manipulate data at execution time and then execute it as code is a bad idea. I believe that this technique will be indispensable in the future which will have "smart" code actually writing most programs. The whole "data/code problem" or not is a matter of security in the language.

One of the problems with most languages is that they were made for a single off line person to execute some functions for themselves. Real world programs require that many people have access at all times and at the same time from multiple Cores and multiple computer clusters. Security should be a part of the language rather than the operating system and in the not too distant future it will be.

2
  • 2
    Welcome to Programmers. Please consider toning down the rhetoric in your answer and backing some of your claims up with external references.
    – user53019
    Commented Jan 30, 2013 at 19:41
  • 1
    Any programmer that would allow user data to automatically become executable code is obviously ignorant. Not stupid. Doing it that way is often easy and obvious, and if they don't know why it's a bad idea and that a better solution exists, you can't really blame them for doing it. (Anyone who keeps doing so after they've been taught that there's a better way, however, is obviously stupid.) Commented Jan 30, 2013 at 21:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.