Why are Higher Order Functions hiding my ES6 Class properties?
Asked Answered
C

2

0

EDIT: I've added updated screenshots (at the end), for further clarification.

I'm attempting to use high order functions to compose subclasses/mixins but noticed I can only access the properties of the very first class I extend and can only access the properties of any subsequent classes after I've called the class. Here is a contrived example of what I mean:

These are functions that will apply subclasses to the parent class:

export const middleNameClass = middlename => BaseClass => {
  return class extends BaseClass {
    constructor(args) {
      super(args);
      this.middlename = middlename;
    }
  };
};

export const lastNameClass = lastname => BaseClass => {
  return class extends BaseClass {
    constructor(args) {
      super(args);
      this.lastname = lastname;
    }
  };
};

Here is firstNameClass, which will be extended directly by the parent class, Person:

class firstNameClass {
  constructor(args) {
    this.firstname = args;
  }
}

This is Person, which extends firstNameClass:

class Person extends firstNameClass {
  constructor(args) {
    super(args);
    this.getNames = this.getNames.bind(this);

    // I'm using to log class properties to the console
    this.getNames();
  }

  getNames() {
    console.log("this inside getNames(): ", this);
    console.log("firstnames inside getNames(): ", this.firstname);
    console.log("middlenames inside getNames(): ", this.middlename);
    console.log("lastnames inside getNames(): ", this.lastname);
  }
}

and finally, here is where I apply my higher order functions and create my class:

const enhancedClass = compose(
  middleNameClass("Allan"),
  lastNameClass("Poe")
)(Person);

const namedPerson = new enhancedClass("Edgar");

However, I see the following when I check my console.log:

this.firstnames inside getNames(): Edgar
this.middlenames inside getNames(): undefined
this.lastnames inside getNames(): undefined

Could someone explain what I'm doing wrong?

EDIT: Here are the contents of my Person class: enter image description here

and here is what is output to the console, after I create the class: the

Courteous answered 1/4, 2018 at 0:49 Comment(2)
Where is compose defined?Civvies
@Paulpro I'm using https://github.com/stoeffel/compose-function/blob/master/module/index.js but the same thing happens with Lodash's flowRightCourteous
H
2

At new enhancedClass('Edgar'), this happens:

  • lastNameClass's constructor calls super
  • middleNameClass's constructor calls super
  • Person's constructor calls super
  • firstNameClass does this.firstName = 'Edgar'
  • Return to Person, which calls getNames
  • Return to middleNameClass, which does this.middleName = 'Allan'
  • Return to lastNameClass, which does this.lastName = 'Poe'

Calling getNames afterwards should work. Same thing would've happened if you used extend every time.

Headmost answered 1/4, 2018 at 1:7 Comment(6)
This is the correct answer to another question. I only created getNames to demonstrate how certain properties are undefined when accessed within my target class; It's not the problem in itself.Courteous
@Courteous It says that they are undefined when you access them inside the Person constructor, where they are not yet initialised. They are only created in the enhancedClass, which does extend Person.Gault
@Courteous Maybe what you actually wanted is to make Person the "target class" as you call it, and instead of subclassing it have Person itself extend the mixin classes: class Person extends middleNameClass("Allan")(lastNameClass("Poe")(firstNameClass)) { … }Gault
@Gault That approach could definitely work but I would sacrifice readability, as currying makes it convoluted and difficult to follow. It seems like I may have to do something to the prototype inside each of my functions.Courteous
@Courteous Of course you can also use compose or functions with multiple parameters or whatever syntax you like, but the idea of having the mixins that Person wants to use in the extends clause is important.Gault
@Gault mixwith.js comes the closest to what you suggestion but there's still the issue of composition. Honestly, I may just use a plain old Objects and scrap the idea of "Higher Order Classes," as this seems like more trouble than it's worth.Courteous
D
1

This isn't an answer to your question but maybe it's a solution to your problem

JavaScript doesn't have multiple inheritance but luckily for you functional programming has nothing to do JavaScript's class system, object prototypes, or other object-oriented concepts. Functional programming is all about functions!

We begin writing our inheritable modules with some functions

// module 1
const hasFirstName = (firstName = "") => o =>
{
  field (o, 'firstName', firstName)
}

// module 2
const hasMiddleName = (middleName = "") => o =>
{
  field (o, 'middleName', middleName)
}
  
// module 3
const hasLastName = (lastName = "") => o =>
{
  field (o, 'lastName', lastName)
}

We haven't defined field yet, but don't worry. Let's look at a somewhat more involved module next

// module 4
const nameable = (first, middle, last) => o =>
{
  inherit (o, hasFirstName (first))
  inherit (o, hasMiddleName (middle))
  inherit (o, hasLastName (last))
  method (o, 'getFullName', (self) => `${self.firstName} ${self.middleName} ${self.lastName}`)
  method (o, 'sayHelloTo', (self, other) => `Hello ${other.getFullName ()}, my name is ${self.getFullName ()}`)
}

