Becard
Next up is the becard
(aka B3 combinator or function composition), which is written:
(c -> d) -> (b -> c) -> (a -> b) -> a -> d
// f => g => h => a => f(g(h(a)))
This is a good one for accessing data deep in a nested structure. For this example I’m going to use ramda, “a practical functional library for Javascript programmers.” Underscore or lodash could just as well be substituted here, but for some methods the arguments will need to be flipped to achieve the kind of terseness used here, so I’m just going to skip that.
In this example, I’ve got an array of teams
, each with an array of players
, each with a property playerId
. I just want to get a list of the playerId
property values:
const R = require("ramda");
const teams = [{
teamId: 1,
players: [{
name: "John",
playerId: 1
}, {
name: "Steve",
playerId: 2
}]
}, {
teamId: 2,
players: [{
name: "Bob",
playerId: 3
}, {
name: "Jim",
playerId: 4
}, {
name: "Jim",
playerId: 4
}]
}];
const becard = f => g => h => a => f(g(h(a)));
// becard( c -> d )( b -> c )( a -> b )
const idFinder = becard(R.pluck("playerId"))(R.flatten)(R.pluck("players"));
// idFinder( a )
playerIds = idFinder(teams);
// [ 1, 2, 3, 4, 4 ]
Breakdown: teams
is an array of objects. We pass this first to h
(R.pluck("players")
), which return the values of each object’s players
property. Because these values are arrays, the return value after h
has been called is an array of array, each of wich contains many player
objects. To operate on each of these values individually in f
(R.pluck("playerId")
), we then pass this array of arrays to R.flatten
which flattens those arrays of values into a single array of values. After g
has been called, we have a single array which contains the player
objects for both teams. This value is then passed to f
(R.pluck("playerId")
), which returns only the values of each playerId
property from each player
object.
Let’s see what this would look like in long form over time:
const arraysOfPlayers = R.pluck("players")(teams);
// [
// [{
// name: "John",
// playerId: 1
// }, {
// name: "Steve",
// playerId: 2
// }],
// [{
// name: "Bob",
// playerId: 3
// }, {
// name: "Jim",
// playerId: 4
// }, {
// name: "Jim",
// playerId: 4
// }]
// ]
const arrayOfPlayers = R.flatten(arraysOfPlayers);
// [
// {
// name: "John",
// playerId: 1
// }, {
// name: "Steve",
// playerId: 2
// }, {
// name: "Bob",
// playerId: 3
// }, {
// name: "Jim",
// playerId: 4
// }, {
// name: "Jim",
// playerId: 4
// }
// ]
const arrayOfPlayerIds = R.pluck("playerId")(arrayOfPlayers);
// [ 1, 2, 3, 4, 4 ]
To do this without using becard, we could use ramda’s R.compose
method. If becard is the B 3 combinator because it takes 3 functions as arguments, R.compose
is a B n because it takes n functions as arguments:
const idFinder = R.compose(R.pluck("playerId"), R.flatten, R.pluck("players"));
playerIds = idFinder(teams);
// [ 1, 2, 3, 4, 4 ]
I use the R.compose
approach with some frequency because it allows me to pass as many functions as arguments as I want. If we wanted to achieve this with the becard combinator, we would need to nest our becards and fill in any function arguments we don’t need with some placeholder that won’t effect our return value such as the I
combinator (R.identity
here), which I’ll discuss in a later post:
const idFinder = becard(R.pluck("playerId"), R.flatten, R.pluck("players"));
const uniqueIdFinder = becard(R.identity, R.uniq, idFinder);
playerIds = idFinder(teams);
// [ 1, 2, 3, 4, 4 ]
playerIds = uniqueIdFinder(teams);
// [ 1, 2, 3, 4 ]
Or we could just nest the function calls directly:
playerIds = R.pluck("playerId", R.flatten( R.pluck("players", teams)));
// [ 1, 2, 3, 4, 4 ]
But that last version with the function calls nested within one another should be avoided at all costs. Writing code like that is not only uglier, but also harder to understand and harder to refactor later.
Next up: blackbird