Showing posts with label Closures. Show all posts
Showing posts with label Closures. Show all posts

Sunday, August 26, 2012

Does Object-Oriented Programming really suck?

I recently read Armstrong essay on how much OOP sucks. While such opinions would probably have considered pure blasphemy a few years ago, nowadays they are becoming more popular. So, what’s the matter with OOP?

The first thing that comes to mind is that OOP promised too much. As already occurred to other paradigms and ideas (e.g., Logic Programming and Artificial Intelligence) sometimes researchers promise too much and then they (or the industry) cannot stay true to their promises.

OOP promised better modularity, better code-reuse [0], better “whatever”. Problem is, bad developers write crap with every single programming paradigm out there. With crap I really mean “utter crap”. A good programmer using structured programming and a bad programmer using OOP, is that the good programmer’s structured program would probably look “more object oriented” than the other one.

Another problem occurred in the OOP community: OOP “done right” was about message-passing. Information Hiding, Encapsulation and the other buzz-words are a consequence of the approach (because object communicate with messages, their internal state is opaque) not a goal of OOP. Objects are naturally “dynamic” and the way we reason about code is separating concerns in objects having related concerns and having the objects communicate in order to achieve the task at hand. And I’m a bit afraid of talking about “OOP done right”: I really just have my vision. OOP is very under-specified, so that it becomes very hard to criticize (or defend) the approach.

However, OOP becomes “data + functions”. To me data is simply *not* part of OOP. It’s an implementation detail of how objects maintain state. As a consequence, I do not really see data-driven applications as good candidates to OOP. Once again, OOP was sold as universal. It is not. Consider the successes that functional programming is achieving (performance and concept-wise) in this area.

This “data + functions” comes from C++ being the first commercially successful OOP platform. The great advantage was that the transition from structured programming to OOP was quite more easier, that programs could be far more efficient (read “less runtime”) than message passing dynamic OOP systems, at least back in the day. However, there was so much missing from the original idea!

Since back then classes were considered good (and C++ had — and has — no separate concept of interface) + static typing and a relatively verbose language with no introspection, it became rather natural to focus on inheritance. Which later was proved to be a bad strategy. Consequently, there is less experience in building OO software than one would think, considering that for a large part of its history OOP was dominated by sub-optimal practices.

So, what is really bad with OOP? Following Armstrong:

Data structure and functions should not be bound together

Agreed! But I believe that Data Structures are not *truly* part of OOP. Let me explain.

Data is laid out in some way. There is a “physical layout”, for example. You can express that very precisely in C with extensions, to the point of exactly specifying the offsets of the various datas. A phyisical layout also exists in high level languages such as Python. However, it is not part of the Python model. Of course with the struct module or ctypes you can fiddle with it (but they are libraries), however, for the most part, it is just outside the conceptual language used to describe the problem.

Data is also laid out in logical ways. Computer Science defined many data structures and algorithms over them. You can use OOP to express such structures and such algorithms. Still they are not “part” of OOP, they are just expressed in an object oriented way (or maybe not, and remain at structured programming level).

One very good practice in OOP is not using together in the same body of code objects reasoning at different levels. So, for example, you have your low level business logic code that is expressed in terms of data structures, high level business logic expressed in terms of low level business code… and that’s it. Functions is not really together with data. You don’t have “data”. You have some layers of objects.

The point is that OOP is about creating languages at semantic level (you usually do not get to change the syntax). That’s it. If the language is good, well… good. If the language is bad, ok, we’ve got a problem.

Is this suboptimal? In a way, yes. All the indirection may be very expensive. And since abstractions, well… abstract, you may find yourself with a language that is not expressive enough (at least not at the expenses of additional performance costs). Still functions and data structure is not bound together. You just have objects and messages. No “functions” and no “data”. Please notice: this may not be the right thing to do. Still, the problem is not with data + functions, is just related to applying OOP to a specific domain that is ill suited for the approach. OOP is not for every task in the world. But the same objections applies to every system that is built as a stack of layered abstractions.

Please also consider the Qc Na story! Objects are not necessarily opposed to functional programming (you can see them as closures that make different actions available). Objects are just about state + behavior + identity.

