Using Array Functions and Composition to Talk to JSON Like a Human
February 28th, 2016 byIn case you haven’t paid much attention to function composition, you should honestly take a second look and reconsider the powerful benefits. Think about it like this, you ask what you want to get, and Javascript decides how to do it. Rather than writing tons of nested loop blocks and statements, just write abstractions that look like verbal sentences. To accomplish this, we’re gonna be using array functions and we’re gonna glue them together to solve problems by describing solutions.
Javascript already provides a good collection of methods to manipulate and query arrays out of the box. I’ll be using some of the most relevant ones, such us map, filter, find, reduce, and some. Check the Mozilla Developer Network for more information about them.
For these examples we’re gonna be targeting the following JSON collection of music records that contain album, artist, rating, genre, and recording information.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// JSON EXAMPLE STRUCTURE const music = [ { "album": "Kind Of Blue", "artist": "Miles Davis", "rating": 4.8, "genre": ["jazz","modal"], "recording": { "label": "Columbia", "release_year": 1959, "format": ["Vinyl", "LP", "Album", "Stereo"] }, ... ] |
Expand complete JSON used in this exercise
Our goals:
- Get the best (highest rated) records, anything above 4 stars
- Get the best records by a specific genre
- Get the artist names of the best records by genre
- Take the album name from the earliest release
- Take one artist name that starts with the letter ‘A’, and uppercase it
- Get a unique (not repeated) list of all the release formats from all the records
Grab the best records
1 2 3 4 |
const grabBestMusic = (content) => music.filter( x => x.rating > 4 ); grabBestMusic(music); |
filter() applies the callback function for each element in the array, returning a new array of all the values where the condition was met. So if I applied filter on an array of 5 elements where the condition was only met for 2 elements, then the returned array will have a length of 2.
Grab best music by genre
First we need to get the music by genre, for this we need to go through all of the records and see if the specified genre is inside of that genre array property. Then filter all of the records where the genre was found.
1 2 |
const grabByGenre = (music, genre) => music.filter( x => x.genre.some( y => y == genre)); |
some() executes the callback for each element in the array until it finds one where the condition provided returns true. When the first condition is satisfied, then it stops and doesn’t traverse the rest of the array.
Putting the last 2 functions together:
1 2 3 4 5 6 7 |
// Compose const bestMusicByGenre = (content, genre) => grabByGenre( grabBestMusic(content, genre), 'pop' ); bestMusicByGenre(music, 'pop'); // output: { album: "Let it be", artist: "The Beatles", genre: ["rock", "blues rock", "pop"], rating: 5, ... } |
When you chain individual actions, bigger problems can be solved.
In the last example we used function composition as a way to apply a function to the results of another. This is fundamentally written in the following way:
Function g is called with a passed value of x, and the result of g is passed to function f. We can already see how wrapping functions on top of each other can scale to having more complex expressions. Let’s add another layer of complexity.
Take the artist names of the best records by genre
1 2 3 4 5 6 7 8 9 10 11 |
const grabRock = (content) => grabByGenre(content, 'rock'); const pickArtistName = (content) => content.map( x => x.artist ); const bestRockArtists = pickArtistName(grabBestMusic(grabRock(music))); bestRockArtists; // output: ["Radiohead", "Miles Davis", "Pink Floyd", "The Beatles"] |
map() applies the callback function to each of the items in the array, creating a new array of all new values that resulted by applying the callback function to each one of them.
A better composition
For ex.
pickArtistName(grabBestMusic(grabRock(music)))
This method can definitely be written in a more reusable way that is also easier to read, and solves an important issue, where the executed functions loose track of their context or value of this. This leads us to the following function that will be in charge of chaining these calls in order.
1 |
compose(doFirstAction, doSecondAction, doThirdAction, etc...); |
compose would now apply each function from left to right by passing the result from one function to the next one, and so on. This way you can re-arrange, replace, remove or insert actions as you wish, making it now easier to read and maintain.
Now, the previous query would be written as such:
1 2 |
const bestRockArtists = compose( grabRock, grabBestMusic, pickArtistName ); bestRockArtists(music); |
The compose method implementation looks like this:
1 2 3 4 5 6 7 8 9 10 |
// pass a bunch of functions, return a function var compose = (...funcs) => (...args) => { // traverse the list of functions and apply their result to the next one funcs.forEach((f) => { // apply needs an array, and passes the right context args = [f.apply(this, args)]; }); return args[0]; }; |
Grab the album title from the oldest record
One way to do this, would be to first sort all of the records by release year, putting the earliest (oldest) at the beginning. And then just grabbing the album title from that first record.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// sort function const sortAscending = x => x.sort( (a, b) => { var res = 0; res = a.recording.year < b.recording.year ? -1 : 1; return res; }); const takeFirst = x => x.length > 0 && x[0]; const getAlbumName = x => x.album; const getOldestAlbum = compose(sortAscending, takeFirst, getAlbumName); getOldestAlbum( music ); // output: Let it be |
To make sort work for sub-properties, you need to pass a callback also called the comparator which contains the formula of how you want to sort your items. In this case we need to sort in ascending order.
Get an artist name that starts with the letter ‘A’, and uppercase it
Solving these problems should be exactly like giving orders. You could even just start by writing the solution, and then go from there.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const getArtistsWithA = compose(pickArtistNames, findStartingWithA, convertToUppercase); const findStartingWithA = (content) => content.find((item) => item.startsWith('A')); const pickArtistNames = (content) => content.map( x => x.artist ); const convertToUppercase = (x) => x.toUpperCase(); getArtistsWithA(music); // output: "ANAMANAGUCHI" |
find() Iterates the array until the callback function returns true and returns that element. Very similar to findIndex() which instead of returning the actual element, it returns the index.
Get a unique list of all formats
The first thing we need to do to make this happen is to grab all of the genre arrays of all the records. Then we’re gonna concatenate all those arrays to form a single array, and finally we’re gonna remove all the duplicates.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var getAllFormats = (x) => x.map( item => item.recording.format ); // concatenate all the arrays by accumulating everything into a resulting array // ex. ['a', 'b'].concat( ['y', 'z'] ) = ['a', 'b', 'y','z'] var accumulate = (x) => x.reduce( (acc, next) => acc.concat(next), []); // We're gonna use Sets to create a structure that cannot contain duplicates, and then convert that newly created list to an real array type by using from(). var removeDuplicates = (x) => Array.from(new Set(x)); var uniqueFormats = compose(getAllFormats, accumulate, removeDuplicates); uniqueFormats(music); // output: ["CD", "Album", "Vinyl", "LP", "Stereo", "Gatefold"] |
reduce() executes the callback function for each element in the array, then applies a function against an initial accumulator and each value of the array (from left-to-right) to reduce it to a single value.
Finishing up
If you look closely, by chaining methods we treated functions as operators, basically acting between values. This touches on a very important characteristic of functional programming called higher-order functions. Which are basically functions that take other functions as arguments and/or produce another function as its result. See also pure functions.
Hopefully this exercise served as a good start to explore more ways to write more reusable, and readable code. To go deep into this topic I highly recommend reading books about functional programming, including Dan Mantyla’s book and the great Javascript Allonge.