Why are objects so hard to debug?
codewords.hackerschool.com
codewords.hackerschool.com
I think this is largely a moot point with debuggers that can (conditionally) breakpoint-on-write -- if you see a global variable being set to a wrong value, put a breakpoint-on-write to that variable with condition to trigger when that wrong value is written, and you'll stop exactly in the code that wrote to it. Then trace back the computation from there to see where that value came from.
What really makes programs hard to debug is their total size and complexity; in object-oriented programs that tend to favour emphasis on reducing local complexity, global complexity increases. Tracing through layers of indirection whose only purpose is seemingly to call other layers is tedious, and the flow of data --- something which is important to see --- becomes more obfuscated.
This right here is probably the one of the only two arguments I would've accepted about why OOP is bad--the other being that certain problems just don't map cleanly onto OOP.
EDIT:
Acton had a great presentation on why OOP is maybe not a good fit in graphics code:
http://macton.smugmug.com/gallery/8936708_T6zQX#!i=593426709...
That aside, I'm curious about your comment on "certain problems just don't map cleanly onto OOP"
FP and OOP are both Turing complete, which kind of problems you think "just don't map cleanly onto OOP"?
For instance, think about Green's, Stokes', and Gauss's Theorems in vector calculus. They say that the integral of a function over the boundary of a region is equal to the integral of a derivative of the function over the whole region. They're equal; you can't get much more equivalent than that. But in practice, one of the integrals is often easier to do than the other, and so these theorems are used in practice to transform a hard problem into an easier one.
Both FP and OOP are Turing complete. But in practice, certain problems may be easier to program in one approach or the other.
If I were writing a numerical analysis package, the functional approach might work better. If I were writing an event-driven embedded system, I'd expect that OOP would do more for me than FP.
I'm not saying that it is wrong or even unnecessary--but the parent criticism was that it seemed more complicated than it had to be, and that it could often be more important to look at the data instead of the objects. I agree with that criticism.
As for problems, I'll agree with my sibling poster--just because two things are academically equivalent doesn't mean that they are practically equivalent.
Consider the link to Acton's talk...a lot of the testing and method calls are redundant or can be pulled out at a higher level, and encapsulation doesn't really win you anything because you know the problem domain concretely.
Numerical code is another case where OOP doesn't quite get you there. If you're just finding the average of a list of numbers, is it more sensible to have a function which maps elements of the space of lists of numbers into a space of scalars (which happen to be averages), or to have a list of numbers object that happens to know how to answer the "what is your average" message?
Now, let's say you wanted to represent a new space only of elements consisting of averages plus some offset. That's again trivial in a functional language with currying, but in OOP is kind of awkward.
The reason we like functions is because they represent a straightforward mapping from one space to another, and can be implemented very performantly. OOP principles, especially encapsulation, require that a function cannot know about the internals of the object it's working with, because it should be able to be changed out at a moment's notice.
It is also because of the requirement of functional programming to know concretely the details of the things passed into them that we see such demand for static and strongly-typed and immutable systems.
Yes! This crystallizes the way I feel about many OOP designs.
And when you design sucks, Natural Selection will let you know.
It doesn't fail.
What does this mean?
Your debugger is broken. The call stack will have implicit "this" pointers/references in each frame, and the instance variables can be found there.
Now, the rant as a whole has a valid point that mutable state causes confusion in terms of finding the code that mutated it. An argument for immutability or time-reversible debuggers. But OOP at least gives you a chance to keep the mutating methods close together where you can find them.
Deleted comment
The more you program the more you tend to design with the intent of making it easier to debug. You end up doing a lot of things that borrow from other paradigms like passing more variables to method calls while using fewer instance variables and consciously avoiding side effects.
Honestly, I don;t know enough about the functional paradigm to argue whether it is easier to debug. It looks like it has a lot of features that would make it so.
Definitely agree that inheritance, instance variables, setters make things more difficult. Inheriting from a framework is asking for trouble.
lolnope
(Have a look at, say, the UK benefit system and the failing Universal Credit project that's supposed to work out what to pay people in only one place rather than across a dozen agencies)
Firstly I don't spend most of my time debugging. I do spend a fair amount of time programming in a way that makes it less likely I'll have to debug (asserting contracts, implementing tests). Whacking out some random spaghetti code, then spending days navigating it isn't productive.
And secondly, thinking about the recent projects I've worked on, the debugging I have done has not been about finding where a variable got the wrong value. The only day-sapping bugs I've had recently have been figuring out why some piece of code performs an incorrect calculation; and figuring out how to ensure the correct order of different operations. The latter I suspect would have been more difficult in an FP with a more complex stack trace and tail call elimination. Most of the smaller debugging problems I have relate to impedance mismatches between my code and 3rd party libraries.
Library interface problems seem less in FP, but mostly because there are fewer libraries. It is easy to make a totally understandable, simple, programming environment if you never have to do any heavy lifting with other people's code.
I program in FP, but a lot of the time I end up in OO languages with good library, tool, testing and debugging support.
I can't help but think this is another case of FP-zealots interpreting the world in a way that maximises the problem they know how to solve.
With objects, state is encapsulated and not modified by the outside, making them much more predictable and thus also easier to test.
For example, in Python, if I have two functions that implement a "download" operation, but they accept different types of locations, I could use different modules to implement them:
from mypkg.endpoints import ftp
from mypkg.endpoints import http
ftp.download(ftp.info(some_ftp_info))
http.download(http.info(some_http_info))
You can see that we pass the result of `ftp.info` to each, which is essentially defining its type somewhat reliably.In OO, you can simplify things a great deal by encapsulating data in an object, as well as organizing functionality accordingly. I think (in Python at least) the same benefit can be accomplished by using modules for namespaces and grouping, like you would a class.
By using modules in this way, it is then possible to utilize types like you might in haskell as well as enforce immutability, assuming the "types" you create are immutable (like namedtuples).
All that said, I've yet to have the courage to enforce this sort of paradigm in a project. I worry that it would be prohibitively expensive to my fellow developers who may not appreciate the benefits. With that said, I would be interested if the paradigm would be reasonable over time.
The other case is dealt with by moving all the methods back into a main (giant) method. Then I'll refactor that. It's more fuss, but not that much more.
This one goes further and argues that oop is !imperative. Have popular definitions changed while I wasn't paying attention?
Though, people refer to languages like lisp and scala as functional even though these langs are only functional if you use them a certain way, and I think it dilutes the term and creates confusion.
Modern debuggers have conditional breakpoints, breakpoints on write, breakpoints on count, sophisticated formatters and all kinds of facilities which make the problem of debugging a method call that might go to a subclass instead of the base class all but trivial.
Debugging complex programs is hard, regardless of the underlying methodology that this code uses.
Author sounds like talking of OO tools frozen in the 60's, really poorly investigated or just sharing his/her own rationalising and enthusiasm about using functional programming instead.
There are advantages to the functional approach - I use it for most of my programming these days - but this article is pure FUD. Actually it goes beyond that, making claims that are outright falsehoods.
They also didn't set out to create a functional language explicitly. They didn't know about monads, applicative, functors etc.
Another funny anectode, Joe Armstrong is quite fond of calling Erlang the only true OO language in its original sense. The idea of methods being messages sent between isolated and opaque objects. This isolation is really enforced at the heap level and methods are really messages being send to an actor's mailbox.
If you listen to Alan Kay, he mostly agrees:
http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay...
---
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them.
Cheers, Alan
---
http://en.wikipedia.org/wiki/Actor_model#No_requirement_on_o...
I really like it because it feels like it was meant to do something other than pad out a thesis--it's probably the only functional language other than F# or LINQ to do that.
And function with closure and/or with a state passed through and it is an as good as an object.
Heck, look at one of the most popular languages -- Python. Method calls look nice an explicit like def method(self,...). Others have self too (usually called "this"), but it appears there implicitly.
At the memory level Erlang actors are even better objects because they actually enforce memory isolation. Which is in spirit with Alan Kay's original OO idea.
"Foolish pupil - objects are merely a poor man's closures." And visa versa.
However, this line of reasoning definitely differs from what one nowadays considers an object-oriented language. Which was exactly what this article was about.
> The technical term for a programming paradigm which uses neither inheritance nor instance variables is imperative (or functional) programming.
what ?!?
Polymorphic virtual methods are also hard to debug because you must figure out what type the object actually is. You must either run the code with breakpoints, or run it mentally in your head.
Don't give up on knowing exactly who's in control and what the data structures look like unless there is a clear benefit.
With heavy OOP in C++, you often end up with code that knows nothing about the data except a heap pointer and a pointer to a virtual method table. Not only does this make debugging a pain, it kills almost every opportunity for compiler optimization.
Of course, this pattern can be really useful. But it should be selected as a very deliberate design process, not "Oh, we put everything behind interfaces because I heard it's good."
The only place where the pattern is really, truly necessary is when a library needs to call user-supplied methods on user-supplied data. In C this looks like:
// library.h
void set_callback(void (*myfunc)(void *, int), void *);
// me.c
struct S { ... };
void update_s(void *s, int new_value); // must cast to struct S *
struct S s;
set_callback(&update_s, &s);
This is ugly but it makes it clear how much information is being lost in such a pattern. a.getB().getC().getD().e() - NullPointerException can have 4 causes
B b = a.getB();
C c = b.getC();
D d = c.getD();
d.e(); // NPE can have only 1 reason, you can inspect b,c,d at debug time without stepping inside
Another thing that hurts debugability are non-reversible transformations in code. If sth is non-reversible it means 1 effect can have many possible causes, so when debugging you need to consider many possibilities why we are here. When there are many non-reversible steps you get exponential increase of possibile causes.The most common things that cause this in code are defaults - when you have defaults and need to debug you need to consider many possible reasons the end effect is what it is.
It's not a big deal, but it makes debuging one step harder (instead of "I know why we get there, let's go deeper" it's "We get there either because of X or Y, let's debug to decide, or go both ways").
I think if you feel this way, you should definitely look for languages and tools that don't admit as many bugs.
It also helps tremendously to limit the scope of what you are doing. One of the reasons the "add print statements" helps so many people is that they reduced the scope of their thinking to be smaller than the entire problem. It isn't, "how can I make this whole program behave," but rather "how can I make this variable what I want it to be?"
There is a trap in this thinking, though. Many feel that if you can reduce everything down to small functions that it will be easier to deal with. This can be true, but it isn't strictly true. If each function is called from many places, then it can become hard to really understand the impact of a change.
Spending "lots of time on debugging" is a signal of lots of reverse engineer that is hard (not easy), probably due to poor method naming (very common problem regardless of paradigm).
Is not the language fault that you design sucks.
So, I guess yeah, if you don't have great tools anything is "so hard to debug"
Also, always using inheritance is bad, but so is never using inheritance. (Does that need explanation?)
With imperative code, you can just hop up the stack!
...unless something screws up your stack frames, in which case you win nothing good day sir.
With OOP code, you alternate framework code with application code, which never happens with imperative libraries!
...except any sort of nontrivial library that has proper abstraction via callbacks, in which case you very much do end up alternating application code with libraries.
Inheriting from a framework object can do things you don't expect!
...except that in languages that aren't C++, it's usually pretty easy to control whether or not you execute parent class code instead of/in addition to your extended function.
The technical term for a programming paradigm which uses neither inheritance nor instance variables is imperative (or functional) programming.
ahahahahha what? no.
Imperative != functional programming. At all. Had a CTO try to convince me this was case, was dead wrong. Imperative code allows for mutation of global state and perhaps no functions-as-first-class-citizens. Functional code (in theory) allows for no mutation of global state and always has functions-as-first-class-citizens.
Author has also left out declarative languages, which also somewhat fit that definition, but hey, who's counting?
The best way to illustrate the issue with debugging multi-threaded programs is to describe the debugging scenario as consisting of multiple stacks.
What? Fuck no. The biggest issue with debugging multi-threaded programs is having shared mutable state. The fact that thread A can write and screw up something depended upon by thread B, which is totally supported in imperative programming and is hard to protect against, is the problem. Second would be the magic of race conditions, live locks, dead locks, and all of that jazz.
OOP is, other than pure functional programming, perhaps the only sane way of dealing with mutlithreaded programs when you aren't subscribing whole-hog to actor-style message passing.
~
This is a bad article, and displays both a fundamental lack of understanding as well as a sort of arrogance in the profession of the aforementioned ignorance.
EDIT:
And no, OOP/actor programming is not a panacea for all of your problems either.
There is quite a lot of room in the world for simple imperative programs (which are basically objects interfaced with from the command line) or chained functional programs (very useful for data transformation pipelines).
That said, there are reasons why we invented OOP. There are use cases for it. There are places where it is the right thing, and to pretend otherwise makes it all the harder to know when not to use it.
In those systems, you technically always work on objects.