0

suppose that, I've this Object for methods chain:

let list = {
  numbers: [],
  create: function(list) {
    this.numbers = list;
    return this;
  },
  asc: function() {
    return this.numbers.sort((a, b) => a - b);
  },
  desc: function() {
    return this.numbers.sort((a, b) => b - a);
  },
  unique: function() {
    return this.numbers.filter((item, i, source) => source.indexOf(item) === i);
  }
}

let new_list = list.create([10, 2, 2, 3, 1, 2, 4]).unique();
console.log(new_list);

but how can handle this chain:

list.create([10, 2, 2, 3, 1, 2, 4]).unique().asc();

I actually want to handle it for non-limited chain... but it works for only one method for now!

Martin Rützy
  • 415
  • 1
  • 11
  • 22
  • Your `list` should be a `class List` or a constructor function (`function List() { ... }` that populates a `prototype`), not an ad-hoc object. – Dai Mar 09 '22 at 08:04
  • @T.J.Crowder But the OP's code is horribly broken: their `create` function _mutates_ `this`, so they're cobbling their own state. – Dai Mar 09 '22 at 08:06
  • @Dai but how can i handle returned value? because I can't return two values... – Martin Rützy Mar 09 '22 at 08:06
  • @MartinRützy Constructor functions don't return anything: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor – Dai Mar 09 '22 at 08:07
  • @T.J.Crowder i was thinking to change `numbers` then call a returnValue method to handle it... – Martin Rützy Mar 09 '22 at 08:07
  • @Dai - I kinda missed the whole `create` thing. :-D – T.J. Crowder Mar 09 '22 at 08:09
  • 1
    @T.J.Crowder I argue it's broken because the singleton `list`'s `this.numbers` property is overrwritten, so `var x = list.create( [somearr] ); var y = list.create( [anotherarr] ); var z = x.unique();` won't work. – Dai Mar 09 '22 at 08:10
  • Also, `Array.prototype.sort` will **mutate `this.numbers` in-place** which is incompatible with how most people think the builder (anti-)pattern works. – Dai Mar 09 '22 at 08:25

3 Answers3

3

but how can handle this chain:

list.create([10, 2, 2, 3, 1, 2, 4]).unique().asc();

In order to chain methods like that, each method (at least before the last method in the chain) must return an object that has the next method on it. Your unique method returns an array, not the list object, so it doesn't have an asc method. In order to have that chain, you'd need unique to return something with asc.

In this specific example, you might use an augmented array. You could directly augment an array instance, but for chains like you're describing, you probably want multiple instances of list (not just one), so that suggests an Array subclass:

class List extends Array {
    static fromList(list) { // Requires either no argument or an iterable argument
        const fromList = new this();
        if (list) {
            fromList.push(...list);
        }
        return fromList;
    }

    asc() {
        const fromList = this.constructor.fromList(this);
        fromList.sort((a, b) => a - b);
        return fromList;
    }

    desc() {
        const fromList = this.constructor.fromList(this);
        fromList.sort((a, b) => b - a);
        return fromList;
    }

    unique() {
        return this.constructor.fromList(new Set(this));
    }
}

const new_list = List.fromList([10, 2, 2, 3, 1, 2, 4]).unique().asc();
console.log(new_list);

That example always creates a new instance of List for each operation, but you can also do it where it mutates and returns the original list:

class List extends Array {
    static fromList(list) { // Requires either no argument or an iterable argument
        const fromList = new this();
        if (list) {
            fromList.push(...list);
        }
        return fromList;
    }

    asc() {
        return this.sort((a, b) => a - b);
    }

    desc() {
        return this.sort((a, b) => b - a);
    }

    unique() {
        const set = new Set(this);
        this.length = 0;
        this.push(...set);
        return this;
    }
}

const new_list = List.fromList([10, 2, 2, 3, 1, 2, 4]).unique().asc();
console.log(new_list);

Alternatively, you might use a builder pattern where you store up the things to do and then have a "go" terminal operation that does all the steps.

Note: Creating augmented array classes like List is fine, but beware that the subclass needs to handle all the signatures of the original Array constructor, which is...eccentric. If you pass new Array a single argument that's a number, it creates a sparse array with that length and no elements in it. If you pass it a single argument that isn't a number, it creates an array with that value as its only element. If you pass it multiple arguments, it creates an array with those elements in it. That's why I used a static fromList method rather than writing our own List constructor.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
3
  • You need to understand how state works in OOP, in addition to JavaScript's (somewhat unorthodox) this parameter.

    • As well as how some methods of Array (or Array.prototype) in JS will mutate in-place (like sort) while others treat this (the Array) as immutable and instead always return a new array (e.g. filter and map).
  • If you're defining a reusable object type (with properties and methods/functions) then you should define a class instead.

  • BTW, I assume this is just a learning-exercise, otherwise there really isn't a reason to reimplment a list type in JavaScript as Array does it all already.

  • As you want method-chaining (as a builder-pattern, I guess?), it's important to ensure you don't mutate this.numbers in-place, so only call Array.prototype.sort() on a new copy:

Something like this:

class MyNumbersList {
    constructor( arr ) {
        if( !Array.isArray( arr ) ) throw new Error( "Argument `arr` must be an array." );
        this.numbers = arr;
    }

    // Array.prototype.sort will mutate `this.numbers` in-place.
    asc() {
        const copy = Array.from( this.numbers );
        copy.sort((a, b) => a - b);
        return new MyNumbersList( copy );
    }
    desc() {
        const copy = Array.from( this.numbers );
        copy.sort((a, b) => a - b);
        return new MyNumbersList( copy );
    }

    // But `filter` returns a new array.
    // Use a `Set` instead of `indexOf`, otherwise you'll have `O(n*n)` complexity, which is horrible:
    unique() {

        const valuesSet = new Set( this.numbers );
        const copy = this.numbers.filter( n => valuesSet.has( n ) );
        return new MyNumbersList( copy );
    }

    // This is a property getter, so you don't need to use `()` to invoke it.
    // To ensure encapsulation (i.e. to prevent unwanted mutation of `this.numbers`) always return a by-value copy (using `Array.from`), instead of exposing `this.numbers` directly:
    get values() {
        return Array.from( this.numbers );
    }
}

Used like so:

const x = new MyNumbersList( [ 1, 2, 2, 3 ] );
const t = new MyNumbersList( [ 3, 2, 2, 1 ] );
console.log( x.values ); // "1, 2, 2, 3"
console.log( y.values ); // "3, 2, 2, 1"
console.log( x.desc() ); // "3, 2, 2, 1"
console.log( y.asc() ); // "1, 2, 2, 3"
console.log( x.desc().unique().values ); // "3, 2, 1"

All of the "modern" members of Array.prototype do not mutate an array instance in-place (like map and filter), however certain older members dating back to the first editions of JS are considered "Mutator Methods" which require you to be careful.

They're listed in this page. MDN used to have a page listing them too but it seems to be gone now.

Dai
  • 141,631
  • 28
  • 261
  • 374
  • @PeterSeliger I would have done that if terseness was my objective, but this isn't codegolf.SE. I wanted to write code that clearly shows each main computational step for the sake of clarity for the benefit of people new to JavaScript. – Dai Mar 09 '22 at 09:29
2

You could implement a function which returns the numbers and all other functions to return this for chaining.

This approach does not prevent the content to be overwritten from other calls, because of having only one object, with one data set at the same time.

let list = {
  numbers: [],
  create: function(list) {
    this.numbers = list;
    return this;
  },
  asc: function() {
    this.numbers.sort((a, b) => a - b);
    return this;
  },
  desc: function() {
    this.numbers.sort((a, b) => b - a);
    return this;
  },
  unique: function() {
    this.numbers = this.numbers.filter((item, i, source) => source.indexOf(item) === i);
    return this;
  },
  values: function() {
    return this.numbers;
  }

}

let new_list = list.create([10, 2, 2, 3, 1, 2, 4]).unique().values();

console.log(new_list);

To overcome this problem, you could use a class with methods, where every instance keeps the own data.

class List {
    constructor (numbers = []) {
        this.numbers = numbers;
    }

    asc() {
        this.numbers.sort((a, b) => a - b);
        return this;
    }

    create(numbers) {
        this.numbers = numbers;
        return this;
    }

    desc() {
        this.numbers.sort((a, b) => b - a);
        return this;
    }

    unique() {
        this.numbers = this.numbers.filter((item, i, source) => source.indexOf(item) === i);
        return this;
    }

    values() {
        return this.numbers;
    }
}

let new_list = new List([10, 2, 2, 3, 1, 2, 4]).unique().values();

console.log(new_list);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • The `create: function(list) {` function is still horrible though, and the entire object acts as a singleton which I doubt is their intention. What's so wrong with the `new` operator that people are trying to avoid it? – Dai Mar 09 '22 at 08:08
  • yes, i think so... but is my idea clean and does follow best practices? i'm not master in JS – Martin Rützy Mar 09 '22 at 08:09
  • @MartinRützy "but is my idea clean and does follow best practices?" - No, I really don't think so. (I'm writing my answer now) – Dai Mar 09 '22 at 08:12
  • @Dai what do you think? is there any way we don't consider `values()` method and each method return the values? – Martin Rützy Mar 09 '22 at 08:18
  • 1
    @MartinRützy ... one has to chose, one either wants the chaining which works upon an abstraction or one always wants to have the values returned. One can not have both. As soon as one returns the pure value, chaining is terminated. – Peter Seliger Mar 09 '22 at 08:20