Everything has to be an object

And this is something that I don’t think is a problem. It is like saying that Scheme is bad because “everything is a list” (which, strictly speaking, is not true, there are atoms and lambdas etc). If you do not want to program OO, then don’t. If you want, you probably want that everything is an object.

The only issue with everything is an object is, sometimes, performance. From a variety of points of view, such strategy may kill performance. Objects usually have indirections (read pointer). 64 bit pointer for every integer is bad. So we have hybrid stuff like Java and then we have to deal with boxing (either manually in the past or automatically right now). Performance issues can be somewhat “fixed” using proper JIT systems or other optimization techniques. Or providing libraries that do the right thing (see Numpy, for example).

Other than that, I prefer objections in the line of “this thing that should be an object is something else” than those requesting that something that is an object really is. So, I can favorably consider an objection that says that, in Python, “if” is not an object (true). Even though I’m convinced that having if as a statement is not bad either.

And the whole objections with “time”… well, time (and dates) are a bitch to handle. But I find that Python datetime module gets as close to perfection as humanely possible. I really can’t see describing time with a bunch of enums + some structures as an improvement. On the other hand, it looks to me as one of the cases where it is *easy* to see that the OO approach works better.

Consider the related problem of representing a date in a locale. If you introduce a new representation of time, you need either to modify the original function or create a new function (maybe one that works with both things). Creating an interface, on the other hand makes it very easy to introduce new representations of time.

Objection 3 - In an OOPL data type definitions are spread out all over the place


And once again, in OOPL languages data type definitions are not spread out all over the place. They are in C++ and perhaps in Java. And even in Java, if things are done properly, you reason in terms of interface, i.e., in terms of typed messages, not in terms of data structure.
“”“In an OOPL I have to choose some base object in which I will define the ubiquitous data structure, all other objects that want to use this data structure must inherit this object. Suppose now I want to create some “time” object, where does this belong and in which object…
””“
Here it is pretty clear what he has in mind. I think that it is clear that, conceptually, time is an interface and that there is really no need for it to define “data structure”. And if your language is duck typed, the interface is implicit and you have finished before you started.

Objection 4 - Objects have private state


And here I have to agree: state is the root of all evil. Problem is that entirely stateless systems are simply unpractical. We reason in terms of state. We can describe lots of things as stateless, but somewhat we have state. We have files. We have documents. We want them saved and retrieved.
So, we have to deal with state. Agreed. But: 1. imperative programming is about state2. object oriented programming is not, strictly speaking
You can use a “functional” style in OOP, for example. Most of the times, you don’t do it because the code becomes harder to write. But in many situations, on the contrary, you do it because it makes it easier. Often I write immutable objects that cannot be modified: they are not more stateful than a parameter in a function.
Sometimes this is not practical. Ok. And surely using a very stateful style of programming is bad. State in OOP is expressed as behavior. That’s it. Sometimes is done properly, sometimes it is not. Some strategy to deal with state are extremely nice (STM, as an example). Others are not. Also consider how “modern” functional languages really merge concepts from OOP (without being OOP) and how “modern” OOP languages merge concepts from FP (without being functional).
Examples: Python from the OOP side, using generators, lazy evaluation, list comprehensions, closures, etc etc etc.Clojure from the FP side… and all the features it has to express what resembles interfaces, STM and so on…

——

[0] someone once told me they feel lucky when they can actually use the code effectively once, let alone reusing it.

Sunday, October 30, 2011

Does Perl suck? (and something about this Quorum Language)

The short story is that a group of researchers created yet another language. And I see nothing wrong with that. Most people sooner or later implement their own language (or wish to); researchers just do it more often. These languages usually have specific goals and some of them become widespread programming languages. Other remain academic toys. This particular language was created to be intuitive and easy to use. And the researcher meant to make it intuitive and easy with usability studies. I will comment on the idea later.

Fact is, that they put out a paper which compared their Quorum language with Perl and another language they designed to be bad. And they claim that the accuracy rates of their users were similar for Perl and Randomo (the bad language) and worse than Quorum.

Does Perl suck?

