The role of inheritance in Javascript's future

Thanks @anuragvohraec for your point of view :)

While I myself tend to lean my code in a more functional slant (though it's very far from the purist ideology of it), I don't have anything against OOP or classes. Yes, I'm attacking one of the four pillars of OOP, but I'm not really attacking OOP itself - OOP often stands on just three pillars in some newer OOP languages that don't provide facilities to do inheritance.

So, let me see if I can discuss some of the points you brought up.

  1. Real world objects can be easily mapped to objects and categorized with help of Inheritance.

Ah, categorization. I'm going to talk about the whole "is-a" vs "has-a" relationship idea here, as that's a common metric used to decide how to categorize something (if it's is-a, use inheritance, otherwise don't). Let's put this concept under a stress test with a commonly cited classical inheritance problem. You're building a payroll application. Salaried employees will be paid very differently from hourly employees, so you ask yourself, Is a salaried employee an employee? Is an hourly employee an employee? Why yes! Inheritance it is! But wait ... they also receive different wages depending on their job title. Is a back-end developer an employee? Is a front-end dev an employee? Why yes they are! Multiple inheritance it is!

Alright, so we'll end up with a nice inheritance hierarchy like this (the mixin pattern is being used to simulate multiple inheritance, because Javascript doesn't natively support it):

class FrontEndSalariedEmployee extends mixin(FrontEndEmployee, SalariedEmployee) { ... }
class BackEndSalariedEmployee extends mixin(BackEndEmployee, SalariedEmployee) { ... }
class DevOpsSalariedEmployee extends mixin(BackEndEmployee, SalariedEmployee) { ... }
// ...

class FrontEndHourlyEmployee extends mixin(FrontEndEmployee, HourlyEmployee) { ... }
class BackEndHourlyEmployee extends mixin(BackEndEmployee, HourlyEmployee) { ... }
class DevOpsHourlyEmployee extends mixin(BackEndEmployee, HourlyEmployee) { ... }
// ...

new FrontEndEmployee()

Hopefully, this company doesn't have too many job titles.

But... perhaps there's another way to formulate this problem.

