Consider the following code snippet:
if (msg.operation == 'create') {
model.blocks.push(msg.block)
drawBlock(msg.block);
} else if (msg.operation == 'select' && msg.properties.snap == 'arbitrary') {
doStuff(msg.properties.x, msg.properties.y);
} else if (msg.operation == 'unselect') {
doOtherStuff(msg.properties.geometry);
}
Is there a way to refactor this so I can pattern match on msg, akin to the following invalid code:
msg match {
case { operation: 'create', block: b } =>
model.blocks.push(b);
drawBlock(b);
case { operation: 'select', properties: { snap: 'arbitrary', x: sx, y: sy } } =>
doStuff(sx, sy);
case { operation: 'unselect', properties: { snap: 'specific' }, geometry: geom } =>
doOtherStuff(geom);
}
Alternatively, what would be the most idiomatic way of achieving this in ES6, without the ugly if-then-else chain?
Update. Granted that this is a simplistic example where a full-blown pattern matching is probably unneeded. But one can imagine a scenario of matching arbitrary hierarchical pieces of a long AST.
TL;DR; the power of destructuring, accompanied with an automatic check if it is possible to do it or not.
You could write a match function like this, which (when combined with arrow functions and object destructuring) is fairly similar to the syntax your example:
/**
* Called as:
* match(object,
* pattern1, callback1,
* pattern2, callback2,
* ...
* );
**/
function match(object, ...args) {
for(let i = 0; i + 1 < args.length; i += 2) {
const pattern = args[i];
const callback = args[i+1];
// this line only works when pattern and object both are JS objects
// you may want to replace it with a more comprehensive check for
// all types (objects, arrays, strings, null/undefined etc.)
const isEqual = Object.keys(pattern)
.every((key) => object[key] === pattern[key]);
if(isEqual)
return callback(object);
}
}
// -------- //
const msg = { operation: 'create', block: 17 };
match(msg,
{ operation: 'create' }, ({ block: b }) => {
console.log('create', b);
},
{ operation: 'select-block' }, ({ id: id }) => {
console.log('select-block', id);
},
{ operation: 'unselect-block' }, ({ id: id }) => {
console.log('unselect-block', id);
}
);
You can use a higher order function and destructuring assignment to get something remotely similar to pattern matching:
const _switch = f => x => f(x);
const operationSwitch = _switch(({operation, properties: {snap, x, y, geometry}}) => {
switch (operation) {
case "create": {
let x = true;
return operation;
}
case "select": {
let x = true;
if (snap === "arbitrary") {
return operation + " " + snap;
}
break;
}
case "unselect": {
let x = true;
return operation;
}
}
});
const msg = {operation: "select", properties: {snap: "arbitrary", x: 1, y: 2, geometry: "foo"}};
console.log(
operationSwitch(msg) // select arbitrary
);
By putting the switch statement in a function we transformed it to a lazy evaluated and reusable switch expression.
_switch comes from functional programming and is usually called apply or A. Please note that I wrapped each case into brackets, so that each code branch has its own scope along with its own optional let/const bindings.
If you want to pass _switch more than one argument, just use const _switchn = f => (...args) => f(args) and adapt the destructuring to [{operation, properties: {snap, x, y, geometry}}].
However, without pattern matching as part of the language you lose many of the nice features:
if you change the type of msg, there are no automatic checks and _switch may silently stop working
there are no automatic checks if you covering all cases
there are no checks on tag name typos
The decisive question is whether it is worth the effort to introduce a technique that is somehow alien to Javascript.
I think #gunn's answer is onto something good here, but the primary issue I have with his code is that it relies upon a side-effecting function in order to produce a result – his match function does not have a useful return value.
For the sake of keeping things pure, I will implement match in a way that returns a value. In addition, I will also force you to include an else branch, just the way the ternary operator (?:) does - matching without an else is reckless and should be avoided.
Caveat: this does not work for matching on nested data structures but support could be added
// match.js
// only export the match function
const matchKeys = x => y =>
Object.keys(x).every(k => x[k] === y[k])
const matchResult = x => ({
case: () => matchResult(x),
else: () => x
})
const match = x => ({
case: (pattern, f) =>
matchKeys (pattern) (x) ? matchResult(f(x)) : match(x),
else: f => f(x)
})
// demonstration
const myfunc = msg => match(msg)
.case({operation: 'create'}, ({block}) => ['create', block])
.case({operation: 'select-block'}, ({id}) => ['select-block', id])
.case({operation: 'unselect-block'}, ({id}) => ['unselect-block', id])
.else( (msg) => ['unmatched-operation', msg])
const messages = [
{operation: 'create', block: 1, id: 2},
{operation: 'select-block', block: 1, id: 2},
{operation: 'unselect-block', block: 1, id: 2},
{operation: 'other', block: 1, id: 2}
]
for (let m of messages)
// myfunc returns an actual value now
console.log(myfunc(m))
// [ 'create', 1 ]
// [ 'select-block', 2 ]
// [ 'unselect-block', 2 ]
// [ 'unmatched-operation', { operation: 'other', block: 1, id: 2 } ]
not quite pattern matching
Now actual pattern matching would allow us to destructure and match in the same expression – due to limitations of JavaScript, we have to match in one expression and destructure in another. Of course this only works on natives that can be destructured like {} and [] – if a custom data type was used, we'd have to dramatically rework this function and a lot of conveniences would be lost.
Sure, why not?
function match(object) {
this.case = (conditions, fn)=> {
const doesMatch = Object.keys(conditions)
.every(k=> conditions[k]==object[k])
if (doesMatch) fn(object)
return this
}
return this
}
// Example of use:
const msg = {operation: 'create', block: 5}
match(msg)
.case({ operation: 'create'}, ({block})=> console.log('create', block))
.case({ operation: 'select-block'}, ({id})=> console.log('select-block', id))
.case({ operation: 'unselect-block'}, ({id})=> console.log('unselect-block', id))
Given there's no easy way to properly do this until TC39 implemented switch pattern matching comes along, the best bet is libraries for now.
loadash
Go ol' loadash has a nice _.cond function:
var func = _.cond([
[_.matches({ 'a': 1 }), _.constant('matches A')],
[_.conforms({ 'b': _.isNumber }), _.constant('matches B')],
[_.stubTrue, _.constant('no match')]
]);
func({ 'a': 1, 'b': 2 });
// => 'matches A'
func({ 'a': 0, 'b': 1 });
// => 'matches B'
func({ 'a': '1', 'b': '2' });
// => 'no match'
patcom
One of the recommended libraries to look at, which has feature parity with the TC39 proposal for switch pattern matching, patcom, is quite small and nicely written - this is the main index.js:
import { oneOf } from './matchers/index.js'
export * from './matchers/index.js'
export * from './mappers.js'
export const match =
(value) =>
(...matchers) => {
const result = oneOf(...matchers)(value)
return result.value
}
Here's the simple example:
import {match, when, otherwise, defined} from 'patcom'
function greet(person) {
return match (person) (
when (
{ role: 'student' },
() => 'Hello fellow student.'
),
when (
{ role: 'teacher', surname: defined },
({ surname }) => `Good morning ${surname} sensei.`
),
otherwise (
() => 'STRANGER DANGER'
)
)
}
So for yours something like this should work:
match (msg) (
when ({ operation: 'create' }), ({ block: b }) => {
model.blocks.push(b);
drawBlock(b);
}),
when ({ operation: 'select', properties: { snap: 'arbitrary' } }), ({ properties: { x: sx, y: sy }}) =>
doStuff(sx, sy)
)
when ({ operation: 'unselect', properties: { snap: 'specific' } }, ({ geometry: geom }) =>
doOtherStuff(geom)
)
)
match-iz
For people wanting to implement the whole thing themselves there is a recommended small library match-iz that implements functional pattern matching in currently 194 lines.
Supercharged switch
I'm wondering if something like this 'supercharged switch' might get close to what your after:
const match = (msg) => {
const { operation, properties: { snap } } = msg;
switch (true) {
case operation === 'create':
model.blocks.push(b);
drawBlock(b);
break;
case operation === 'select' && snap === 'arbitrary':
const { properties: { x: sx, y: sy }} = msg;
doStuff(sx, sy);
break;
case operation === 'unselect' && snap === 'specific':
const { geometry: geom } = msg;
doOtherStuff(geom)
break;
}
}
Reducers
Also the whole concept of matching on strings within objects and then running a function based on that sounds a lot like Redux reducers.
From an earlier answer of mine about reducers:
const operationReducer = function(state, action) {
const { operation, ...rest } = action
switch (operation) {
case 'create':
const { block: b } = rest
return createFunc(state, b);
case 'select':
case 'unselect':
return snapReducer(state, rest);
default:
return state;
}
};
const snapReducer = function(state, action) {
const { properties: { snap } } = action
switch (snap) {
case 'arbitrary':
const { properties: { x: sx, y: sy } } = rest
return doStuff(state, sx, sy);
case 'specific':
const { geometry: geom } = rest
return doOtherStuff(state, geom);
default:
return state;
}
};
Related
I have this kind of schema:
const schema = {
actions: {
ident: {
action: (v) => v,
path: 'some.path.key',
},
mul: {
action: (v) => v * 2,
path: 'some.other.path.key',
},
},
};
And a helper function, that takes object with keys present in schema actions, e.g:
const obj = {
ident: 1,
mul: 2,
}
const res = helper(schema, obj);
/* res */
{
some: {
path: {
key: 1,
},
other: {
path: {
key: 4,
}
}
},
}
And construct a new object with a function applied to the value.
Sometimes i need a behavior when both keys present in source object, e.g:
const schema2 = {
actions: {
ident: {
action: (v) => v,
path: 'some.path.key',
},
mul: {
action: (v) => v * 2,
path: 'some.other.path.key',
},
'mul:ident': {
action: (v1, v2) => v1/v2,
path: 'key',
}
},
};
In case like this, i need the result object to be:
const obj = {
ident: 1,
mul: 2,
}
const res = helper(schema, obj);
/* res */
{
key: 2 // 2/1 == 2
}
How can i implement such conditional logic in a good way?
I'd traverse your actions backwards, then delete the keys from the input, and skip the action if the keys are missing:
const input = { ...obj };
const output = {};
// you might want to sort the keys in the desired order first (e.g. by the number of parameters)
for(const [key, { action, path }] of Object.entries(schema.actions).reverse()) {
const keys = key.split(".");
// If one ov the values is missing, another action already consumed it
if(keys.some(key => !(key in input))
continue;
// consume all keys into values
const values = keys.map(key => {
const value = input[key];
delete input[key];
});
// TODO: assign path to output correctly
output[path] = action(...values);
}
Given an object that may be null and may have the following properties:
{
templateId: "template1",
templates: {
template1: "hello"
}
}
How would you get the template in a failsafe way? (templateId might not be defined, or the template it reffers might be undefined)
I use ramda and was trying to adapt my naive version of the code to use something like a Maybe adt to avoid explicit null/undefined checks.
I'm failing to come up with an elegant and clean solution.
naive ramda version:
const getTemplate = obj => {
const templateId = obj && prop("templateId", obj);
const template = templateId != null && path(["template", templateId], obj);
return template;
}
this does work but I would like to avoid null checks, as my code has a lot more going on and it would be really nice to become cleaner
Edit
I get from severall answers that the best is to ensure clean data first.
That's not allways possible though.
I also came up with this, which I do like.
const Empty=Symbol("Empty");
const p = R.propOr(Empty);
const getTemplate = R.converge(p,[p("templateId"), p("templates")]);
Would like to get feedback regarding how clean and how readable it is (and if there are edge cases that would wreck it)
As others have told you, ugly data precludes beautiful code. Clean up your nulls or represent them as option types.
That said, ES6 does allow you to handle this with some heavy destructuring assignment
const EmptyTemplate =
Symbol ()
const getTemplate = ({ templateId, templates: { [templateId]: x = EmptyTemplate } }) =>
x
console.log
( getTemplate ({ templateId: "a", templates: { a: "hello" }}) // "hello"
, getTemplate ({ templateId: "b", templates: { a: "hello" }}) // EmptyTemplate
, getTemplate ({ templates: { a: "hello" }}) // EmptyTemplate
)
You can continue to make getTemplate even more defensive. For example, below we accept calling our function with an empty object, and even no input at all
const EmptyTemplate =
Symbol ()
const getTemplate =
( { templateId
, templates: { [templateId]: x = EmptyTemplate } = {}
}
= {}
) =>
x
console.log
( getTemplate ({ templateId: "a", templates: { a: "hello" }}) // "hello"
, getTemplate ({ templateId: "b", templates: { a: "hello" }}) // EmptyTemplate
, getTemplate ({ templates: { a: "hello" }}) // EmptyTemplate
, getTemplate ({}) // EmptyTemplate
, getTemplate () // EmptyTemplate
)
Above, we start to experience a little pain. This signal is important not to ignore as it warns us we're doing something wrong. If you have to support that many null checks, it indicates you need to tighten down the code in other areas of your program. It'd be unwise to copy/paste any one of these answers verbatim and miss the lesson everyone is trying to teach you.
Here is an ADT approach in vanilla Javascript:
// type constructor
const Type = name => {
const Type = tag => Dcons => {
const t = new Tcons();
t[`run${name}`] = Dcons;
t.tag = tag;
return t;
};
const Tcons = Function(`return function ${name}() {}`) ();
return Type;
};
const Maybe = Type("Maybe");
// data constructor
const Just = x =>
Maybe("Just") (cases => cases.Just(x));
const Nothing =
Maybe("Nothing") (cases => cases.Nothing);
// typeclass functions
Maybe.fromNullable = x =>
x === null
? Nothing
: Just(x);
Maybe.map = f => tx =>
tx.runMaybe({Just: x => Just(f(x)), Nothing});
Maybe.chain = ft => tx =>
tx.runMaybe({Just: x => ft(x), Nothing});
Maybe.compk = ft => gt => x =>
gt(x).runMaybe({Just: y => ft(y), Nothing});
// property access
const prop =
k => o => o[k];
const propSafe = k => o =>
k in o
? Just(o[k])
: Nothing;
// auxiliary function
const id = x => x;
// test data
// case 1
const o = {
templateId: "template1",
templates: {
template1: "hello"
}
};
// case 2
const p = {
templateId: null
};
// case 3
const q = {};
// case 4
const r = null; // ignored
// define the action (a function with a side effect)
const getTemplate = o => {
const tx = Maybe.compk(Maybe.fromNullable)
(propSafe("templateId"))
(o);
return Maybe.map(x => prop(x) (o.templates)) (tx);
};
/* run the effect,
that is what it means to compose functions that may not produce a value */
console.log("case 1:",
getTemplate(o).runMaybe({Just: id, Nothing: "N/A"})
);
console.log("case 2:",
getTemplate(p).runMaybe({Just: id, Nothing: "N/A"})
);
console.log("case 3:",
getTemplate(q).runMaybe({Just: id, Nothing: "N/A"})
);
As you can see I use functions to encode ADTs, since Javascript doesn't support them on the language level. This encoding is called Church/Scott encoding. Scott encoding is immutable by design and once you are familiar with it, its handling is a piece of cake.
Both Just values and Nothing are of type Maybe and include a tag property on which you can do pattern matching.
[EDIT]
Since Scott (not the encoding guy from just now) and the OP asked for a more detailed reply I extended my code. I still ignore the case where the object itself is null. You have to take care of this in a preceding step.
You may think this is overengineered - with certainty for this contrived example. But when complexity grows, these functional style can ease the pain. Please also note that we can handle all kinds of effects with this approach, not just null checks.
I am currently building an FRP solution, for instance, which is essentially based on the same building blocks. This repetition of patterns is one of the traits of the functional paradigm I would not want to do without anymore.
You can use R.pathOr. Whenever any part of the path isn't available, a default value is returned. For example:
const EmptyTemplate = Symbol();
const getTemplateOrDefault = obj => R.pathOr(
EmptyTemplate,
[ "templates", obj.templateId ],
obj
);
A collection of tests can be found in this snippet. The example shows that pathOr handles all (?) "wrong" cases quite well:
const tests = [
{ templateId: "a", templates: { "a": 1 } }, // 1
{ templates: { "a": 1 } }, // "empty"
{ templateId: "b", templates: { "a": 1 } }, // "empty"
{ templateId: null, templates: { "a": 1 } }, // "empty"
{ templateId: "a", templates: { } }, // "empty"
{ templateId: "a" } // "empty"
];
Edit: To support null or undefined inputs, you could compose the method with a quick defaultTo:
const templateGetter = compose(
obj => pathOr("empty", [ "templates", obj.templateId ], obj),
defaultTo({})
);
Try this,
const input = {
templateId: "template1",
templates: {
template1: "hello"
}
};
const getTemplate = (obj) => {
const template = obj.templates[obj.templateId] || "any default value / simply remove this or part";
//use below one if you think templates might be undefined too,
//const template = obj.templates && obj.templates[obj.templateId] || "default value"
return template;
}
console.log(getTemplate(input));
You can use a combination of && and || to short-circuit an expression.
Also, use [] (instead of .) with objects to get the value if the key is stored in a variable.
Complete check
const getTemplate = (obj) => {
const template = obj && obj.templateId && obj.templates && obj.templates[obj.templateId] || "default value"
return template;
}
I created a general function called unique() to remove duplicates from a specific array.
However I'm facing a problem: I want to build the conditions dynamically based on properties that I pass to the function.
Ex: Let's suppose that I want to pass 2 properties, so I want to check these 2 properties before "remove" that duplicated object.
Currently I'm using eval() to build this condition "&&", however according to my search it's really a bad practice.
So, my question is:
What's the proper way to do this kind of thing?
Below is my current code:
function unique(arr, ...props) {
const conditions = [];
for (let prop of props) {
conditions.push(`element['${prop}'] === elem['${prop}']`);
}
const condStr = conditions.join(' && ');
return arr.filter((element, index) => {
const idx = arr.findIndex((elem) => {
return eval(condStr);
});
return idx === index;
});
}
const arr1 = [{
id: 1,
name: 'Josh',
description: 'A description'
}, {
id: 2,
name: 'Hannah',
description: 'A description#2'
}, {
id: 1,
name: 'Josh',
description: 'A description#3'
}, {
id: 5,
name: 'Anyname',
description: 'A description#4'
}];
const uniqueValues = unique(arr1, 'id', 'name');
console.log('uniqueValues', uniqueValues);
This question is a bit subjective as far as implementation details, but the better way if you ask me is to pass in a callback function to hand over to filter.
In doing it this way, you can compose the function anyway you see fit. If you have a complex set of conditions you can use composition to build the conditions in the function before you pass it into your unique function https://hackernoon.com/javascript-functional-composition-for-every-day-use-22421ef65a10
A key to function composition is having functions that are composable. A composable function should have 1 input argument and 1 output value.
The hackernoon article is pretty good and goes much further in depth.
this will return a single function that applies all of your preconditions
function unique(arr, callback) {
return arr.filter(callback);
}
const compose = (...functions) => data =>
functions.reduceRight((value, func) => func(value), data)
unique(
[1, 3, 4, 5 ,7, 11, 19teen]
compose(
(someStateCondition) => { /** return true or false **/ },
(result) => { /** return result === someOtherStateCondition **/}
)
)
Use Array#every to compare all properties inline:
function unique(arr, ...props) {
return arr.filter((element, index) => {
const idx = arr.findIndex(
elem => props.every(prop => element[prop] === elem[prop]);
);
return idx === index;
});
}
Ex:
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
Expected Result: const result = groupBy(arr, 'group', 'question.templateId');
const result = [
[{
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 100
}
}],
[{
group: 1,
question: {
templateId: 300
}
}],
[{
group: 2,
question: {
templateId: 200
}
}]
];
So far: I am able to group the result by a single property using Array.prototype.reduce().
function groupBy(arr, key) {
return [...arr.reduce((accumulator, currentValue) => {
const propVal = currentValue[key],
group = accumulator.get(propVal) || [];
group.push(currentValue);
return accumulator.set(propVal, group);
}, new Map()).values()];
}
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
const result = groupBy(arr, 'group');
console.log(result);
I would recommend to pass a callback function instead of a property name, this allows you to do the two-level-access easily:
function groupBy(arr, key) {
return Array.from(arr.reduce((accumulator, currentValue) => {
const propVal = key(currentValue),
// ^^^^ ^
group = accumulator.get(propVal) || [];
group.push(currentValue);
return accumulator.set(propVal, group);
}, new Map()).values());
}
Now you can do groupBy(arr, o => o.group) and groupBy(arr, o => o.question.templateId).
All you need to do for getting to your expected result is group by the first property and then group each result by the second property:
function concatMap(arr, fn) {
return [].concat(...arr.map(fn));
}
const result = concatMap(groupBy(arr, o => o.group), res =>
groupBy(res, o => o.question.templateId)
);
#Bergi's answer is really practical but I'll show you how building a multi-value "key" can be possible using JavaScript primitives – don't take this to mean Bergi's answer is bad in anyway; in fact, it's actually a lot better because of it's practicality. If anything, this answer exists to show you how much work is saved by using an approach like his.
I'm going to go over the code bit-by-bit and then I'll have a complete runnable demo at the end.
compound data equality
Comparing compound data in JavaScript is a little tricky, so we're gonna need to figure out a way around this first:
console.log([1,2] === [1,2]) // false
I want to cover a solution for the multi-value key because our entire answer will be based upon it - here I'm calling it a CollationKey. Our key holds some value and defines its own equality function which is used for comparing keys
const CollationKey = eq => x => ({
x,
eq: ({x: y}) => eq(x, y)
})
const myKey = CollationKey (([x1, x2], [y1, y2]) =>
x1 === y1 && x2 === y2)
const k1 = myKey([1, 2])
const k2 = myKey([1, 2])
console.log(k1.eq(k2)) // true
console.log(k2.eq(k1)) // true
const k3 = myKey([3, 4])
console.log(k1.eq(k3)) // false
wishful thinking
Now that we have a way to compare compound data, I want to make a custom reducing function that uses our multi-value key to group values. I'll call this function collateBy
// key = some function that makes our key
// reducer = some function that does our reducing
// xs = some input array
const collateBy = key => reducer => xs => {
// ...?
}
// our custom key;
// equality comparison of `group` and `question.templateId` properties
const myKey = CollationKey ((x, y) =>
x.group === y.group
&& x.question.templateId === y.question.templateId)
const result =
collateBy (myKey) // multi-value key
((group=[], x) => [...group, x]) // reducing function: (accumulator, elem)
(arr) // input array
So now that we know how we want collateBy to work, let's implement it
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
const k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation())
}
Collation data container
Ok, so we were being a little optimistic there too using Collation() as the starting value for the xs.reduce call. What should Collation be?
What we know:
someCollation.set accepts a CollationKey and some value, and returns a new Collation
someCollation.get accepts a CollationKey and returns some value
Well let's get to work!
const Collation = (pairs=[]) => ({
has (key) {
return pairs.some(([k, v]) => key.eq(k))
},
get (key) {
return (([k, v]=[]) => v)(
pairs.find(([k, v]) => k.eq(key))
)
},
set (key, value) {
return this.has(key)
? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
: Collation([...pairs, [key, value]])
},
})
finishing up
So far our collateBy function returns a Collation data container which is internally implemented with an array of [key, value] pairs, but what we really want back (according to your question) is just an array of values
Let's modify collateBy in the slightest way that extracts the values – changes in bold
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
let k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation()).values()
}
So now we will add the values method to our Collation container
values () {
return pairs.map(([k, v]) => v)
}
runnable demo
That's everything, so let's see it all work now – I used JSON.stringify in the output so that the deeply nested objects would display all content
// data containers
const CollationKey = eq => x => ({
x,
eq: ({x: y}) => eq(x, y)
})
const Collation = (pairs=[]) => ({
has (key) {
return pairs.some(([k, v]) => key.eq(k))
},
get (key) {
return (([k, v]=[]) => v)(
pairs.find(([k, v]) => k.eq(key))
)
},
set (key, value) {
return this.has(key)
? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
: Collation([...pairs, [key, value]])
},
values () {
return pairs.map(([k, v]) => v)
}
})
// collateBy
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
const k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation()).values()
}
// custom key used for your specific collation
const myKey =
CollationKey ((x, y) =>
x.group === y.group
&& x.question.templateId === y.question.templateId)
// your data
const arr = [ { group: 1, question: { templateId: 100 } }, { group: 2, question: { templateId: 200 } }, { group: 1, question: { templateId: 100 } }, { group: 1, question: { templateId: 300 } } ]
// your answer
const result =
collateBy (myKey) ((group=[], x) => [...group, x]) (arr)
console.log(result)
// [
// [
// {group:1,question:{templateId:100}},
// {group:1,question:{templateId:100}}
// ],
// [
// {group:2,question:{templateId:200}}
// ],
// [
// {group:1,question:{templateId:300}}
// ]
// ]
summary
We made a custom collation function which uses a multi-value key for grouping our collated values. This was done using nothing but JavaScript primitives and higher-order functions. We now have a way to iterate thru a data set and collate it in an arbitrary way using keys of any complexity.
If you have any questions about this, I'm happy to answer them ^_^
#Bergi's answer is great if you can hard-code the inputs.
If you want to use string inputs instead, you can use the sort() method, and walk the objects as needed.
This solution will handle any number of arguments:
function groupBy(arr) {
var arg = arguments;
return arr.sort((a, b) => {
var i, key, aval, bval;
for(i = 1 ; i < arguments.length ; i++) {
key = arguments[i].split('.');
aval = a[key[0]];
bval = b[key[0]];
key.shift();
while(key.length) { //walk the objects
aval = aval[key[0]];
bval = bval[key[0]];
key.shift();
};
if (aval < bval) return -1;
else if(aval > bval) return 1;
}
return 0;
});
}
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
const result = groupBy(arr, 'group', 'question.templateId');
console.log(result);
I have a sequence of objects that I need to asynchronously modify by adding a property to each object:
[{ id: 1 }, { id: 2 }] => [{ id: 1, foo: 'bar' }, { id: 2, foo: 'bar' }]
The synchronous equivalent of this would be:
var xs = [{ id: 1 }, { id: 2 }];
// Warning: mutation!
xs.forEach(function (x) {
x.foo = 'bar';
});
var newXs = xs;
However, in my case I need to append the foo property asynchronously. I would like the end value to be a sequence of objects with the foo property added.
I came up with the following code to solve this problem. In this example I'm just adding a property to each object with a value of bar.
var xs = Rx.Observable.fromArray([{ id: 1 }, { id: 2 }]);
var propertyValues = xs
// Warning: mutation!
.flatMap(function (x) {
return Rx.Observable.return('bar');
});
var newXs =
.zip(propertyValues, function (x, propertyValue) {
// Append the property here
x.foo = propertyValue;
return x;
})
.toArray();
newXs.subscribe(function (y) { console.log(y); });
Is this the best way to solve my problem, or does Rx provide a better means for asynchronously mutating objects in a sequence? I'm looking for a cleaner solution because I have a deep tree that I need to mutate, and this code quickly becomes unweidly:
var xs = Rx.Observable.fromArray([{ id: 1, blocks: [ {} ] }, { id: 2, blocks: [ {} ] } ]);
var propertyValues = xs
// Warning: mutation!
.flatMap(function (x) {
return Rx.Observable.fromArray(x.blocks)
.flatMap(function (block) {
var blockS = Rx.Observable.return(block);
var propertyValues = blockS.flatMap(function (block) {
return Rx.Observable.return('bar');
});
return blockS.zip(propertyValues, function (block, propertyValue) {
block.foo = propertyValue;
return block;
});
})
.toArray();
});
xs
.zip(propertyValues, function (x, propertyValue) {
// Rewrite the property here
x.blocks = propertyValue;
return x;
})
.toArray()
.subscribe(function (newXs) { console.log(newXs); });
Perhaps I shouldn't be performing this mutation in the first place?
Is there a reason you need to create two separate Observables: one for the list you're updating and one for the resulting value?
If you simply perform a .map() over your original list, you should be able to asynchronously update the list and subscribe to the result:
// This is the function that generates the new property value
function getBlocks(x) { ... }
const updatedList$ = Rx.Observable.fromArray(originalList)
// What we're essentially doing here is scheduling work
// to be completed for each item
.map(x => Object.assign({}, x, { blocks: getBlocks(x)}))
.toArray();
// Finally we can wait for our updatedList$ observable to emit
// the modified list
updatedList$.subscribe(list => console.log(list));
To abstract this functionality, I created a helper function that will explicitly schedule work to occur for each item using setTimeout:
function asyncMap(xs, fn) {
return Rx.Observable.fromArray(xs)
.flatMap(x => {
return new Rx.Observable.create(observer => {
setTimeout(() => {
observer.onNext(fn(x));
observer.completed();
}, 0);
});
})
.toArray();
}
You can use this function to schedule work to be completed for each item:
function updateItem(x) {
return Object.assign({}, x, { blocks: getBlocks(x) }
}
var updatedList$ = asyncMap(originalList, updateItem);
updateList$.subscribe(newList => console.log(newList));