cli  
Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Where to Discuss?

Local Group

Preface

Goal: Continue Part One


4: Without Flat and Flatmap

So we have talked about edge cases using ES10 (ES 2019). What if we want to be more conventional, not using this brand new feature. Of course we can.

For simplicity, this example is using, spread operator from ES9 (ES 2018). You can manage not use this spread operator, and find oldschool alternatives from the internet.

Merge Unique

Avoid For-of

import songs from "./songs-data.js";

const
  unique = array => [... new Set(array)],
  merge  = (array1, array2) => [...array1, ...array2],
  mergeUnique = (a1, a2) => unique(merge(a1,a2));

// const mergeUnique = (a1, a2) => [... new Set([...a1, ...a2])];

let allTags = [];

songs.forEach(song => {
  if(song.tags) {
    allTags = mergeUnique(allTags, song.tags)
    console.log(allTags);
  }
});

As an alternative we can write it in cryptic way as below:

const mergeUnique = (a1, a2) => [... new Set([...a1, ...a2])];

It is still the long old code, with a little improvement. We are going to improve more.

Inline Ternary

Avoid Conditional

We can get rid of the conventional conditional checking, by using ternary.

import songs from "./songs-data.js";

const
  unique = array => [... new Set(array)],
  merge  = (array1, array2) => [...array1, ...array2],
  mergeUnique = (a1, a2) => a2 ? unique(merge(a1,a2)) : a1;

// const mergeUnique = (a1, a2) => a2 ? [... new Set([...a1, ...a2])] : a1;

let allTags = [];

songs.forEach(song => {
  allTags = mergeUnique(allTags, song.tags)
  console.log(allTags);
});

As an alternative we can write it more cryptic way as below code:

const mergeUnique = (a1, a2) => a2 ? [... new Set([...a1, ...a2])] : a1;

It is not recommended to write cryptic code.

Destructuring in Loop

Fancy Loop Syntax

We can rewrite the loop above as below

for (const song of songs) {
  const { title, tags = [] } = song
  allTags = unique(merge(allTags, tags));
  console.log(allTags);
}

And even use destructuring syntax in loop.

import songs from "./songs-data.js";

const
  unique = array => [... new Set(array)],
  merge  = (array1, array2) => [...array1, ...array2];

let allTags = [];

for (const { title, tags = [] } of songs) {
  allTags = unique(merge(allTags, tags));
  console.log(allTags);
}

This is actually simpler. We can handle the default value for undefined, using { title, tags = [] }, the empty array will be merged later.

Alternatively: Using Map

Avoid For-each

Finally we can get rid of the conventional loop, and using map iterator instead.

import songs from "./songs-data.js";

const
  unique = array => [... new Set(array)],
  merge  = (array1, array2) => [...array1, ...array2],
  mergeUnique = (a1, a2) => a2 ? unique(merge(a1,a2)) : a1,
  mapUnique = (allTags, songs) => songs.map(song => 
    allTags = mergeUnique(allTags, song.tags)
  );

const allTags = mapUnique([], songs).pop();
console.log(allTags);

Nomore let keyword. All are immutable.

With result as below output:

[ '60s', 'jazz', 'rock', '70s', 'pop' ]

Songs: Using Map: Avoid For-each

If we do not use pop(), the code result will be as below:

$ node 07-songs-map.js
[
  [ '60s', 'jazz' ],
  [ '60s', 'jazz', 'rock' ],
  [ '60s', 'jazz', 'rock', '70s' ],
  [ '60s', 'jazz', 'rock', '70s', 'pop' ],
  [ '60s', 'jazz', 'rock', '70s', 'pop' ]
]

Now you know how it works 😁. This is not a very intuitive approach.

Finally: Flatten Using Reduce

This is basically an oldschool method, before flat and flatmap coming in.

import songs from "./songs-data.js";

const
  unique  = array => [... new Set(array)],
  getTags = songs => songs.map(song => song.tags || []),
  flatten = array => array.reduce((a,b) => a.concat(b));

const allTags = getTags(songs);
console.log(allTags);

We can handle the default value for undefined, using song.tags || [], the reduce will merge the empty array.

Songs: Using Reduce: Avoid For-each

This way is more intuitive.

Opinion

It is a blessed that we have flat and flatmap, that making it easier to solve this issue.


5: Not so Pure Functional

Using map without returning value is considered anti pattern.

We can somewhat, ask the map function, to iterate without really returning value. This way, we can also avoid for-loop and stuff.

Just remember, this approach is using mutable data, that you can spot with the let keyword. Therefore, this is not a pure functional.

We are going to do it step by step, following original code with imperative thinking.

Map

Just a little improvement from the original code.

import songs from "./songs-data.js";

// using map without returning value is considered anti pattern.
const addTags = (tagSet, tags) => tags.map(tag => tagSet.add(tag));

let tagSet = new Set();

// using set feature to collect tag names
songs.forEach(song => {
  if(song.tags) {
    let tags = song.tags;
    console.log(tags);

    addTags(tagSet, song.tags);
  }
});

console.log(tagSet);

// normalize to array
let allTags = [...tagSet];

console.log(allTags);

Ternary

And also removing conditional.

const
  addTags     = (tagSet, tags) => tags.map(tag => tagSet.add(tag)),
  addSongTags = (tagSet, song) => 
        song.tags? addTags(tagSet, song.tags) : undefined;

let tagSet = new Set();

// using set feature to collect tag names
songs.forEach(song => {
  addSongTags(tagSet, song);
});

// normalize to array
let allTags = [...tagSet];

console.log(allTags);

More Map

I am getting dizzy.

const
  addTags      = (tagSet, tags) => tags.map(tag => tagSet.add(tag)),
  addSongTags  = (tagSet, song) => 
        song.tags? addTags(tagSet, song.tags) : undefined,
  addSongsTags = (tagSet, songs) =>
        songs.map(song => addSongTags(tagSet, song));

// using set feature to collect tag names
let tagSet = new Set();
addSongsTags(tagSet, songs);
console.log(tagSet);

// normalize to array
let allTags = [...tagSet];

console.log(allTags);

Spread

And finally confused

const
  addTags      = (tagSet, tags) => tags.map(tag => tagSet.add(tag)),
  addSongTags  = (tagSet, song) => 
        song.tags? addTags(tagSet, song.tags) : undefined,
  addSongsTags = (tagSet, songs) =>
        songs.map(song => addSongTags(tagSet, song)),
  setToArray   = tagSet => [...tagSet];

// using set feature to collect tag names
let tagSet = new Set();
addSongsTags(tagSet, songs); // no assignment
let allTags = setToArray(tagSet);

console.log(allTags);

With result as below output:

[ '60s', 'jazz', 'rock', '70s', 'pop' ]

Although this works well, this is siffcult to be understood. This is also actually, an imperative programming, written in functional fashioned.


Conclusion

Now you see the difference, between the three.

  1. The imperative programming.
  2. The functional programming.
  3. The imperative programming written in functional fashioned.

What do you think?


What is Next 🤔?

We still have interesting concurrency topic.

Consider continue reading [ Ecmascript - Concurrency with Event Emmitter ].