Does an employee have a salary, or an hourly payment type? Does the employee have a front-end or back-end job title? Why... yes they do! So, we could also model this problem with composition instead (in this case, I'm specifically using the strategy pattern).

const hourlyPaymentType = { ... }
const salaryPaymentType = { ... }
// ...

const frontEndPosition = { ... }
const backEndPosition = { ... }
const DevOpsPosition = { ... }
// ...

new Employee({
  paymentType: hourlyPaymentType,
  position: frontEndPosition,
})

Wow that's simpler, and much more flexible as well!

The thing is, you can take any is-a relationship and rephrase it so it sounds more like a has-a relationship. Perhaps there are times when inheritance is the right choice, and using it make the overall structure of your codebase nicer, but I wouldn't automatically choose it simply because at first glance, it falls into an "is-a" category, because, perhaps placing it into the has-a category instead can lead to a nicer-looking codebase. Perhaps this is the reason why the phrase "favor composition over inheritance" gets thrown around, because the is-a vs has-a thing doesn't do a great job of letting you know when inheritance should be used.

Anyways, I guess all this means is, if we're going to use inheritance, we've got to understand what problem it's trying to solve and why the codebase needs it. We can't just reach for it because "it's the thing to do", because we have an is-a relationship.

(aside: I assume you probably wouldn't have actually modeled this problem using inheritance for each employee position, because it makes for multiple inheritance. The point is, it could be done, the only reason we don't is because we look for reasons outside of is-a vs has-a to make these decisions. Which is exactly my point - is-a vs has-a doesn't do a great job at making these decisions for us.)

OK, what's next...

  1. To achieve modularity and Inversion of control.

Perhaps this point was only meant to help explain why classes are useful? If so, I wholeheartedly agree that classes can be used to achieve modularity and inversion of control. If you intended to use this point to explain a pro of inheritance, feel free to expound and show why inheritance is useful for achieving modularity and/or inversion of control. I can guess at how inheritance would fit into it, but I'm scared I'd end up rambling about something we both agree on anyways.

  1. Easy testing : I can create dummy objects and push them for testing, without touching my original code. There are libs which can automate that for me.

Honestly, a lot of "good practices" get changed when you go into testing mode. For example, you're no longer supposed to keep your code DRY, instead, it's good to repeat yourself all over the place. If you want to use inheritance in a testing environment, I'm good with that.

However, I don't think I've ever seen inheritance get used to create mocks. The popular mocking libraries I've seen, like Sinon, usually just monkey-patch your objects, then restore them when testing is over.

  1. A way of combining related functions and variable under one roof : Classes and their objects. All functions and states belonging to a Class Boy are encapsulated in One object. There is no way he is going to behave other wise ( a better control in other words). A splattered states and function modfiying them across different files make it very hard for me to understand whats and where and how things are happening in my code. I can very conveniently convert real-world problems into Classes and Objects. And then create different versions of them with inheritance.

This sounds like it's mostly an argument for classes, which again, I have no problem with.

I will point out that there are ways to achieve the end goal of having "different versions of a class" without using inheritance. For example, you can create a single class and customize its behaviors using the strategy pattern. Or, you can create an interface (if you're using typescript) and create independent classes that all implement that interface. In fact, there's very few scenarios I'm aware of where inheritance (or Go lang's embedding feature) really becomes the only good way to achieve a desired effect.

  1. Inheritance help me precisely change my object behaviour and help me trace who has changed its properties. In a FP programming any one can attach properties to an object and it will be really hard to conclude which portion of the code did it. While in inheritance a particular behaviour is attached to an object just because a particular type of instance was passed in.

I think my feedback for this one is pretty similar to the previous one. Inheritance can be used to solve this problem, but so can other techniques.


Overall, I think my point is, in general it's considered good practice to favor composition over inheritance, and if you look around, there's actually a lot of ways to achieve a desired effect using various composition techniques. If we want, we can take a look at specific concrete examples where inheritance is usually cited as a "good thing", then try to model it without inheritance and see if the non-inheritance version is just as good if not better. I've already deconstructed the classic salary vs hourly employee example, which is normally cited as a shining example of where inheritance should be used, and showed that the same effect can be done in a simpler way via the strategy pattern (granted, I did purposely overcomplicate it with multiple inheritance for the purposes of talking about is-a vs has-a, but even with single inheritance, I would still prefer the strategy pattern there).

It sounds like your view of FP is simply "OOP, but without classes". Honestly, that's probably anyone's view of a different paradigm before they get comfortable with it. While you're trying to use a new paradigm, all you'll ever think about is "But how do you do X"? Unfortunately, it's not until after you've used it for a while that you begin to understand its strengths, and then, going back to your original paradigm you'll realize that it too lacks some pretty important features.

And, actually, if you look closely at FP, you'd find many of these "missing features" in there too - modularity, testability, encapsalation, polymorhpism, etc. They just come in different forms and flavors that you might not recognize at first.

I want to correct myself on some of the terminology I've been using in this thread.

First of all, I kept calling instanceof checks polymorphic, but I don't think it really is. In JavaScript, you get polymorphism simply via duck-typing. Still, instanceof publicly exposes the fact that you had used inheritance, which I view as a downside. If, in JavaScript, inheritance is primarily a tool for code reuse, then why does the tool of choice we use to simplify our code need to be publicly exposed? And, if we're using a language like TypeScript where polymorphism really does come into play, we can always use interfaces to achieve it. If we want to provide users with a functional equivalent to instanceof, we can just provide a doesThisObjImplementMyInterface function, then embed a unique sentinel into any class that claims to implement this interface. It relies on trust, but so does inheritance in general - we are always trusting that subclasses don't break the Liskov Substitution principle. I believe it would be nice if the protocol proposal could help streamline this sort of thing, as discussed in this previously mentioned issue, but it's not necessarily required.

Next, I don't think Go's embedding feature did quite what I imagined it did. Go only has structs, not classes, so "embedding" just merges data properties into a single struct, it does not also merge methods.

So, I've done a lot more researching and playing around with ideas, and I've come to the conclusion that, every once in a long while, a very restricted form of inheritance is ok to have. Let me try and present my thoughts.


As I've talked about previously, there's two main parts to inheritance: code reuse and polymorphism, or in the case of an untyped language like JavaScript, code reuse and "creating a conceptual, publicly exposed hierarchy". What happens if we split inheritance in half, and try to handle each side of the tool separately? I already explained above how to create conceptual relationships without inheritance, we just need to expose functions that tell you if the relationship exists. In the case of the Map and ReadonlyMap topic, this would simply mean having a Map.isMap(), ReadonlyMap.isReadonlyMap(), and AbstractMap.isAbstractMap(), where the isAbstractMap function would return true if it receives either a Map or a ReadonlyMap. If at any point we wanted to add a new map type that contained a different subset of functions of Map, we could, and we could add a new abstract class for it, with another isWhatever() function to go with it. We're not bound by a single hierarchy anymore, we can make as many of these conceptual relationships that we want to make, with as many of these isWhatever() functions.

This is just one way to go. There's certainly a number of ways to get this effect, but the important thing is that we're creating a public relationships, without forcing ourselves into awkward inheritance relationships when the code-reuse side of inheritance is not needed at all. And, we're allowing users of these APIs to "program to an interface" - if something passes the AbstractThing.isAbstractThing check, then it conforms to some abstract idea of an interface, and users can program to it.

Hopefully it should be possible to see that this can entirely replace the "conceptual, public hierarchy" side of inheritance. There's nothing in addition that inheritance offers here that can't be done via a tool like private sentinels, but private sentinels give us additional flexibility and interface-like behaviors that inheritance withholds from us.

Now onto code reuse. I think there are plenty of good ways out there to achieve code reuse. Just because something is-a something else, does not mean that inheritance is the best way to re-use the code. It's why phrases like "favor composition over inheritance" exist - because it's easier to follow what's going on when you use composition and explicitly define each function.

There's plenty of ways to extract common, repeated logic out to make it more reusable, but they all technically fall short of the power of inheritance. Inheritance is capable of providing many classes with a whole host of shared functions at once. Every other code-reuse solution out there requires you to explicitly define each function on each class (which isn't bad to do if you don't have many shared functions), or to change the shape of your API, for example, instead of inheriting a bunch of methods, you can just make a bunch of public helper functions that can operate on any of these classes.

The amount of times when your class grows large enough to cross the "composition is just too tedious" should be few and far between, but it still happens. When it does happen, then it can be appropriate to use inheritance. However, I think inheritance provides a lot more power than what we actually need to solve this problem. We just need to be able to "mix in" a bunch of shared functions. We don't really need other behaviors, such as method overloading (use the strategy pattern if you want to customize the way the base class behaves), public hierarchies (keep the fact that you're using inheritance as an internal detail, and rely on private sentinels to expose hierarchies), etc.

I actually made a nice little mnemonic for this idea :). These are the four P.A.I.L. rules of restricted inheritance.

  1. Inheritance is a private implementation detail. Use something else for your public hierarchies. This liberates you to use whatever code-reuse tool of your choice, while keeping the public side stable.
  2. Keep your base classes abstract.
  3. Isolate your base and child classes. Your base and child classes should be completely oblivious of the properties the other class provides. A child class should never overload a base class’s methods or even access a property inherited from the base class, and a base class should never expect a child class to provide a missing method implementation.
  4. Inheritance is a last resort.

