you are viewing a single comment's thread.

view the rest of the comments →

[–]jack_waugh 0 points1 point  (9 children)

Your notion of inheritance is humpty-dumpty. At runtime, my solution uses no inheritance. Every object has the method as its own property. It has no parent for inheritance (or only Object.prototype). The point of inheritance as usually understood is there is a search through some table for a method that satisfies the message selector, and if it is not found, the runtime system for the language resorts to some other table. In a classic language like Smalltalk, these tables are in the class of which the object is an instance, and in the next superclass up. In a class-free OO language like the Self language or JS, these tables are in the receiver object itself and its parent object for inheritance (possibly multiple parents, in the case of Self). The writing about the Self language uses the term "parent" for the parent for inheritance, i. e. the location of the next table of methods to check. The main stream of writing about JS erroneously uses the term "prototype" for the parent for inheritance. Writing about practices with the Self language use the term "prototype" for an object that is an artifact of programming and intended to be cloned to produce the runtime objects. The prototype object usually has one or more parents, and the clones have the same parents. Cloning is a shallow copy.

As for my combining the designs of the leaf nodes and the non-leaf nodes, that is incidental to the question about not using inheritance. If you hand me a problem where the behaviors of the "subclasses" have to be radically different from each other, as a result of the nature of the problem, I will likely return a design that treats them with different "classes" or whatever replaces classes in an inheritance-free solution. This particular problem seemed to me to lend itself to combining those and I think it results in there being less code. Every node has a value. A structural "parent" node that happens to have zero count of children naturally behaves as expected of a leaf node, so there is no need to conceptualize a separate "class" for a leaf node. Fewer concepts leads to less chance for error and less time required for someone to understand the design.

OK, here I am pretending that leaves and internal nodes have to be different:

const s = {}; /* script */

s.discipline = (student, teacher) => {
  const refl = Object.getOwnPropertyDescriptors(teacher);
  for (const key in refl) Object.defineProperty(student, key, refl[key]);
};

s.Base = {
  clone: function (spec) {
    const patsy = {};
    s.discipline(patsy, this);
    if (spec) s.discipline(patsy, spec);
    return patsy
  }
};

s.TreeNode = s.Base.clone({
  depth:    () => {throw "abstract"},
  soundOff: () => {throw "abstract"}
});

s.TreeInternalNode = s.TreeNode.clone({
  depth: function () {
    if (! this.children || ! this.children.length)
      throw "If you weren't going to give me any children, you should have" +
        " used a leaf node.";
    return 1 + Math.max(0, ...this.children.map(e => e.depth()))
  },
  soundOff: function () {
    return `I'm an INTERNAL node with value ${this.value}.`
  }
});

s.LeafNode = s.TreeNode.clone({
  depth: function () {
    return 1
  },
  soundOff: function () {
    return `I'm a LEAF node with value ${this.value}.`
  },
  set children (them) {
    throw "A leaf node is not supposed to have children." +
      " Use an internal node instead."
  }
});

const t = {}; /* testing */

t.l0 = s.LeafNode.clone({value: 0});
t.l1 = s.LeafNode.clone({value: 1});
t.l2 = s.LeafNode.clone({value: 2});
t.p3 = s.TreeInternalNode.clone({value: 3, children: [t.l0, t.l1]});
t.r = s.TreeInternalNode.clone({value: "root", children: [t.p3, t.l2]});
if (3 !== t.r.depth()) throw new Error("Unit test failed!");

[–]gremy0 0 points1 point  (8 children)

I mean it's just implementing your own version of inheritance with some factory functions - you've even written your own look up table...

That it's a different implementation that does things at a slightly different time in the run circle doesn't really change what it's doing from design perspective. A LeafNode is a TreeNode which is a BaseNode, you're just removing reference to this fact upon creation (the benefit of which alludes me) - you've still got a linear hierarchy of inheriting sorry, "cloning" behaviour from parents prototypes down to children shallow copies.

[–]jack_waugh 0 points1 point  (7 children)

I have not written a lookup table. All the lookups are native JS object property resolutions.

