List Comprehension

Proposal - List Comprehension

Hi, I have an idea to propose but not sure how it can be implemented. Still, I wanted to contribute a new syntax feature for ECMAScript with a functional programming feature: "List Comprehension".

Introduction

I know there are languages adopt the feature such as Python:

aList = [1, 2, 3, 4, 5]
doubled = [ x * 2 for x in aList ]

'aList' is the existing list, but we can use the List Comprehension syntax to create another list.

Simply put, it is a feature for creating a list (or array) structured object from another list structured object.

I know we can already use basic syntax in JS like the operation Array.prototype.map, but I wanted to propose a more advanced use case which combines with the ESMAScript standard.

Additionally, I searched from the internet and found out that the MDN doc already has a similar feature called "Array Comprehension". Again, I know this feature is obsoleted in the browser implementation, but I wanted to reference how this feature has experimented in the past.

The examples from the docs are:

/* Normal Case */
[for (i of [1, 2, 3]) i * i ]; 
// [1, 4, 9]

var abc = ['A', 'B', 'C'];
[for (letters of abc) letters.toLowerCase()];
// ["a", "b", "c"]

/* Multiple Parameters Case */
var numbers = [1, 2, 3];
var letters = ['a', 'b', 'c'];

var cross = [for (i of numbers) for (j of letters) i + j];
// ["1a", "1b", "1c", "2a", "2b", "2c", "3a", "3b", "3c"]

/* Conditional Case */
var years = [1954, 1974, 1990, 2006, 2010, 2014];
[for (year of years) if (year > 2000) year];
// [2006, 2010, 2014]
[for (year of years) if (year > 2000) if (year < 2010) year];
// [2006], the same as below:
[for (year of years) if (year > 2000 && year < 2010) year];
// [2006] 

The following contents are about what I have come up with.

Basic Syntax

I think the proper way is to represent in a more mathematical way:

[ i * i | for (i of [1, 2, 3]) ]; 
// [1, 4, 9]

var abc = ['A', 'B', 'C'];
[ letters.toLowerCase() | for (letters of abc)];
// ["a", "b", "c"]

Secondly, instead of using multiple for...of... expressions, we can simply use a little bit of destructuring style to simplify the cases:

/* Multiple Parameters Case */
var numbers = [1, 2, 3];
var letters = ['a', 'b', 'c'];

var cross = [ i + j | for ([i, j] of [numbers, letters])];
// ["1a", "1b", "1c", "2a", "2b", "2c", "3a", "3b", "3c"]

However, it is important to note that i represented the elements iterated from the numbers array; j represented the elements iterated from the letters array.

Lastly, when it comes to the conditional case, I think we can do something like:

/* Conditional Case */
var years = [1954, 1974, 1990, 2006, 2010, 2014];
[ year | for (year of years) if (year > 2000)];
// [2006, 2010, 2014]
[ year | for (year of years) if (year > 2000 && year < 2010) year];
// [2006] 

Additionally, the conditional case above just filter the element out of the list and didn't do any additional computation. I think it can be simplified like this:

/* Conditional Case */
var years = [1954, 1974, 1990, 2006, 2010, 2014];
[ for (year of years) if (year > 2000)];
// [2006, 2010, 2014]
[ for (year of years) if (year > 2000 && year < 2010) year];
// [2006] 

However, if it didn't have the conditional case and didn't do any computations, such as:

[ i | for (i of [1, 2, 3]) ]; 
// [1, 2, 3]

I think it shouldn't be simplified in:

[ for (i of [1, 2, 3]) ]; 
// Wrong case

Reasons are:

  1. i isn't used, so declaring i doesn't have any meaning
  2. The operation above is the same as cloning an array which I think it could just use [...[1, 2, 3]]

However, if it comes to the multiple parameter case, then:

// Finding the coordinates in the first coordinate quadrant
[ [x, y] | for ([x, y] of [xPositions, yPositions]) if (x > 0 && y > 0) ]

Can be reduced as:

// Finding the coordinates in the first coordinate quadrant
[ for ([x, y] of [xPositions, yPositions]) if (x > 0 && y > 0) ]

Since the destructured format [x, y] in for ... of ... matches the desired result, the "[x, y] |" can be omitted.

May be confused with Bitwise OR operator? (which is |)

I think this might not be the problem, because in this article, the list comprehension syntax idea, the | should always followed by the keyword for.

[ a | b | for ([a, b] of [list1, list2]) ]

However, it might be best to distinguish by using parentheses:

[ (a | b) | for ([a, b] of [list1, list2]) ]

Alternative List Comprehension Syntax which I've Came Up With

  1. Omit the |