These rules help one to avoid all of the major pitfalls of inheritance, e.g. there should never be a need for multiple levels of inheritance, so no deadly diamond of death. Fragile base class should also not be an issue, because you never publicly expose your base class, or use method overloading, which is what makes base classes fragile in the first place.

The third rule may seem extra restrictive, but it's actually not bad to follow if you mix in other code-reuse techniques. I won't go into too much detail about this, but if anyone cares, here's a gist that shows how someone might use inheritance in its full, unrestricted form, with method overloading and everything, and here's pretty much the same implementation, redone to show one possible way to follow these restrictions.

When dealing with highly-used libraries, where extra API stability is required, you may have to follow a mix-in approach instead of using direct inheritance, so that no one can find your base class by doing Object.getPrototypeOf().

Anyways, this is the current conclusion I have come to. I have put all of my thoughts around this into a more in-depth article over here. Feel free to agree or disagree with the above thoughts, especially around those P.A.I.L. restrictions - if you're able to think of any concrete instance where those restrictions might make for more harm than good, I would love to hear about it. And, perhaps others might not agree with all of those restrictions, and would rather follow their own set, which is fine. Probably the most important restriction among them, for the purposes of this conversation, is the fact that we're keeping the inheritance relationship private, and we are using private sentinels (or whatever else) in order publicly expose relationship details. In general, I feel this is a good practice to follow, and could see benefit if the language itself started to go this direction as well.

You might want to look at Pony programming language; which is a modern OO language.
It provides an alternative way to look at OOP than our conventional perspective.

As I've talked about previously, there's two main parts to inheritance: code reuse and polymorphism

but most javascript-code in web-development is NON-REUSABLE glue-code to serialize and mesage-pass dynamic data between external endpoints.

the few reusable stuff are typically small static-functions to help serialize or transform data while its in-transit between these endpoints, and NOT instance-bound class-methods.