The question is that the story became very popular because of this: because they claim to have scientifically proved that Perl sucks. And such pieces of news have huge diffusion. The essence here is that I do not think that the results are so very relevant: they used only the easy part of Perl. As a consequence, I think that many other languages would have scored similarly. And think about it... if the easy part of Perl is hard for novices... what about the hard one?

As far as I can tell, the thing is mostly a syntactical issue. I haven't found papers on the full semantics of Quorum. From the examples in the paper, it looks like the illegitimate child of Ada and Ruby. I'm sure that I'm missing something, but it is mostly a matter of substituitions like:

for -> repeat [ which other major language had a repeat statement? ]
float -> number [*very* clear, especially before explaining the noobs how much different are floating point numbers from real numbers]

I don't consider it particularly readable. On the contrary to me something like:

  if d > e then
    return d
  end
  else then
  return e
  end
  

looks like a walking syntax error. I think I would take lot of time to convince me that it is proper english. I'm not a native speaker, but to me:

  if something is true, then do this, else do that
  

sounds quite more convincing than:

  if something is true, then do this, else then do that
  

All considered, it seems to me that they are optimizing the easy part of learning. The syntax is an issue in the first like 2-3 months of programming. After that the problems become semantic and theoretic. Computer Science is hard. It is not the syntax of the programming language you first learn to program with that makes it easier.

To me, it looks like they would use sponge rubber balls to make rugby less violent, completely ignoring the full contact issue.

Besides, I believe that most people who have serious issues with the syntax of todays' programming languages would also have problems with the semantics and with calculus and lambda calculus and whatever hard stuff is taught in the courses. Moreover, hiding the complexity is not always a good thing.

In a computer we can't represent a real number. We know that. At a given point in our curriculum, we also know exactly why. However, usually the odd behavior of floating point number strikes. Odd in the sense that people usually expect them to behave like real numbers. They do not.

A popular surprise is in Python when they use the console to make operations. In older Python versions, a number was printed just how it was in memory.

>>> 1.1
  1.1000000000000001

and guess what... this is extremely surprising for noobies (and in Python 2.7 the algorithm changed so that the printed number tries to be more 'intuitive' and in the case just prints 1.1). The fact is that 1.1 cannot be represented in memory (as a floating point number) and an approximation is used. This is quite hard stuff.

Pretending that floating point numbers are not floating point numbers is going to lead to disaster as soon as things get rough. So, I while I think there are far easier languages to learn to program with than Perl (Python, Ruby, Scheme) I don't think that Quorum is such a huge advantage past the "very very noob - why this does not compile if(foo); {...} else; {...}" part. And that is not the part to optimize.

And what about quorum?

Unfortunately enough, I do not know very much about usability. So I really cannot comment their methods. However, I think that I can say a couple of things from the point of view of language design. The first thing is that most languages that I think are worth learning and using have some very complex stuff and that stuff is what makes the language powerful. With powerful here I mean that it gets the job done quicker, with less line of code and, why not, shapes the programmer's mind in good ways, giving him insight.

Remove this advanced stuff and you have only dumb ugly languages. Such languages would not make you or your programs better. Remove first order functions and macros from Lisp or Clojure and you are just coding Pascal.

And I'm not exactly sure that "usability" can be applied to programming language design. Some argue that since most "experts" disagree on which features should be added to a programming language (and agree that having them all is a bad thing), such studies are needed to determine how to build the one and "true" language. And I have to say that I disagree.

Democracy plainly does not work in technical issues. Noob programmers are plainly not entitled to discuss about advanced language features that they have not yet the experience to understand and which solve problems they have not yet encountered. So called "professional" (i would say business) programmers do not care about the programming language, be it Java or Cobol. They probably would not have advanced features either, because they may thing such features would slow their juniors down. They mostly care about powerful libraries: the discussion is not at the language level, but at an architectural level. They don't do algorithms, they have to make complex under-documented systems interact and work correctly.

So we have just people who "love" programming languages. People who have a strong opinion on how a programming language should be. And guess what? We disagree. Some of us prefer Ruby to Python. Someone else thinks they are the same and that Haskell is the best.