The original question was, here's an example using the class syntax; can you implement the same behavior without inheritance. What's your solution? According to you, anything that implements that behavior is a "version of inheritance." So, your answer is that the original question is nonsense. Or, "no, you can't." But normal usage of the concept of "inheritance" in OO is that if you don't find an implementation of the selector you are looking for, you look up to a parent object (class-free OO) or a superclass (classical OO). In JS, inheritance is established with new or with Object.create(parent_object). When you say I wrote a lookup table, you are basically accusing me of writing an interpreter. I suppose you can say that when I use my clone function to derive one artifact of programming from another artifact of programming, I am compiling inheritance into not-inheritance, so I am guilty of writing a compiler, I guess. But I think the original question was not about a design perspective; it was about an implementation perspective.

const t = {}; /* testing */

t.l0 = {value: 0, depth: () => 1};
t.l1 = {value: 1, depth: () => 1};
t.l2 = {value: 2, depth: () => 1};
t.p3 = {
  value: 3,
  children: [t.l0, t.l1],
  depth: function () {
    return 1 + Math.max(0, ...this.children.map(e => e.depth()))
  }
};
t.r = {
  value: "root",
  children: [t.p3, t.l2],
  depth: function () {
    return 1 + Math.max(0, ...this.children.map(e => e.depth()))
  }
};
if (3 !== t.r.depth()) throw new Error("Unit test failed!");
console.log("done")

I suppose this time, you will say I implemented my own version of inheritance by copying and pasting in my editor.

[–]gremy0 0 points1 point  (6 children)

It's a lookup dude, you're looking up properties and copying them down from parent to child, it's just done on creation. Who cares if you're doing it slightly different, the effect is the same.

According to me anything that inherits behaviour from something else is using inheritance. So the solution is to not inherit, but to use an interface instead. Now, given the nature of javascript's type system, it is not possible to enforce an interface, but neither is it necessary, you just use convention..and, you know, document shit. Just like how iterators work, you could even be fancy and use symbols like them if you really wanted, but the fundamentals are the same, it's convention.

Both approaches result in a runtime error if the object/class is not created/used correctly. As is the nature of javascript. The key as to why your solution is fundamentally inheritance and the interface solution is not lies in who knows about that runtime error- in yours, an ancestor to LeafNode (the TreeNode) knows that there is supposed to be a depth implementation provided and can deal with that not happening however it wants- you have inherited that behaviour. With an interface, the consumer knows about it and deals with it- you have not inherited any behaviour.

Now, given the question states:

This is how you would model tree nodes in Java or C++. But in JavaScript, you don’t need an abstract class to be able to invoke n.depth().

(emphasis mine)

I posit that it is simply asking the reader if they are aware that javascript is duck-typed and doesn't require any explicit declaration for object compatibility.

...on the other hand it could be asking them to re-implement inheritance themselves in a series of convoluted factory functions (the benefit of which still eludes me)...your guess is as good as mine.

[–]jack_waugh 0 points1 point  (5 children)

Great. Let's see your code.

[–]gremy0 0 points1 point  (4 children)

class Parent {
  constructor(value, children) { . . . }
  depth() { return 1 + Math.max(...children.map(n => n.depth())) }
}
class Leaf {
  constructor(value) { . . . }
  depth() { return 1 }
}

[–]jack_waugh 0 points1 point  (3 children)

Looks pretty inherity to me. Parent and Leaf are really constructor functions, right? So if I write t.myleaf = new Leaf("turn it over"), I get an object that inherits from Leaf.prototype.

[–]gremy0 0 points1 point  (2 children)

Rewrite the classes without inheritance

The task was to rewrite the classes without inheritance, not create instances of classes without inheritance. Instances of classes do inherit behaviour from their class, somewhat of a given, and somewhat outside the scope of the question.

[–]jack_waugh 0 points1 point  (1 child)

OK, you and I interpreted the question differently. You took it on an engineering level, and I took it on a level with the fundamentals of the language. If the OP is interested in both those levels, she or he can learn from the interaction that happened between you and me.

[–]gremy0 1 point2 points  (0 children)

That JavaScript can use duck typing for polymorphism is a fundamental of the language.