Why are objects so hard to debug?(codewords.hackerschool.com)
codewords.hackerschool.com
Why are objects so hard to debug?
https://codewords.hackerschool.com/issues/one/why-are-objects-so-hard-to-debug
62 comments
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...
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...
This is just an emotional appeal. You can equally have "layers" of many dependant functions that have tedious flows of data obfuscating what's important to see.
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"?
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"?
Things that are equivalent in theory are not necessarily equivalent in practice.
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.
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.
Oh, that's absolutely true--but in OOP as practiced with deep inheritance trees (as distinct from, say, actors with internal functional implementations), it's not just possible but indeed is the preferred way of doing things.
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.
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.
Well, this is the debate. OO tends to reduct the total lines of code, but increase the lines of code you have to view when you debug per issue. When you look at the code that triggered the bug, it's hard to tell what is local and what was inherited, patched, subclassed etc.
> in object-oriented programs that tend to favour emphasis on reducing local complexity, global complexity increases.
Yes! This crystallizes the way I feel about many OOP designs.
Yes! This crystallizes the way I feel about many OOP designs.
Which is interesting since global complexity is the only one that scales.
And when you design sucks, Natural Selection will let you know.
It doesn't fail.
And when you design sucks, Natural Selection will let you know.
It doesn't fail.
> Which is interesting since global complexity is the only one that scales.
What does this mean?
What does this mean?
I think he means that, as you try to grow your system, it's global complexity that limits your ability to scale.
From the perspective of debugging, an instance variable is not in the scope of any of the methods on the stack
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.
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](1)
I think the most important aspect of the object paradigm is that it models real world entities. The real world has a logical consistency so code properly modeled on it will have a logical consistency. This makes it easier to extend as you try to solve more problems in the given domain.
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.
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.
My C++ coding style is motivated by debugging easiness. I write many local `const` variables acting like a big `let` expression in Lisp. I trust the compiler's data flow analysis to optimize them into the minimal needed registers and memory in optimized builds, but in debug builds I can look at the whole history of the computation to find out what went wrong.
Real world entities are harder to model than we sometimes like to think. This is a problem in all paradigms because its the fundamental challenge of understanding the world. Entities in the real world often have hidden or poorly understood behaviours.
This translates into those moments where you whish you could start all over - because you just learnt something important about your domain.
The real world has a logical consistency
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)
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)
i'm not sure how to contradict this since it seems orthogonal to my point. I don't know those programs and can't tell which aspects of them you consider unmodelable. A human designed system could be stupidly designed but as long as the design doesn't change randomly, you can model it.
I don't recognize the problem from this article (programming for 20 years, including 10 doing R&D as a consultant).
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.
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.
Objects are much easier to debug then their predecessors: methods making use of global state variables.
With objects, state is encapsulated and not modified by the outside, making them much more predictable and thus also easier to test.
With objects, state is encapsulated and not modified by the outside, making them much more predictable and thus also easier to test.
I'd rather deal with a procedural program modifying global variables than an object-oriented one. Strewing an algorithm across a bunch of methods makes control flow incomprehensible.
I've felt the same way at times until I realized the issue is just my perspective. OO provides an organizational tool for organizing functions and variables. The same thing can often be accomplished by changing the way code is organized.
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:
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.
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.
Me too, but I'm going to immediately take that procedural program and embed it into its own class, refactoring it until the complexity is nicely tucked away.
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.
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.
Objects are far from the only way to limit access to state. Modules, lexical scope and closures, even C's static storage class all predate the current crop of OO languages.
This has to be one of the first times I've seen functional programming called imperative. I think it was just last week I was in a debate where people were basically calling functional as the same as !imperative.
This one goes further and argues that oop is !imperative. Have popular definitions changed while I wasn't paying attention?
This one goes further and argues that oop is !imperative. Have popular definitions changed while I wasn't paying attention?
No, the article does not use language very accurately. There's an extremely long paragraph suggesting that instance variables are essentially global variables because they aren't lexically scoped (without ever using the term "lexical scope"). And it uses 'imperative' where it should use 'procedural'.
As far as I know functional languages would be declarative and OO langs would be procedural and imperative.
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.
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.
You can even use Smalltalk in a functional style which feels quite good actually.
This whole article seems to be more about the author not having a good debugger at his disposition than "OO programs being hard to debug".
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.
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.
Yes, completely biased. It takes "having to see implementors or senders in the whole system" as if it where some kind of a problem. Same for looking a hierarchy or debugging.
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.
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.
Also debuggers are scriptable. Instead of just single stepping like a maniac and trying to keep everything in your head, you can essentially just write a script that debugs the app for you. Eventually if you do this enough times you have a pretty good library of scripts and debugging is a breeze.
Utterly backwards, as those who've tried it in practice know. If you attempt to program in a functional way you replace mutation of values with e.g. State monad constructs. And you go from having inspectable, debuggable state changes (you can breakpoint the point in code where a value is modified, and this will correspond to the point where it's actually modified, and you can inspect the changes in your debugger) to having state changes that happen invisibly, implicitly, later, when a function is actually called. A closure carries just as much state as an object, but it carries it implicitly, invisibly, undebuggably.
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.
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.
It's interesting that author is avoiding talking about the Actor Model which has an obvious overlap with the mainstream OOP in terms of stateful objects communicating with messages yet is being embraced by functional developers (Akka, Erlang).
Interestingly Erlang creators saw the problem and solved in a practical way a while back. It was funny in that they didn't go read academic literature, they hadn't even heard about the "actor model". They just realized that they needed to minimize large mutable states in concurrent programs. Variables are single assignment, data is immutable. One object (actor) crashing doesn't impact others. Then eventually later someone said "hey that's the actor model you are using".
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
---
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
---
Very true, also Erlang is different from pure Actor Model AFAIK because it provides certain guarantees that messages will be ordered, unlike:
http://en.wikipedia.org/wiki/Actor_model#No_requirement_on_o...
http://en.wikipedia.org/wiki/Actor_model#No_requirement_on_o...
I've heard it put that Erlang was created to solve the issues of distributed system, and as a result, solved concurrency for free.
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.
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.
An actor in Erlang is not an object, but a (usually recursive) function where receive is called in each recursion. It's stateful in the sense that Haskell functions are stateful: you pass a new state as an argument to the next function.
Actors and objects are closely related. Erlang processes provide encapsulation, the indefinite recursion allows maintaining local state, and dynamic dispatch is provided directly by the programmer or by behaviors.
> An actor in Erlang is not an object
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.
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.
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.
"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.
"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.
Same same, but different.
To the OP,please to stop trying to make general points about OOP,because I wonder what language you are talking about.
> The technical term for a programming paradigm which uses neither inheritance nor instance variables is imperative (or functional) programming.
what ?!?
> The technical term for a programming paradigm which uses neither inheritance nor instance variables is imperative (or functional) programming.
what ?!?
The technical term for that paradigm, in my day, was structured programming (because it used block structure, rather than gotos, to control the flow of execution). It meant C or Algol, not Lisp or Haskell.
Yes, any system where you're not in control of the main loop becomes hard to debug very quickly. Flow of control should be tree-like whenever possible.
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:
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.Small things that I noticed hurt debugability a lot:
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").
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").
> If you believe (as I do) that the majority of effort a programmer expends is devoted to finding and fixing bugs
I think if you feel this way, you should definitely look for languages and tools that don't admit as many bugs.
I think if you feel this way, you should definitely look for languages and tools that don't admit as many bugs.
Or find a way to exert and capture more thinking ahead of time. Language may help. May. Similar for tooling. (If you are convinced that they will both automatically help, consider that you can't just give them to a 1 year old and get results.)
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.
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.
This is the voice of the experience talking.
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.
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.
Very true. The majority of my efforts these days are adjusting to alternating specifications, external dependencies and debugging JavaScript.
Sadly, functional languages with lazy evaluation and data pipelines are harder to debug as the stack has far less meaning. At least with N layers of indirection in OOP you can see all the layers in the stack trace.
Well Smalltalk was designed to be easily mastered by one individual so its browser can look the entire hierarchy in two clicks and list the senders or implementors of any method you want in one keystroke. Not to mention that its debugger can restart the stack at any point, change code, recompile it on hot and continue in the same thread.
So, I guess yeah, if you don't have great tools anything is "so hard to debug"
So, I guess yeah, if you don't have great tools anything is "so hard to debug"
This was a terribly written piece. It is terribly long winded, when what it was trying to say could have been said much better in a few short paragraphs. I don't think it is possible to take seriously the words of anyone who thinks the "art of programming" is the art of debugging programs. That's like thinking the art of astronomy is about cleaning telescopes.
On a related subject, React has been designed to make tracing up bugs easy via various techniques explained on those slides: https://speakerdeck.com/vjeux/why-does-react-scale-jsconf-20...
You are on the right path, dear author, but you are still not at the point of understanding debugging. One huge part of debugging is avoiding bugs, and because this is often done poorly by the average coder, the next big part is understanding what a piece of code does and specify what it should do. Before you can fix a bug, you need to know what's wrong, not just where it happens.
Also, always using inheritance is bad, but so is never using inheritance. (Does that need explanation?)
Also, always using inheritance is bad, but so is never using inheritance. (Does that need explanation?)
Alternately: http://c2.com/cgi/wiki?ForgetTheDebugger
Many misleading statements here (paraphrased):
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.
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.
lately in clojure i find closures incredibly difficult to debug. Not having a step debugger is of course very annoying but the root problem is the same - state is captured which means you have to find wherever the closure was generated and recreate that flow...
Yeah a step debugger is a must. I miss it in http://amber-lang.net/
Wouldn't it be easier to debug your classes not objects ?;)
It's a bit difficult to test an instance method at the class level. Once you've instantiated an object from a class, now you're debuggin' with objects.
Depends. There are systems where everything is an object. Including classes (and if you ask what's the class of the class object the answer is Metaclass).
In those systems, you technically always work on objects.
In those systems, you technically always work on objects.
Well, technically you're still debugging the class, not the object, you're just using the instantiated object to do it. :)
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.