Here, @kaizhu256, later today when I have time, I'm going to go ahead and open up a dedicated thread to have a friendly discussion around your message-passing idea in more depth (unless you want to beat me to it). I can link this thread to it for anyone who's interested. That way, we can have a dedicated place to go in-depth into this idea, without dragging other threads too far in awkward directions.

Not that it's off-topic for this thread - certainly if you're correct, then it will heavily influence the conversation of this thread, which is why I'll link this thread to it. But, I think such a conversation will need a little more space than what this thread can afford.

1 Like

Most JavaScript code in web development is entirely reusable, which is why the js ecosystem has always been full of such reusable browser-only libraries.

you mean like GitHub - EnterpriseQualityCoding/FizzBuzzEnterpriseEdition: FizzBuzz Enterprise Edition is a no-nonsense implementation of FizzBuzz made by serious businessmen for serious business purposes.?

i would argue js ecosytem is full of "resuable" code because ppl keep trying to shoehorn "resuable" stuff into non-reusable scenarios -- resulting in more "reusable" code for the shoehorn -- rather than the underlying ux-problem.

I'll make a note to add this to the new thread when I create it, so we can further discuss reusable code there as well.

Here's a link to the dedicated thread to discuss these ideas.

Classes are great, and they're not going anywhere.

JavaScript does "natively" support it in a sense, because that code example you wrote (that I slightly modified) actually works. We can implement mixin using Proxy and Reflect to create a "prototype tree". An implementation is here:

See the sibling multiple.test.ts file for unit tests.

The benefit of this is that you can import any plain class from anywhere and extend from it, without it being required to be class-factory mixin function.

multiple even works with builtins and Custom Elements (see tests).

class MyEl extends multiple(HTMLElement, SomethingElse) {...}

What if, syntax wise, we had something like class Foo extends One, Two, Three {}? Then it would be native. We would need to iron out all the semantics.

There's a proposal called maximally minimal mixins

I've previously posted an idea on Reflect.mixin(), with a minimal implementation. You can check it out if you want!

There isn't much interest in TC39 regarding mixins. The above mentioned proposal is inactive for quite some time :(

Oh, that's pretty interesting that you were able to come up with a way to accomplish this. So, I guess it is natively possible to accomplish multiple inheritance.

class FrontEndSalariedEmployee extends multiple(FrontEndEmployee, SalariedEmployee) {
test() {
super.foo()
super.bar()
}
}

what if, as it turns out:

  1. the most common-task in javascript is not performing business-logic on FrontEndSalariedEmployee
  2. but instead, its message-passing it over-the-wire to / from external endpoints (e.g. dom / cookie-storage / server)?

its an open-question whether the above conjecture is true (i obviously believe it is), and if so, it would be more efficient for the 2nd scenario, to represent FrontEndSalariedEmployee as an easy-to-serialize "struct", and create disposable, static-functions whenever an uncommon, business-logic use-case arises:

function disposableFunctionFoo(frontEndEmployee) {
// throwaway-code to perform uncommon business-logic "foo"
...
}

function disposableFunctionBar(salariedEmployee) {
// throwaway-code to perform uncommon business-logic "bar"
...
}

// common-case message-pass "frontendSalaried" struct from server -> js
let data = await fetch("/api/front-end-salaried-employee?id=1234");
data = await data.json();

// common-case message-pass "frontendSalaried" struct from js -> dom
document.querySelector("input.foo").value = disposableFunctionFoo(data);

// common-case message-pass "frontendSalaried" struct from js -> dom
document.querySelector("input.bar").value = disposableFunctionBar(data);
1 Like

That's true. Use cases use cases.

Three.js solves this by having toJSON methods in every class.

Another solution could be a smarter JSON2.stringify function that skips trying to serialize anything that can't be. But even with this option, if the factory approach uses prototypes, how will a round trip know how to re-structure the prototype chain? Three.js from/toJSON handles this, mapping data to/from the class instances.

The only way to have separate static functions and for (de) serialization to always work, is to use concatenative inheritance, but then some things like method extension becomes difficult.

The only way to keep full benefits of inheritance while having a data API is having to/fromJSON or similar methods in all our classes, at the cost of more work to make those methods.

Six months later. Guess I've been too busy lately.

First, let me attack that employee example. Sure, the functional version is a lot more flexible than what you came up with, but it's also subject to becoming a web of tangled functions where 1 small change can cause the whole system to start behaving irrationally. Let's get something straight. FP is just procedural programming with a single extra constraint: keep it mathematical, i.e same input, same output, no storage. So FP suffers from all the same spaghetti code issues that lead developers down the class/module road.

