5 ms·
This article, like many that cheer functional programming, falls into a certain cognitive bias, that prevents it from seeing what OO is good at. Alan Kay wrote
by millstone 12y ago
This article, like many that cheer functional programming, falls into a certain cognitive bias, that prevents it from seeing what OO is good at.
Alan Kay wrote "The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be."
To start to see what this means, consider the annoying String / Data.Text split in Haskell. String is very much in the "leave data alone" mindset, baring its guts as a [Char]. Now you're stuck: you can't change its representation, you can't easily introduce Unicode, etc. This proved to be so rigid that an entirely new string type had to be introduced, and we're still dealing with the fallout.
Great and growable systems! The large scale structure of our software, decomposed into modules, not just at a moment frozen in time, but in the future as well. We are tasked with thinking about relationships and communication.
So here is how "the better way" is introduced:
> Data is immutable, and any complex data structure...
There's that insidious FP bias: to immediately dive into data structures, to view all programs as a self-contained data transformation. So reductionist! It's completely focused on those "internal properties and behaviors" that we were warned about above.
I would end here, but I just couldn't pass this up:
> To come up with a better solution [for dispatching], Haskell and Clojure take very different approaches, but both excel what any OO programmer is commonly used to.
"Any OO programmer?" No way! OO as realized in truly dynamic languages exposes not just a fixed dispatch mechanism, but the machinery of dispatch itself, i.e. a metaobject protocol:
"...there are not only classes for workaday tools, such as UI widgets, but there are also classes that represent the metastructure of the system itself. How are instances themselves made? What is a variable really? Can we easily install a very different notion of inheritance? Can we decide that prototypes are going to work better for us than classes, and move to a prototype-based design?"
This is far richer than the anemic switch-style dispatch that Haskell's guards and pattern matching provide. For example, try modifying a Haskell program to print a log every time a string is made. You can't!
I'm not familiar with Clojure but I'll bet its object model has its roots in CLOS. Whether or not you call it "object oriented," CLOS is solidly in the spirit of having a dynamic meta-structure.
- colanderman 12y agoI think you and the author have posed a false dichotomy. I avoid "traditional" OO in my own work for the some of the same reasons the author points out; not least of which that traditional classes are a kitchen sink. But many of the ideas of OO; notably extensionality (what the author incorrectly calls intensionality), I could never do without. I agree with you, that exposing the innards of my data structures is a crime: not only do I lose control over their construction and use (including defining equality), but I'm restricted from ever modifying the structure. But nothing in FP prevents hiding structure. You can see it all the time in OCaml: a module signature will declare an opaque, possibly parametric, type, as well as a set of operators over that type. The internal structure of that type is never exposed. All creation and use, and ideally comparisons (though it is unfortunately not enforced in OCaml) must go through the module's API. (Module signatures, it should be noted, may be shared by multiple implementations, permitting compile-time dispatch.) Yet while maintaining opacity, I am free to dispense with the excess baggage an OO class entails: run-time dispatch; a single "self" (i.e. the "friend" problem); that abomination known as inheritance; all these things I need no longer worry about, and my code can be cleaner and more efficient.
- millstone 12y agoThe module system in OCaml sounds very nice (and we all know what the "O" for!). But there's still a bias towards a sort of static-ness in FP. For example, the use of abstract data types where a Java programmer may use a class hierarchy. Clients cannot extend an ADT: I can't make my own List in Haskell and pass it off to a function. Regarding the OO "excess baggage," I would respond that what is "excess" depends on the nature of the system. I can understand dismissing that stuff when your program is self-contained. When the only code at play is your own, when you can statically enumerate every type, function call site, etc, it may be hard to see the value in those features. My project is a shared library, and so is dynamically linked with code written by other teams, perhaps years ago, or even yet-to-written. The system is thus not my program in isolation, but an intimate collaboration between my component and client components. Runtime dispatch, inheritance, reflection, and even occasional mucking with meta-objects are the tools we use to cooperate. This is a type of extensibility that Haskell doesn't even try to support. I don't know about OCaml here. (Alan Kay called this the "negotiation and strategy from the object’s point of view.")
- pacala 12y agoGreat comment, provides good food for thought. The core FP idea is to focus on immutable data and data transformations. This is the minimal set of concepts one needs to juggle to get computations going. When modules communicate, they need to pass data and identify the transformations, so there is no dichotomy here between FP and OO (!). Especially if you think of method tables as data. The String / Data.Text split in Haskell is an artefact of Haskell's ecosystem. It is not a conceptual hurdle, but rather an implementation detail. It is not too hard to imagine a different FP ecosystem where one can readily substitute different implementations under the same immutable data structure API, all with very explicit parametrization of the data transformations. All of (1)immutability, (2)simple data API, (3)polymorphism and (4)explicitism are important. Note that OO systems encourage (3), while FP systems encourage (1), (2) and (4). Code as if you have immutable data and apply data transformations, tune performance by using the best implementations under the common simple data API. The question becomes how to build a system where all of them are ergonomic to use. IMHO, Haskell is not quite it, rather places like Dart / C# offer better ergonomics. The other example is also thought provoking. In a system with polymorphism support, it's relatively straightforward to supply one's favorite String implementation, including one that prints a log on every String construction. The question is how to provide the new module to clients, which is reminiscent of dependency injection, but concrete implementations of DI are magic bad. In an explicit style, this would be realized by making modules functors of other modules and explicitly passing in the method tables: function Foobar(string) return { foo: function(x) return string.concat(x, string.new('abc')) end } end function main1() string = String() foobar = Foobar(string) foobar.foo(string.new('xyz')) end function LoggingString() return String() + { new: function(x) print(x) return String.new(x) end } end function main2() string = LoggingString() foobar = Foobar(string) foobar.foo(string.new('xyz')) end But it takes discipline to write the above and not sprinkle the code with String().new(...) everywhere, which defeats the purpose.
- denibertovic 12y agoHow does FP, most notably Haskell, not encourage polymorphism? This statement is just plain wrong. Type classes are the definition of polymorphism. And you can write very generic, abstract, polymorphic code using these constructs. It's not any languages fault if you write your code expecting only concrete types. By this standard it's Javas fault if the developer isn't using generics. No it's not it's the developers fault. The mechanisms are there. Use them.
- klodolph 12y ago> This proved to be so rigid that an entirely new string type had to be introduced, and we're still dealing with the fallout. There are a lot of things wrong with, say, Haskell '98 from the perspective of a modern Haskell programmer. Strings are one, but monads aren't applicative functors, it took us a long time to figure out how we wanted to write monad transformers, lazy I/O is terrible and we should use conduits or whatever instead. But you picked strings. This example does not help your point for the following reasons: 1. You can't just change the implementation of the string type without messing up someone's program. For a fantastic example, look at the recent change to Oracle's string type in Java. In theory, the interface is the same. In practice, it made a bunch of people mad. 2. You can encapsulate data in Haskell. Look at the "Text" data type, and ignore Text.Unsafe which exposes the gory innards. This is module level encapsulation, which is just as good as class-level encapsulation (better, actually, since it's more flexible). You could replace Text with a UTF-8 implementation or a UTF-32 implementation or some magic implementation that switches between the types, and you wouldn't break consumers of the Text interface. > For example, try modifying a Haskell program to print a log every time a string is made. You can't! This is a really contrived example. First of all, there is the question of whether you will need to create a string whenever you log something to a file, and presumably you wouldn't want to log those strings. Second, this is something you'd do with a debugger, you wouldn't actually do this to a program. Besides, if you had access to the string implementation (which I'm assuming here is Text, because that's what most people use), you could just put some kind of unsafePerformIO call in front of uses of the Text constructor, and since the Text constructor isn't exported from the Text module, you're done.
- mpweiher 12y ago> 1. You can't just change the implementation of the string type without messing up someone's program. Yeah, you can. 'NSString' is in fact a class cluster that provides different implementations/representations. Well, used to be on OS X, because it was changed to be a wrapper for a single CoreFoundation representation. In GNUstep and Cocotron, I think they still use the older class-cluster implementation, and programs are portable between these implementations. Polymorphism, baby :-)
- rtpg 12y ago
- deleted 12y ago[deleted]
- naasking 12y ago> String is very much in the "leave data alone" mindset, baring its guts as a [Char]. Now you're stuck: you can't change its representation, you can't easily introduce Unicode, etc. You're conflating the fact that Haskell had poor modularity when it was first conceived and String first defined, with the claim that only OO can provide the necessary modularity. Clearly ML modules provide and always provided the necessary modularity to abstract over string representations, but there's no OO in most MLs. And now with support for ML modules as first class values, we don't need objects for modularity at any level of programming.