OK, so now we can see how some modules can be composed of other modules. Before we look at inherit and method, let's see how we'd use our module

const Person = (first, middle, last) =>
  Obj (self => {
    inherit (self, nameable (first, middle, last))
  })

So maybe you're annoyed that I keep making stuff up in each new code paste, but this is a very powerful practice called wishful thinking

"Wishful Thinking" is a very powerful programming practice:

Before implementing a component you write some of the code that actually uses it. This way you discover what functions with what parameters you really need, which leads to a very good interface. You will also have some good test code for your component.

The idea is based on the fact that an interface's purpose is to simplify the code that uses the component, not to simplify the code that implements it.

Exercising this practice, we wished up this imaginary object system based on what we need it to do - not based on what JavaScript's object system is capable of.

Of course we expect that using our Person will be straightforward

const p1 =
  Person ('Augusta', 'Ada', 'King-Noel', 166)

const p2 =
  Person ('Gerald', 'Jay', 'Sussman', 71)

console.log (p1.sayHelloTo (p2))
// Hello Gerald Jay Sussman, my name is Augusta Ada King-Noel

And here's the dependencies: What I want you to see here is that no class or even this is used. So even if JavaScript didn't have a native object system, this demonstrates you could make your own

const Obj = (f, instance = {}) =>
  (f (instance), instance)
  
const inherit = (o, f) =>
  Object.assign (o, f (o))

const field = (o, name, value) =>
  Object.assign (o, { [name]: value })

const method = (o, name, f) =>
  Object.assign (o, { [name]: (...xs) => f (o, ...xs) })

Full program demonstration

// DIY class system
const Obj = (f, instance = {}) =>
  (f (instance), instance)
  
const inherit = (o, f) =>
  Object.assign (o, f (o))

const field = (o, name, value) =>
  Object.assign (o, { [name]: value })

const method = (o, name, f) =>
  Object.assign (o, { [name]: (...xs) => f (o, ...xs) })

// module 1
const hasFirstName = (firstName = "") => o =>
{
  field (o, 'firstName', firstName)
}

// module 2
const hasMiddleName = (middleName = "") => o =>
{
  field (o, 'middleName', middleName)
}
  
// module 3
const hasLastName = (lastName = "") => o =>
{
  field (o, 'lastName', lastName)
}

// module 4
const nameable = (first, middle, last) => o =>
{
  inherit (o, hasFirstName (first))
  inherit (o, hasMiddleName (middle))
  inherit (o, hasLastName (last))
  method (o, 'getFullName', (self) => `${self.firstName} ${self.middleName} ${self.lastName}`)
  method (o, 'sayHelloTo', (self, other) => `Hello ${other.getFullName ()}, my name is ${self.getFullName ()}`)
}

// Person class
const Person = (first, middle, last) =>
  Obj (self => {
    inherit (self, nameable (first, middle, last))
  })

// demo
const p1 =
  Person ('Augusta', 'Ada', 'King-Noel')

const p2 =
  Person ('Gerald', 'Jay', 'Sussman')

console.log (p1.sayHelloTo (p2))
// Hello Gerald Jay Sussman, my name is Augusta Ada King-Noel

Our Person class can obviously define its own fields and methods as well

const dateDiff = (d1, d2) =>
  Math.abs (d1 - d2) / 1000 / 60 / 60 / 24 / 365 >> 0

const Person = (first, middle, last, birthdate = new Date) =>
  Obj (self => {
    inherit (self, nameable (first, middle, last))
    field (self, 'birthdate', birthdate)
    method (self, 'calculateAge', (self) => dateDiff (new Date, self.birthdate))
    method (self, 'sayAge', (self) => `I am ${self.calculateAge()} years old`)
  })

const p2 =
  Person ('Gerald', 'Jay', 'Sussman', new Date ('February 8, 1947'))

console.log (p2.sayAge ())
// I am 71 years old

Get creative and invent any other features you want

  • Maybe you want to make an overrideMethod that gives you the ability to define a new method with the same name, but still give the caller access to both methods
  • Maybe you want privateMethod or classMethod helpers
  • Maybe field could be enhanced to emit events when values changed
  • Maybe field could be changed so that values cannot be set, where mutableField could be used for fields that can change

Write it how you want it then make your wishes come true. Any limitations are you own.

Dextran answered 1/4, 2018 at 23:55 Comment(2)
I don't understand JS much, but "wishful thinking" is something I was thinking about recently quite a lot. Maybe one day we'll just write stuff down without at all implementing the helpers we used! (when there can be only one valid implementation). or e.g. we write yet another piece of code and get warned "no, that's inconsistent! would be impossible to implement!".Mcmillon
Now the real question is: would that language be high-level enough? :DDextran

© 2022 - 2024 — McMap. All rights reserved.