So why inheritance? Well, as it turns out, much of the spaghetti was often caused by having multiple versions of the same function that did slightly different things under slightly different circumstances, or worse, a bunch of different functions doing nearly the exact same thing with different data that couldn't be easily refactored because of the structure the data came in. Even back then, we understood that DRY code is easier to maintain. That's part of what inheritance provides. When used properly, inheritance prevents the need to duplicate code.

Now let's look at that Employee example as I would do it:

class Currency;
class Schedule;

virtual class PayModel {
   ...
   public:
      virtual Currency getCheckAmount(Schedule sched) = 0;
}

class SalaryPay: public PayModel {
   ...
   public:
      virtual Curency getCheckAmount(Schedule sched) { ... }
}

class HourlyPay: public PayModel {
   ...
   public:
      virtual Curency getCheckAmount(Schedule sched) { ... }
}

class Position {
   private:
      PayModel wage;
   ...
}

class Employee {
   private:
      Position job;
   ...
}

For what was described, this covers it. The only place inheritance is desirable is in dealing with the pay model. Functions for calculating taxes would live in PayModel while other functions that necessarily need to operate differently would live in the respective derived classes.

The point here is that inheritance is just a tool. Of course you're going to cause yourself undo pain if you misuse or abuse it. That's just the nature of anything. And of course there are non-inheritance ways of handling this. However, why use a flathead screwdriver to turn a phillips head screw? Use the tool that's most convenient for the job. Just make sure you understand the job!

Much was said about other languages leaving out inheritance. That's all well and good. However, after having used such languages, I often found myself doing unsavory things like leaving public things I wish to keep secret. Why? Because the only other solution was to duplicate code, or worse, refactor and designate a resulting function as a "utility" function when it was actually code specifically used by a small collection of other related functions.

Modularity does help in this regard, but it also hurts as sometimes, you need to expose functions to the world just so you can call it from another module. But, especially in languages like ES, that just opens the target function up for anyone to use, which isn't always acceptable. Yes there are workarounds to even this, but those workarounds serve to do little more than increase the risk of spaghettification.

Here's a rewrite of your P.A.I.L. rules, because I agree that inheritance should be somewhat restricted.

  1. Polymorphism is as much your friend as a pet dog. Abuse it and it will bite back! Like any other tool, it has its good uses and its pitfalls. Use sparingly.
  2. Abstract your base classes as much as is reasonable. The only valid implemented functions in a base class are ones that can serve all derived classes without conditional customization.
  3. Inheritance is costly, but not as costly as wet code. Keep re-arranging the design to minimize the sum of these costs.
  4. Long functions is usually a good sign you screwed up at least one of the previous rules.

As for multiple inheritance, while I don't see it as a bad thing, I get why people have trouble with it. If you can't even get single inheritance right....

Sure, the functional version is a lot more flexible than what you came up with

I wouldn't call my alternative version functional at all. It's still utilizing classes, and it's using the gang of four's strategy design pattern (an principle from the OOP community), it's just not using inheritance. So, I would still classify it as very much an OOP solution.

Let's get something straight. FP is just procedural programming with a single extra constraint: keep it mathematical, i.e same input, same output, no storage.

That's one principle of functional programming, and certainly an important principle, probably the most famous one, but I wouldn't call that an accurate summary of the paradigm at all. It's missing lots of other important pieces like function composition, tacit programming, etc, all which are unrelated to function purity but are unique to functional programming, and which influence how the codebases are organized. A completely pure program that has no other quality of functional programming besides being pure I would categorize as "inspired by FP" or "leans on FP", but I wouldn't say it is FP, due to the complete lack of other important aspects of the paradigm.

Anyways, I digress, none of this is really about functional programming. As previously mentioned, I'm comparing two different OOP approaches to solving the same problem.

Well, as it turns out, much of the spaghetti was often caused by having multiple versions of the same function that did slightly different things under slightly different circumstances, or worse, a bunch of different functions doing nearly the exact same thing with different data that couldn't be easily refactored because of the structure the data came in.

Perhaps my strategy pattern example wasn't fleshed out enough to explain it's true power. This sort of duplication you're talking really isn't needed. Unless you're dealing with a handful of odd cases, you can avoid duplication just as easily with the strategy pattern.

