10 ms·
>In good OO programming, we don’t make class hierarchies in order to satisfy our inner Linnaeus. This correctly identifies one of the worst problems with how p
by colllectorof 7y ago
>In good OO programming, we don’t make class hierarchies in order to satisfy our inner Linnaeus.
This correctly identifies one of the worst problems with how people construct OO programs when they get out of college. They are infected with the idiotic desire to make neat taxonomies. They don't know why. They can't even explain the rules that make one taxonomy good and another one bad. They just feel some immense pressure to arrange things in trees. I've been like that when initially learning Java and it took a while to recover from it. It's like fucking brain damage from bad examples.
Inheritance is not about creating taxonomies.
Namespaces are not about creating taxonomies.
Both are practical solutions to practical problems that occur when writing code.
Inheritance is a result of several insights:
1. It's convenient to be able to sketch out a protocol which a set of objects will follow - without having to work out every damn detail about its implementation.
2. Most of the protocol can be implemented once and then reused.
3. Often you want to use the default protocol implementation with some changes. Instead of re-implementing the rest of the protocol, it's highly convenient to be able to specify only the differences.
#3 is the core reason why inheritance was developed.
- hota_mazi 7y agoWell said. And #3 is also an area where Functional Programming falls completely flat. There is simply no easy way in any FP language that I know to capture such a simple mechanism as: "Reuse 90% of this piece of code but alter just this 10%". Specialization is a very, very powerful aspect of OOP, one that's extremely easy to explain and which comes in handy to model a surprisingly wide variety of problems.
- colllectorof 7y agoFunctional programmers will reply that you can use partial evaluation and higher-order functions to do that. The sad reality is that since so many people are only familiar with the strawman version of OOP rather than the real thing intelligently discussing the advantages and disadvantages of these paradigms is very hard.
- hota_mazi 7y agoYes, I've heard them say that indeed. They often gloss over a lot of issues about these. For example, partial evaluation is limited by the fact that parameters need to be ordered a certain way and you can't realistically cover all combinations. Higher order functions don't enable any kind of useful specialization at all. And of course, all modern OOP languages also support higher order functions, so they are the ones giving you more tools.
- tome 7y agoCould you give some concrete examples? At the moment it's just assertions.
- nitrogen 7y agoDo any functional languages support partial evaluation using named parameters? That would solve the parameter ordering complaint. As for higher order functions you would write a function that does the 90% and accepts a function parameter for the other 10%.
- kragen 7y agoOCaml has named (“labeled”) parameters, and of course you can use combinators like flip to bind positional parameters like the second or third. I'm not sure what the relevance to inheritance is, though, and “partial evaluation” usually means an optimization strategy rather than just currying.
- catalogia 7y agoRacket's curry supports keyword arguments. https://docs.racket-lang.org/reference/procedures.html#%28def._%28%28lib._racket%2Ffunction..rkt%29._curry%29%29 https://docs.racket-lang.org/reference/procedures.html#%28de... (god links into the racket docs are ugly...)
- hota_mazi 7y ago> As for higher order functions you would write a function that does the 90% and accepts a function parameter for the other 10%. Surely you see how this doesn't scale, right? You would need to first write your code, then abstract every single piece of it as a call to a function passed in parameters. Now your function is accepting five different functions in parameters, and calls them. This is a terrible and impossible to scale way to emulate class specialization.
- CBLT 7y ago>There is simply no easy way in any FP language that I know to capture such a simple mechanism as: "Reuse 90% of this piece of code but alter just this 10%". Honest question: why does the Common Lisp Object System not do this for you?
- hota_mazi 7y agoHonest answer: you have a point :-) Yes, CLOS offers all of that. I just didn't quite consider it to be in the bucket of FP languages I had in mind (which to me means: statically typed, and some derivative of Haskell or OCaml).
- xyzzyz 7y agoAnd #3 is also an area where Functional Programming falls completely flat. There is simply no easy way in any FP language that I know to capture such a simple mechanism as: "Reuse 90% of this piece of code but alter just this 10%". Haskell’s typeclasses and OCaml modules solve this problem neatly. It is not as easy as in Java, because you need to spend a few minutes thinking how to abstractly specify the interface you expect, but really if your object hierarchy is supposed to satisfy Liskov’s substitution principle, you need to do that just the same.
- hota_mazi 7y ago> Haskell’s typeclasses and OCaml modules solve this problem neatly. Neither do, really. Imagine you have an existing typeclass with three functions. How do you create your own typeclass that reuses two of these three functions but your own instance reimplements the third one? Try it. It's pretty much impossible, or at least not without a lot of boilerplate and forwarding.
- xyzzyz 7y agoNot sure what you mean, exactly. Could you sketch a simple example with some types and typeclasses?
- callmeal 7y agoThis is how it would be done in Java: class Parent { public void first() {}; public void second() {}; public void third() {}; } class Child extends Parent { @Override public void third() {}; //change implementation of third function, reusing the first two. }
- mrkeen 7y agothird = first + second
- Hermitian909 7y agoI would accomplish this in modules in the following way. I use reasonML syntax but it's OCaml under the hood module Parent { let first = () => (); let second = () => (); let third = () => (); }; module Child { include Parent; //add parent funcs to namespace //@Override let third = () => (); }; I make regular use of this pattern production.
- deleted 7y ago[deleted]
- gigatexal 7y agoGive me a new function that is a partial of some other function where I can define the 10% to change than any of the arcane polymorphism magic.
- Mathnerd314 7y agoIt doesn't seem hard to add a parameter and then make two stubs: complex_function small_part = do a <- complex_thing_a b <- small_part a other_variables_in_scope more_complex thing original = complex_function orig_version mine = complex_function my_version There are some issues if you have a lot of parameters where you might want a record to pass them around, but specialization is easier to use than inheritance in all the cases I've seen.
- dwohnitmok 7y agoI actually think FP languages I know of have this functionality, although it's used to differing degrees in each ecosystem. ML modules have modules and functors, which can exactly mimic this and in fact can be even more flexible because they are structurally rather than nominally typed. Clojure can get some of this via multimethods, but it's not quite the same thing. Haskell has Backpack. This is fairly new, but takes an approach similar to ML modules. (Interestingly enough to your point elsewhere in this thread, this is a good example of how type classes were not sufficient for this use case). Scala has it, but that's cheating :). Although it does have a really cool thing which is that every object instance is itself an importable package, which leads to some really nice code reuse at times. Stepping back, I actually think "Reuse 90% of this piece of code but alter just this 10%" is actually best captured just with splitting things up into functions/procedures in imperative and functional programming languages, rather than building a large class hierarchy, but that's another story altogether. EDIT: I suppose what you're talking about specifically is overriding an already implemented method, which you can get via module name shadowing and re-exporting of a parent module as Hermitian909 points out elsewhere.
- voidhorse 7y agoHaskell’s typeclasses[1] support this use case—though they are oftentimes a bit clukier to use than OOP approaches. [1]: https://en.m.wikipedia.org/wiki/Type_class https://en.m.wikipedia.org/wiki/Type_class
- camgunz 7y agoIsn't this just composition? Function A calls B, C, and D. You make a new function E that calls B, C and F. Or higher order functions like map?
- unexaminedlife 7y agoWhen I stopped thinking about code and data as being inherently intertwined it liberated me as both an OOP and Functional programmer.
- wowthisanacct 7y agohttps://news.ycombinator.com/item?id=21304512 https://news.ycombinator.com/item?id=21304512
- hinkley 7y agoWe spend so much time in college learning about tree structures that hash tables are almost an exception. Which is weird because I use hash tables every single day. I also didn’t learn about the Liskov Substitution Principle in college or in my first few years in an OOP. Had to find it on my own and start teaching others. But I was in good company because Josh Bloch also didn’t understand it and a whole generation of devs aped his solutions. People think of inheritance as additive but if you look at the code they write it’s often subtractive. Mammal is not a contract. Every fork in the tree is carving out more negative space than it adds. Mammal is really a pretty terrible root. Some mammals are nocturnal. Some fly. One lays eggs. A few have no hair and we think hairy humans are gross. Some can hold their breath for hours and live four hundred years. Others measure life in days. But not a single one is all of these things. If we talk about what mammals can do we include it all. But you can’t ask these things of most of them. You don’t even organize them by these traits (what’s in the tropical or night exhibit at a large zoo? Why are the seals and penguins together?)
- postsantum 7y agoMammal interface is fine interface Mammal { fun milk() }
- hinkley 7y agoI’m a mammal, Greg. Can you milk me?
- postsantum 7y agoDepends on your species, Steve https://en.wikipedia.org/wiki/Male_lactation https://en.wikipedia.org/wiki/Male_lactation
- danielg6 7y agoIt was a quote from Meet the Parents lol.
- twblalock 7y agoThese days I find composition via dependency injection (of interfaces, not concrete implementations) to be a much better way to solve #3.
- caseymarquis 7y agoI would argue you should inject concrete implementations until you need an interface. Why add unnecessary indirection?
- mrkeen 7y agoIt lets you constrain your logic code to depend as loosely as possible on its dependencies. If I start from a concrete 'FooDatabase' (and wait until later to interface it), I might start putting things like open(), close(), flush(), etc. into my logic code. Then when I interface it, it will just be unnecessary indirection. If I start from the other end and think "I just need somewhere to put my Foos", then I'll pass in a Consumer<Foo> and be done with it.
- tome 7y ago> 3. Often you want to use the default protocol implementation with some changes. Instead of re-implementing the rest of the protocol, it's highly convenient to be able to specify only the differences. > #3 is the core reason why inheritance was developed. No, absolutely not. As sibling comments say, composition is sufficient for that. I can only see exactly one reason why inheritance is necessary: to allow superclasses to call methods defined in subclasses, also known as late binding.
- colllectorof 7y agoFirstly, late binding is orthogonal to inheritance. It simply means that methods are looked up based on runtime information, rather than determined at compile time. You can have late binding in a language with no inheritance or classes at all. Secondly, OO composition does not solve the problem I described in #3. If object X has 9 methods I want to reuse in object Y, making X a component of Y still requires me to re-implement those 9 methods in Y, even if they are just pass-throughs.
- couchand 7y ago> If object X has 9 methods I want to reuse in object Y, making X a component of Y still requires me to re-implement those 9 methods in Y, even if they are just pass-throughs. That is also not universal. In Ruby, for instance, one can write: class Y def method_missing(m, *args, &block) @x.send(m, *args, &block) end end
- kragen 7y agoI'm not sure why inheritance was developed. I had thought it happened in Smalltalk between 1971 and 1976; in Smalltalk-76 http://worrydream.com/refs/Ingalls%20-%20The%20Smalltalk-76%20Programming%20System.pdf http://worrydream.com/refs/Ingalls%20-%20The%20Smalltalk-76%... it's justified as follows: "This capability leads to a highly factored system." The first example given is that Window has subclasses such as a text editor for the source code of a class. (This was before per-method editing.) A later example is Number, which provides many comparisons such as ≤ and ≠ in terms of a smaller number of basic comparisons. However, it turns out it was in SIMULA 67, taken from a 1965 proposal by Tony Hoare for record handling in Algol, which I haven't been able to find yet. That is, it was proposed in a context where objects had fields, but not methods, and thus not protocols either. SIMULA 67 had overriding of superclass methods if they were marked “virtual”, as in C++. Turning back to Window and Number, an alternative using composition instead of inheritance puts the shared and unshared code in different objects. The Window class becomes one class only, with a field indicating what its contents are, to which it delegates paint messages and handling of input events; Number becomes a wrapper that expects its contents to implement < and ==, perhaps, and implements the other four methods on top of them. How does this differ from the approach using inheritance? It's a great deal more hassle to change your mind about which methods are delegated to the wrapped object, but much easier to be sure that other refactorings are correct, because it's much easier to tell which methods could potentially be “overridden by a subclass”. It affords the possibility of changing the contents of a Window over time—particularly useful in languages like Python where reloading code after a modification creates new classes rather than modifying the existing ones. It costs an extra allocation and an extra method call on every delegated method (fatal in the case of SmallInteger, normally a subclass of Number, but SmallInteger is already a collection of hacks for efficiency; giving it an independent implementation of the Number protocol is reasonable). Perhaps we should regard inheritance as an efficiency and convenience hack for cases where memory is tight or we're initially exploring an OO design, one we should remove later to improve maintainability once it's more or less clear how to divide up the responsibilities. Smalltalk-80 used inheritance for its collections library, in a way that does not respect LSP, perhaps understandably because Barbara Liskov was on the other coast, and CLU was a very different language from Smalltalk. Some more recent languages like Java modeled their collections after Smalltalk’s. I don't think it's entirely fair to ding Josh Bloch for this. Rigorously modeling inheritance (with overriding, open recursion, and covariant self type) turns out to be quite challenging; I recommend Abadí and Cardelli's A Theory of Objects to those who are interested. The most interesting thing I'm looking at right now with regard to software design is Jackson's Alloy model checker, which does a kind of abstract-interpretation exhaustive test of your high-level design to verify that in examples to a certain size, your desired properties hold. This is of course a different level of abstraction from the factoring of the implementation to eliminate duplication, but it can tell you which contemplated protocols are fatally flawed.
- balabaster 7y agoTeaching your points 1, 2 and 3 seem to be overlooked in many courses. It's as if they know that you have to do these things, but they don't know why. As if "Oh, you have to do this because polymorphism." I think if they took a single lecture to teach these specific points of OO, we'd get more effective developers straight out of school.
- jhbadger 7y agoI disagree. The whole reason Linnaeus created taxonomies was to simplify what had to be expressed about living things, so basically to shorten the "mental code" involved. If you know something is true of all mammals, and humans are mammals, then everything that is true of all mammals is also true of all humans (but the converse isn't true; things can be true of humans that aren't true of all mammals). Of course, later on taxonomies had another purpose; Darwin and later thinkers realized that taxonomies often correspond to the actual historical evolutionary relationship between organisms. But Linnaeus himself had no evolutionary beliefs.
- chacham15 7y ago> Inheritance is a result of several insights: > 1. It's convenient to be able to sketch out a protocol which a set of objects will follow - without having to work out every damn detail about its implementation. > 2. Most of the protocol can be implemented once and then reused. > 3. Often you want to use the default protocol implementation with some changes. Instead of re-implementing the rest of the protocol, it's highly convenient to be able to specify only the differences. > #3 is the core reason why inheritance was developed. Those insights lead to what was taught in my intro to cs class: abstraction. Inheritance can be used to implement abstraction, but so can many other concepts such as duck typing or aspect oriented programming.
- userbinator 7y agoThe most important phrase in that post was this: We make class hierarchies in order to simplify the code In fact, much of the abstractions of programming were invented for this reason, and it's unfortunate that a lot of courses don't really teach the why. They tend to start off on the right track with something like "use a loop to repeat a sequence of operations X times", but then get caught up in preaching about abstraction with functions when it's far more intuitive and sensical to approach it from the perspective of "repeat a sequence of operations, with perhaps some small differences in parameters". For the same reason, I think OOP should be taught only after basic arrays (grouping homogenous data together), structures (grouping heterogenous data together) and functions, since OOP is really just another level of organisation that can --- not must, unlike what a lot of people seem to believe --- be applied to code once it becomes complex enough. The dogmatic attitude towards OOP and treating it like a goal leads to ridiculous explosions in complexity for even the simplest of applications; what can be done by someone who hasn't been indoctrinated in a few lines of code can turn into hundreds of lines instantiating dozens of objects. "Design patterns" helps to amplify that too.