[ x ** 2 for (x of aList) ]
  1. Using where
[ x ** 2 | where (x of aList)]
  1. Omit the | and using where
[ x ** 2 where (x of aList) ]

But I personally still wanted to stick with for...of... syntax because of the following cases which combines with ECMAScript standards I'm going to address.

Combining with ECMAScript Standards

The above are the basic cases are covered. I know we can use similar approaches such as Array.prototype.map or Array.prototype.filter to deal with list-like structure creation.

However, since more and more ECMAScript standard covered features such as ES6 Iterators and Generators, ES7 Async-Await and ES9 Asynchronous Iterations, I have several ideas came up.

Feature 1. Use Iterator object as the existing list

function *genExample() {
  yield 1;
  yield 2;
  yield 3;
}

[ x ** 2 | for (x of genExample())];
// [1, 4, 9]

for...of... loop can accept and iterate through objects with Symbol.iterator property (because it can generate the Iterator object).

And if iterator object is acceptable, which means:

// Originally JSON object isn't iterable
let info = { name: 'Max', age: 20, hasPet: false };

// We can declare its own generator function to generate the iterator
info[Symbol.iterator] = function*() {
  for (prop in this){
     yield [prop, this[prop]]
  }
}

// It can now be plugged into the for...of... loop
for (let [p, v] of info) { console.log(p, v) }

// Which means it can also be used in List Comprehension:
[ `${prop} is ${value}` | for [prop, value] of info ]

Feature 2. Generating an iterator from List Comprehension

Similar to how we declare the generator function, we can use a similar approach to declare an iterator object:

const list = [1, 2, 3];
let iterator = *[ i ** 2 | for (i of list) ];

// Using for...of... loop
for (let i of iterator) {/* ... */}

// Or simply use the built-in APIs like the next method, etc...

Feature 3. Await the Promise.all like list structure

This feature is considered a little hard for me to come up with. Overall, I reckon that this might be a case where we can combine the Promise.all utility with an expression to create a new array data structure.

async function example() {
  const responses = await [ res.data | for (res of requests) ];
  // Do something...
}

Feature 4. Use "Asynchronous Iterator" as the existing list

This feature states that it generates the asynchronous iterator via list comprehension.

const lines = async *[ line.trim() | for await (line of readline) ];

Basically, its a bit like generating an iterator from this asynchronous function:

async function* () {
  for await (const line of readline) {
    yield line.trim();
  }
}

Adopt Function Programming Feature

1. Eta Conversion

JavaScript already had embraced this feature, for instance:

// Declare a function which calculates the squared value of number
function squared(value) { return value ** 2; }

const list = [1, 2, 3];

// Map each element with squared value
list.map(x => squared(x));

Can be simplified into:

list.map(squared)

Because it applies the elements from the array and directly inputs into another function.

So, as in the list comprehension case, the code:

[ squared(x) | for (x of list) ];

Can also considered to simplified into this way:

[ squared | for (x of [1, 2, 3]) ];

But then the x will violate the rule which I have mentioned in this article. The x isn't used in this case. So it kind of a bit awkward to come up with another solution:

[ squared | for (list) ]

// It should also be the same as:
// [ squared | for list]
//

Well, I'm not sure if this is appropriate, but please see the next feature, although it could be a very far future to propose and entering the implementation phase.

Feature 2. Function Composition in List Comprehension

Normally, in mathematics, assume we have function f(x) and g(x), we can use f・g to represent f(g(x)).

To achieve function composition with list comprehension idea in this article:

function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
  return str + '!';
}

let names = ['maxwell', 'martin', 'leo'];

[ exclaim(capitalize(name)) | for (name of names) ]

If we want to do it in a more elegant way by using Eta conversion, we need to do something like:

function customConversion(str) {
  return exclaim(capitalize(str));
}

[ customConversion | for names ]

However, considering there are existing proposals focusing on pipeline operator which is about function composition, I think we can simplify to:

[ capitalize |> exclaim | for names ]

Summary

I think that's all, this is my idea about contributing ECMAScript by proposing List Comprehension feature.

I know this article is long, but your patience and kind feedbacks would be very appreciated. :)

See also:

1 Like

First of all, I generally like the idea of simplyfiying the process of building up arrays from logic.

However, I do have a few concerns:

for, async and others are fundamental statements of the language, now they are suddenly used as expressions . The only way I can think of, for this to work in the way you propose it, is to introduce a whole new set of syntax, that redefines these keywords.

That's a lot of new syntax, just to support little new functionality ... I mean, there is already:

const result = [...(function*() {
  for (year of years) if (year > 2000 && year < 2010) yield year;
})()];