Let me take your rewrite as an example. You're using inheritance here to avoid duplication, so that your base PayModel class can provide other methods (e.g. like a getTaxes() method), and all subclasses automatically get those exact same methods. And, you're stating that this is much better than the solution I proposed, because it can avoid duplication. But, let me now show how the strategy pattern would work in this scenario, and how it can achieve the same level of duplication removal.

interface PayBehavior {
  getCheckAmount(sched: Schedule): Currency;
}

class PayModel {
  this.#payBehavior;
  constructor(payBehavior: payBehavior) {
    this.#payBehavior = payBehavior;
  }

  getTaxes(sched: Schedule): Currency {
    const checkAmount = this.#payBehavior.getCheckAmount(sched);
    const taxes = /* ... some fancy logic ... */
    return taxes;
  }

  getCheckAmount(sched: Schedule): Currency {
    return this.#payBehavior.#getCheckAmount(sched);
  }
}

class SalaryPay implement PayBehavior {
  getCheckAmount(sched: Schedule): Currency {
    ...
  }
}

class HourlyPay: public PayModel {
  getCheckAmount(sched: Schedule): Currency {
    ...
  }
}

class Position {
  #wage: PayModel;
  ...
}

class Employee {
   #job: Position;
   ...
}

Notice how the getCheckAmount() method is able to exist in exactly one location? I don't have to copy-paste it around and slighly modify it, as you alluded that I would need to do. That's the whole point of the strategy pattern - you provide a single class with various methods, along with a handful of customization hooks that users can utilize when they construct an instance of your class.

Is it a little more verbose? Yeah, a bit. But not much. And, in most scenarios, I think the small verbosity cost is worth it.

Of course you're going to cause yourself undo pain if you misuse or abuse it.

