Hacker News new | ask | show | jobs
by millstone 4291 days 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.

4 comments

I 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.

I suspect some of the problems that many people have with OO tend originate from the C++ and relate languages such as Java. These languages aren't really OO in the Alan Kay sense of the term[1]. They are languages with classes, polymorphic inheritance, and object style binding of methods to structures, but they do not feature "everything is an object with message passing".

By comparison, you really see a lot more of the utility of OO in languages smalltalk or possibly ruby[2] where you can extend everything. I know tend to write my ruby (despite it being a multi-paradigm language) in a manner that you describe: FP style with objects hiding the details.

Of course, all of these languages have their strengths and weaknesses and OO isn't useful for everything. I just think OO has gotten a bit of a bad reputation from some of the languages that chose to label themselves OO even when their implementation was only superficial. This bad reputation may lead to dismissal of the whole idea, producing the false dichotomy you mention.

Incidentally, the lack of strict OO (or any language style) in ruby is what I really like about the language. You can be strict OO if you want, but you can also use classic (C-style) imperative programing when it makes more sense (or FP, or whatever).

[1] http://c2.com/cgi/wiki?AlanKaysDefinitionOfObjectOriented

[2] Regrettably, OCaml is one of those languages that is still in my "looks interesting, I should learn that" queue, so I cannot speak to how it implements OO.

OCaml's OO is IMHO not very interesting, beside the concept of "functional objects", which really ought to exist without the rest of the Java-style OO baggage. (Briefly: methods can easily return a copy of an object with some fields modified; and anonymous, structurally typed objects may be constructed.) Otherwise it is standard Java/C++ fare (albeit more streamlined and with better typing).

On the other hand, OCaml is worth learning for the module+type system alone. Every other language could benefit from its ideas; the only language I've seen that's comparable is Coq (which bases its module system on OCaml's). (And the module and type system really work in tandem: there are advanced mechanisms for type structure hiding that aid forward compatibility.)

The 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.")

In the same way that many recommend programming to interfaces rather than concrete classes in static OO languages, programming to typeclasses rather than concrete types (when you can't avoid that kind of dependency and write completely generic code) is an important recommendation in Haskell.
> The 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.

Depends on what your function accepts. If it takes explicitly a list, you're screwed, but it clearly was never intended to be generic. If it accepts something Foldable or Traversable, just make sure your data structure has an instance for these type classes.

In OCaml, you can have objects and inheritance if you absolutely want to, but you can get a lot out of structural typing before going there. If you want extensible ADTs, you can, but you need to plan for it by using polymorphic variants [1] at the expense of some safety.

1: https://realworldocaml.org/v1/en/html/variants.html - scroll down to the "Polymorphic variants" section

> 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.

Constructing a component architecture is the goal of many approaches, and shared libraries is one expression of that ideal. When a library is compatible with the calling application and the OS, a library can closely approximate an ideal component.

However, components, applications and OSs are not static but constantly changing. In order for a library to be a component used by many other entities, the library must be continually (at least frequently) curated to remain compatible with all the other components it cooperates with.

While the point of a library is to abstract an API so users of the library don't have to think about how it's implemented, the creator of the library must consider those details very deeply.

Whatever techniques or languages are used to create a library, OO, FP, both or neither, the most important consideration is that its source code is clear, concise, logical, and understandable. The library I create today will decay if not maintained, and if I'm not around, how easily can someone pick up where I left off?

The inevitable tricks employed making procedures or methods work in real code will not be obvious to our successors. Good ideas, even embodied in obsolete code can be useful if clearly expressed and adequately explained. Thorough documentation transforms the work into lasting value.

> (Module signatures, it should be noted, may be shared by multiple implementations, permitting compile-time dispatch.)

With first class modules, you can even get runtime dispatch, just build a new module dynamically, selecting the concrete implementation depending on, say, on a command line parameter.

> 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.

> 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 :-)

Is there a good reason for why lazy I/O is terrible? It seems like the ideal solution for async-heavy programs
http://www.reddit.com/r/haskell/comments/1e8k3k/three_exampl...

Tekmo

I highly recommend reading these slides by Oleg:

http://okmij.org/ftp/Haskell/Iteratee/IterateeIO-talk-notes....

They are his old annotated talk notes and they give a really thorough description of real problems that lazy IO causes with lots of examples.

Edit: Here's a select quote from the talk:

> I can talk a lot how disturbingly, distressingly wrong lazy IO is theoretically, how it breaks all equational reasoning. Lazy IO entails either incorrect results or poor optimizations. But I won’t talk about theory. I stay on practical issues like resource management. We don’t know when a handle will be closed and the corresponding file descriptor, locks and other resources are disposed. We don’t know exactly when and in which part of the code the lazy stream is fully read: one can’t easily predict the evaluation order in a non-strict language. If the stream is not fully read, we have to rely on unreliable finalizers to close the handle. Running out of file handles or database connections is the routine problem with Lazy IO. Lazy IO makes error reporting impossible: any IO error counts as mere EOF. It becomes worse when we read from sockets or pipes. We have to be careful orchestrating reading and writing blocks to maintain handshaking and avoid deadlocks. We have to be careful to drain the pipe even if the processing finished before all input is consumed. Such precision of IO actions is impossible with lazy IO. It is not possible to mix Lazy IO with IO control, necessary in processing several HTTP requests on the same incoming connection, with select in-between. I have personally encountered all these problems. Leaking resources is an especially egregious and persistent problem. All the above problems frequently come up on Haskell mailing lists.

Oleg is a good guy to listen to.

Its harder to reason about resource usage with lazy IO. For example, when is it safe to call hclose to close a file handle?

     do
        f <- open "file.txt"
        s <- readContents f
        hclose f
        print s
Since readcontents is lazy, it only tries to get data from the file when you print s. But by that point the file has already boon closed!

If you think about it, its a bit similar to the tradeoffs between garbage collection and reference counting.

> 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.

Great 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.
How 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.

I'm not hacking Haskell very often, but I get now and then vibe from the community that:

a. Avoid typeclasses until strictly necessary, http://www.reddit.com/r/haskell/comments/1j0awq/definitive_g...

b. Haskell has no first class instances. http://joyoftypes.blogspot.com/2012/02/haskell-supports-firs...

> The ability to have only a single instance for each class associated with a type makes little theoretical sense. Integers form a monoid under addition. Integers also form a monoid under multiplication. In fact, forming a monoid under two different operations is the crux of the definition of a semi-ring. So, why does it make any sense at all that in Haskell there can be at most one definition of “Monoid” for any type?

<removed useless stuff>