The simple decision of creating an object oriented imperative language is a very strong assumption. I could not rationally say that object oriented is easier than functional programming. Not even if I thought it. And you can't ask noobs if they prefer functional languages (which they do not know what they are), nor you can ask business programmers (which probably dislike them). And if you ask to object zealots, they will answer that FP is just harder.... however, if you ask to functional programmers, they will say that FP languages are easier.

There are lots of design issues which are too bloody architectural to be decided with statistics.

Regarding the Quorum language itself... I strolled through the library files. And what? I changed my initial opinion... its more something between VisualBasic and Java. It has generics. Which are one of the things which are bloody harder to understand in Java (especially if you throw in extends, ? and company). And if you don't, you have a type-system that cannot express some perfectly useful stuff.

It has packages and includes (they are just called 'use'). The nice thing is that Java without an IDE is a PITA partly because of the redundancy of the interaction between classpath/packages and directory structure. And as far as I can tell that stuff is still there. Not sure that being strongly typed makes things easier for the noobs too.

Well, good luck then... my first impression is that it is basically Java without some nice stuff and a visual basic-ish syntax. On the other hand the compiler code looks very well written (still... writing compilers in Haskell/scheme is just so much easier than doing that in Java)... but really: in 2011, with everybody re-discovering the merits of functional programming (Clojure/Land of Lisp/Learn yourself Haskell/Racket/.../C# being more functional than ever/Erlang)... do we need yet another object oriented language?

Sunday, February 27, 2011

call/cc! I yield! Bah, lazy-seq.

Modifications

  1. 03-01-2011: an additional scheme variant has been added.
  2. 03-02-2011: minor modifications

Imperative Iterators

The archetypal implementation of imperative iterators are Java's subclasses of the Iterator interface. Everyone who has ever implemented an Iterator should acknowledge the pain that is to explicitly maintain state between calls. The essential problem is that the state has to be explicitly maintained. In particular there are two different concepts of state: the iterator "low level" state and the iteration state. In the iterator we have to save "what we have done" and explicitly do so.

Then every time next() or hasNext() are called, we have to do some more computation, save our internal state and return the control explicitly to our caller. This is the implementation of a Javish iterator in clojure to pre-order visit a tree like structure.

I decided to use Clojure instead of Java because the example structure was easier to write, the example could be just run in a REPL, i prefer Clojure to Java and other irrelevant details. However, that is essentially the way iterators are written in object oriented languages (included nice ones, like Python) or languages that have to interoperate with OO languages.

The principal issues is that the "value returning" logic has to be mingled with the iterator bookkeeping logic; I believe that this (although more versatile) is far more complicated that simply adding all the values in a (flat) structure and returning the structure. The reason why this is not the dominant approach is that it is less flexible and less efficient. We also notice that iterators can be infinite.

Ah, if you like OO mumbo-jumbo, this is of course the Iterator pattern.

Yield Statement

In this section we briefly examine Python yield statement. The corresponding yield expression (which essentially allows limited coroutines) is not considered here. If a Python function contains the yield keyword, its semantic changes.

The yield statement may only be used inside functions. A function that contains a yield statement is called a generator function.

[...]

When a generator function is called, the actual arguments are bound to function-local formal argument names in the usual way, but no code in the body of the function is executed. Instead a generator-iterator object is returned; this conforms to the iterator protocol[6], so in particular can be used in for-loops in a natural way.

[...]

Each time the .next() method of a generator-iterator is invoked, the code in the body of the generator-function is executed until a yield or return statement (see below) is encountered, or until the end of the body is reached.

If a yield statement is encountered, the state of the function is frozen, and the value of expression_list is returned to .next()'s caller. By "frozen" we mean that all local state is retained, including the current bindings of local variables, the instruction pointer, and the internal evaluation stack: enough information is saved so that the next time .next() is invoked, the function can proceed exactly as if the yield statement were just another external call.

Essentially instead of using the structure of the previous example, we could have written:

The idea is that when yield is encountered the generator "returns" the value and when next() is subsequently invoked, it resumes execution from that spot. Essentially it is a neat syntactical facility that "saves" somewhere the continuation of the computation in order to be ready to resume from that point.

call/cc

Ok. I said it. Continuation. That is. When the goal is to experiment with control, scheme comes to mind. This is our representation of the tree:

Now I provide an easy example for doing the preorder "functionally". The idea is to provide a function taking a callback function that is called on each element of the tree when is visited. The function also returns the return values of the callback function (in reverse order); anyway, use display as a callback function and we have our familiar example:

Here we did not use continuations. In the following example, I use continuations to save the state of the computation where to resume (after the yield) and to jump out (simulating python yield statement) with another continuation. I think this code should be written more clearly; I'm no expert with continuations (I usually find them too general), so suggestions are welcome.

When generator is called, it returns a function that every time that is called returns the successive element. It behaves like an almost like an iterator and has the structure of Python generator functions. Boilerplate code could be avoided using a macro. Essentially, lines 2-6 are boilerplate and similarly are lines 11-13 and 18. The difference with a "java like" iterator is that when there is no way to test if there is a "next" element (no hasNext method, closures are objects with just one method! ;)) and it returns null forever. So the implicit protocol is "do not put null objects in the tree" and if you get null, then you are done. Other strategies could be used, e.g., instead of returning the value, a list containing the value is returned; empty list means that the iteration completed; list containing an empty list meant that the tree contained a null valued node.