One of the reasons why I believe inheritance gets mis-used so much, is that people just aren't made aware of the handful of alternative code-reuse options avaialble that can do similar jobs, but isn't as heavy as inheritance (or they do know about the alternatives but don't think about them when making decisions). In fact, I feel that the primary reason inheritance turns into a problem is simply the fact that it gets overused - people create codebases filled with sprawling inheritance hierarchies due to the fact that it's constantly being applied to problems that don't benefit much from it over the alternative options.

You talked about using two different screwdrivers, so I'll put forth this alternative analogy: With the inheritance chain-saw, everything looks like a tree that you can cut down. Until you learn of the other tools, like a hatchet or a butterknife, that are also able to do similar jobs but in a more effecient way. Yes, sometimes you need the chainsaw, but often you don't.

So, perhaps we're both looking at the same problem, and you feel inheritance is a better fit (it's not the "flat-head" screw-driver), and I feel that inheritance is overkill (you don't relly need the entirety of inheritance to solve a problem this small)

Here's a rewrite of your P.A.I.L. rules

Haha, that's great! Love it :). I think the world would be a better place if at least these rules were followed whenever inheritance was used. To discuss a handful of them:

  • "The only valid implemented functions in a base class are ones that can serve all derived classes without conditional customization." - I like that rule, I think that if it were followed, a fair amount of inheritance tanglement would be lost.
  • "Inheritance is costly, but not as costly as wet code." - I agree that if you really do start running into wet code, and alternatives to inheritance is not doing it for you, then you shouldn't feel ashamed to use inheritance.

The reason why I don't use those as a characterization of FP is because they're techniques. What's more, they're not unique to FP. They can be used in OOP as well. It's just not as obvious what the benefit of doing so would be.

There's something you're missing here. OOP and FP are not mutually exclusive. A lot of people get this wrong. OOP is all about how to structure and group your functions and data. FP says write your functions so you don't need to store data as much as possible, but doesn't say much about how to organize the resulting functions. So it's possible to use inheritance in a functional program. It's just that most choose to avoid the added complexity of mixing the 2 paradigms.

Did you know that you still used inheritance? You don't recognize it because it's hidden away behind a sneaky mechanism: interface. That same interface written in C++ looks like this:

class PayBehavior {
   virtual Currency getCheckAmount(Schedule sched) = 0;
}

which means SalaryPay and HourlyPay look like this:

class SalaryPay: public PayBehavior {
   virtual Currency getCheckAmount(Schedule sched) {
      ...
   }
}
class HourlyPay: public PayBehavior {
   virtual Currency getCheckAmount(Schedule sched) {
      ...
   }
}

While it is often presented as something special, an interface is just a pure virtual class. It still provides a vtable and function slots, just with them all set to null. It's still inheritance. You're still using polymorphism. You're just not providing an implementation at that level.

Now, is there anything wrong with doing things that way? No. Not at all. In some scenarios, it's even preferable to the way I did it. But the same could be said for how I did it. It just depends on what the design goals are, what constraints you're required to operate within, and what language tools you have available.

My point is this: Just because there are chainsaws that make short work of chopping down a tree does not mean that axes no longer have a good and valid use. You never needed the chainsaw to begin with. However, the fact that it makes short work of the tree is beneficial. It's all about knowing what the use cases and limitations of your available tools are.

That's half of it. The other half is that it gets used incorrectly, much like your naive FrontEndSalariedEmployee example. Seriously, that was utter abuse! :smile: Constantly taking the naive approach to inheritance, modifying in place instead of refactoring when changes are needed, and otherwise not following S.O.L.I.D. principles are the main reason for untenable code when it comes to inheritance. Even though inheritance can be used to reduce and reuse code, it still has to be designed properly to achieve that end. No matter what paradigm you use and what patterns you adhere to, if you don't choose the right tools for the job and actually put some thought into the design, you're just going to make a mess. Don't blame the tools. Blame the developer who misused and abused them.

The reason why I don't use those as a characterization of FP is because they're techniques. What's more, they're not unique to FP. They can be used in OOP as well. It's just not as obvious what the benefit of doing so would be.

I don't quite understand this argument. Isn't the use of writing pure functions a technique as well that can be used in OOP (and is, in fact, often encouraged in modern OOP)? I know from other conversations that you don't particularly like over-zealously using pure functions due to the performance issues, which is understandable, but it's still a principle that's often encouraged in any paradigm.

OOP and FP are not mutually exclusive.

This is often true - many parts of OOP and FP can be used together, however there weren't any functional techniques I can think of that I was using in that code sample, but I did use techniques that are associated with OOP. So, I stand by my classification of this piece of code as an "OOP solution".

I would also point out that some parts of OOP and FP don't mix. For example, many would say that when writing functional code you should organize your code around your behaviors - switch-like statements would be encouraged, as that actually allows you to keep everything related to a particular behavior in one place ("switch-like" meaning pattern-matching on a union/variant type and writing code for each piece of that union). This is in stark contrast to the OOP way of organizing logic around data and preferring polymorphism to handle these types of problems. For this reason, encapsulation matters much less in a functional program, since many functions are operating on many pieces of data at once. In like manner, inheritance makes much less sense if you're not organizing code around data. It's not that you never should use polymorphism in functional code, but that's simply not the preferred tactic to solve the problem.

In the end, there's really no set definition on what it means for something to be functional or object-oriented. But, if you hear someone talking about functional programming, and they actually know a thing or two about the paradigm, they're probably not just saying "pure functions", otherwise they would just use that word, instead they're probably referring to a large group of techniques and patterns that are often associated with this umbrella term. If you're hoping to shoot down the paradigm and show that it provides no value, the first thing you'll have to do is make sure you're using roughly the same definition that others use, otherwise you'll run into the same problem that most "OOP is bad" articles run into, which is that they use a particular definition of OOP that no one else accepts, or, they talk about an over-zelous adherence to the paradigm that no practical person does, then they shoot down this thing that no one was propping up to begin with.

In other words, if you want to claim that "procedural programming with pure logic is bad", I can sympathize with that argument. But, I've never seen anyone define functional programming like that, nor have I seen anyone write a functional program that looks like that. (Actually, I have seen a number of JavaScript developers who aren't that familiar with the paradigm write procedural code and call it functional, because JavaScript is "functional", and the code they wrote couldn't be OOP, they're not touching classes, and it's certainly not procedural, that's an icky word, so it must be functional, besides, their code does have some pure functions, they use array.map(), etc - perhaps you've ran into a number of developers like this, which is why you've come to your definition of "functional programming" - if you want to shoot this sort of thing down, I'm good with that).



Did you know that you still used inheritance?

If you want to call the use of interfaces "inheritance", fine, but it has certain restrictions that make it much more safe than normal inheritance.

In a similar vein, you might see people who use break and continue, but who denounce the use of label/goto, even though break/continue are practically restricted forms of label/goto. And it's completely fine to argue like that, because the restrictions placed on break/continue make those constructs much safer to use and much harder to abuse.

Seriously, that was utter abuse! :smile:

Haha, yeah I sure hope no one actually codes like that. But, its purpose was intended to show that blind adherence to is-a/has-a as a metric to decide when to use inheritance would produce garbage code (not necessarily that inheritance in-and-of-itself is garbage), so the fact that you agree that code is garbage means it did it's job :).


In the end, I think we're coming around to a similar perspective here. We both agree that inheritance is often overused and abused. You state that there is a time and place where it's appropriate to use inheritance, and I can agree with that. I'm sure I would recommend it's use far less often than you would, but even more important for me is that developers know the pros and code of using inheritance, and they're familiar with the alternatives, so they can make informed decisions instead of just using the tool they know how to use.

The only defining characterization of FP is that this paradigm prefers functions to be mathematical statements as much as possible. Almost anywhere you look, it's defined in terms of pure functions with no data sharing, mutability, or side-effects, i.e. mathematical functions. The other things you mis-characterized as being "unique to functional programming" simply aren't, and are not part of the defining characteristics of FP. My description may have been myopic from your perspective, but it was none-the-less accurate.

I would argue that instead of claiming that these don't mix, that they are instead different arrangements of the exact same technique. Where FP uses pattern matching to redirect the code flow, OOP done well generally isolates the behaviors related to a given pattern into an object and its methods. There's no real focus on data being present in OOP. Instead, there is simply the rule to keep data encapsulated with the code that knows how to correctly manipulate it. I can see how you might think of this as incompatible, but I guess I have a very different perspective on it. Just consider the fact that pattern matching is still used to select the appropriate class to use if it's not known in advance.

This single statement is probably where we disagree the most. Inheritance is still very useful even if all methods are written in a functional fashion. The point of using it is to take advantage of polymorphism to apply new functionality to existing logic without having to rewrite or refactor the existing logic. That's a large part of it's use. That's something you can't get without inheritance, and it has nothing to do with code being written in a data-centric fashion.

Neither am I. There are far too many scenarios when using a functional language where you have no option but to reference some data. If it was an absolute law that FP must be composed of only "pure functions", then most functional languages wouldn't exist in any useful sense.

Just No. It's just another tool. Such a claim would be like saying "opening a paint can with a flat head screwdriver is bad". That may not be the best use of that kind of screwdriver, and certainly not what it was intended for, but it is none-the-less very useful for that purpose.

Just 1 of the many [examples](Master the JavaScript Interview: What is Functional Programming? | by Eric Elliott | JavaScript Scene | Medium.

Among other things, yes.

My point is that, restrictions included, it is normal inheritance. This method of inheriting has long since predated the keyword interface. That's why I don't see it as anything unique or special.

I'm one of those people. The problem that goto has that is not reflected in break or continue is that goto doesn't clean up the stack! It can easily leave the stack frame corrupted where as the others only jump around within the function, much like if or loop statements.

We do have a similar perspective. What I've been trying to do is pull you back when your characterization of inheritance leans a little too hard on rhetoric as is often done by those who don't understand its utility. FP, OOP, inheritance, encapsulation, composition: instead of viewing these things as paradigms and starting a pseudo-religious campaign for or against any of them, I encourage everyone to see them as just tools in your bag. Learn how to use them properly, then pick the best one for the task at hand. So as you said, similar perpsective.

Side Note:
My perspective comes from a time when memory management was still mostly done manually and memory space was a premium. You didn't write code that didn't need to be written since you might not be able to load the code if it grew too large. This meant code re-use was a premium, both at compile time and runtime. Proper inheritance in languages that supported it was a powerful tool towards that end without risking the spaghetti that came along with pre-modular procedural-only programming.

In regards to the functional topic, I'll DM you about that when I have time with some responsese. I think it's an interesting conversation that I'd like to pursue, but it's starting to branch away from the main topic of this thread.


Thinking about this more, you are right that inheritance can be used just fine without centering logic around data. It just means the classes you're working with don't have any state, just methods, and inheritance can be used to shadow specific methods and override their behaviors. I guess these sorts of techniques are often accomplish in other ways in functional languages (e.g. via the strategy pattern - they don't call it that in FP but that's basically what they use), but the need for something that solves the problem you're describing is always going to be needed.

This is fair, as that's certainly the way I've been characterizing it.

Ok, you're absolutely right, I must have been a little sleepy or something when I wrote that reply. Sometimes, I'll throw the term "implementation inheritance" into this sort of discussion to make it clear that I'm talking about a specific type of inheritance, but I didn't do that here. I don't know of anyone who has an issue with "interface inheritance" like you're referring to. So, going forwards, if I'm talking about "inheritance", know that I'm specifically talking about "implementation inheritance", and if it's preferred, I'll be happy to start using that full "implementation inheritance" phrase each time instead.

1 Like