We could have used continuation passing style and that would perhaps have been more similar (even though far more complex to understand and write) than the corresponding Clojure way to do it.

In the comments a different scheme continuation based solution has been suggested by Marco. It is far more elegant than my original solution, but is structurally different from the Python code of the sections above. In fact, it does not use an explicit stack to manage the tree traversal (as both the Python version and my scheme version do), but relies on more recursive calls, essentially using the implicit call stack.

What about lazy-seq?

Essentially we specified that iterators are needed because they are efficient and more general. Indeed in languages such as Python, generators are an easier way to write iterators. What about Clojure? The example at the very beginning is not the way things should be done in Clojure. On the other hand the iterator in scheme would be a good API for Clojure as well. In fact it is the very same pattern that we have with map (Haskell programmers here may want to extend the discussion on "mapping" on different algebraic types, such as Trees, and not only Lists).

Imperative iterators are just a design pattern. Call/cc is a feature so powerful you can use it build almost every other control abstraction. Yield can be used to implement coroutines (which could also be implemented with call/cc). And what about lazy-seq? Lazy-seq is nothing like that. Many Lisp books show a simple example where macros are used to implement lazy evaluation (or at least list lazy evaluation, which is what matters most). lazy-seq basically does that. The difference is that other Lisps are usually not built around the idea of lazy sequences the way clojure is.

Consequently, most libraries and APIs do not work (or do not play well) with lazy evaluation. They could be made to work, but it is simply outside Common Lisp pragmatics, and there is not anything wrong with that.

But back to lazy-seq... why it is here? Consider the code below.

I want to point out the structure of the tree-seq-aux function. Indeed, the function is not recursive. It simply return a lazy-seq. The second parameter of the cons cell we put in the lazy-seq, is another call to tree-seq-aux. However, this is not recursion: when lazy-seq is evaluated, we already returned from the previous call.

But thing are even more interesting than this. That cons cell is somewhat similar to the iterator we saw at the very beginning. Its car (first) is the current value. It is "next". And what is its cdr (rest)? Suppose that our iterator was not stateful. Suppose instead that calling next() returns both the current value and another iterator which will provide the successive values. Well... basically it is our cons cell.

Pair<Value, ConstIterator<Value>> v = it.next();

That is essentially our cons cell. But in fact we somehow have the same pattern we had in the continuation based example. We "save" what to do next (akin to the jump continuation) in the cdr of the cons cell; however, we do not have to evaluate it right now because of the lazy-eval. As I already said, if we used continuation passing style, we would have placed the recursive call which we put in the lazy-seq cons cdr in the explicit continuation.

The advantage is that it is easier to write, easier to understand, easier to use. Unfortunately is much more specific and powerful. But I don't mind practical tools, when they have the elegance lazy-seq has.

Technorati Tags: , ,, ,