Discussed: Tiny modules — Hate of pyramids — Permutations — Simons age and taste in beverages
So, yesterday I published a small ~53 LOC npm module called permuter on Github. Permuter is a small utility for doing permutations between objects and applying rules to the generated permutations.
It was all triggered by this pyramid of nested for loops/if statements:
// Apply mixins
if (r.metadata.mixin) {
for (range in r.metadata.mixin) {
for (version in report.services[r.service]) {
if (lib.semver.satisfies(version, range)) {
for (attribute in r.metadata.mixin[range]) {
report.services[r.service][version][attribute] = r.metadata.mixin[range][attribute];
}
}
}
}
}
I always hate when I have to choose between long ass object-walking report.services[r.service][version][attribute]
or adding lots of temporary reference variables into the scope. So these ten lines of code annoyed me, and thus permuter was born, and allowed me to do this:
lib.permuter(
"mixins", r.metadata.mixin,
report.services[r.service],
function satisfiesRange(mixin, service) {
return lib.semver.satisfies(service.key, mixin.key);
},
"$mixins",
function permutation(mixin, service, attribute) {
service.value[attribute.key] = attribute.value;
});
This doesn't really cut it as an introductory example to permuter, but kind of illustrates how it can be used to solve some problems a teensy bit more elegantly.
A simple example
permuter(
["a", "b"],
[1, 2],
function(letter, number) {
console.log(letter.value, number.value);
}
)
This produces the following output:
a 1
a 2
b 1
b 2
A slightly more advanced example
Here's a slightly more advanced example where we add a blocker function applySomeReasoning
that only accepts permutations where the xFactor
is twice the size of Number
. WE also use a backreference to iterate over the letters of the selected word.
permuter(
[1, 2, 5, 10], // Number
[10, 4], // xFactor
function applySomeReasoning(number, xFactor) {
return number.value*2 == xFactor.value;
},
"words", ["a", "bo"],
"$words", // Letter
function(number, xFactor, word, letter) {
console.log(word.value, letter.value, number.value, xFactor.value);
}
)
The result of this is:
a a 2 4
bo b 2 4
bo o 2 4
a a 5 10
bo b 5 10
bo o 5 10
A more convoluted example
Here is a more convoluted and silly example where backreferences and block functions also are used. Backreferences are used to include the value of a previously selected item in the permutation set. In the example below we will include ["Cola", "Tonic"] when "beverage" is selected and ["Snickers", "Chips"] when "snack" is selected. Blocker functions are used to exclude certain permutations, if "Simon" is too young to see the movie "Splatter" we'll stop those combinations in checkAgeLimit
, likewise we don't force people to eat or drink what they really dislike by adding the blocker checkIntenseDislikes
.
var lib = {
permuter: require('permuter')
}
var people = {
"Hugo": {"age":31},
"Simon": {"age":24, "dislikes":"Tonic"},
}
var snacks = {
"beverage": {
"Cola": {"effect":"refreshed", "leftover":"mug"},
"Tonic": {"effect":"happy", "leftover":"bottle"}
},
"snack": {
"Snickers": {"effect":"sticky", "leftover":"wrapper"},
"Chips": {"effect":"thirsty", "leftover":"bag"}
}
}
var salons = {
"Salon 1": {"movie": "Slasher", "ageLimit": 30},
"Salon 2": {"movie": "Kidpix", "ageLimit": 10}
}
lib.permuter(
people,
"snackType", snacks,
salons,
function checkAgeLimit(person, snackType, salon) {
return person.value.age >= salon.value.ageLimit;
},
"$snackType",
function checkIntenseDislikes(person, snackType, salon, snack) {
return !person.value.dislikes || person.value.dislikes != snack.key;
},
function permutationResult(person, snackType, salon, snack) {
var snackName = snack.key, salonName = salon.key;
salon = salon.value, snack = snack.value;
salon.trash = salon.trash || {};
salon.trash[snack.leftover] = salon.trash[snack.leftover] ?
salon.trash[snack.leftover] + 1 : 1;
console.log(person.key, "goes to", salonName ,"and sees", salon.movie, "and consume the tasty", snackType.key, snackName,
"and will then feel", snack.effect, "and leave a", snack.leftover, "on the floor");
}
);
console.log("\nAfter a long day the staff collects the following from the floor");
lib.permuter(salons, function(salon) {
var trash;
console.log(salon.key + ":");
for (trash in salon.value.trash) {
console.log('\t', trash+':', salon.value.trash[trash]);
}
})
The output from this is:
Hugo goes to Salon 1 and sees Slasher and consume the tasty beverage Cola and will then feel refreshed and leave a mug on the floor
Hugo goes to Salon 1 and sees Slasher and consume the tasty beverage Tonic and will then feel happy and leave a bottle on the floor
Hugo goes to Salon 2 and sees Kidpix and consume the tasty beverage Cola and will then feel refreshed and leave a mug on the floor
Hugo goes to Salon 2 and sees Kidpix and consume the tasty beverage Tonic and will then feel happy and leave a bottle on the floor
Hugo goes to Salon 1 and sees Slasher and consume the tasty snack Snickers and will then feel sticky and leave a wrapper on the floor
Hugo goes to Salon 1 and sees Slasher and consume the tasty snack Chips and will then feel thirsty and leave a bag on the floor
Hugo goes to Salon 2 and sees Kidpix and consume the tasty snack Snickers and will then feel sticky and leave a wrapper on the floor
Hugo goes to Salon 2 and sees Kidpix and consume the tasty snack Chips and will then feel thirsty and leave a bag on the floor
Simon goes to Salon 2 and sees Kidpix and consume the tasty beverage Cola and will then feel refreshed and leave a mug on the floor
Simon goes to Salon 2 and sees Kidpix and consume the tasty snack Snickers and will then feel sticky and leave a wrapper on the floor
Simon goes to Salon 2 and sees Kidpix and consume the tasty snack Chips and will then feel thirsty and leave a bag on the floor
After a long day the staff collects the following from the floor
Salon 1:
mug: 1
bottle: 1
wrapper: 1
bag: 1
Salon 2:
mug: 2
bottle: 1
wrapper: 2
bag: 2
The permuter call with the main data set replaces the following loop:
var firstname, person, snackType, snackList, salonName, salon,
snackName, snack;
for (firstname in people) {
person = people[firstname];
for (snackType in snacks) {
snackList = snacks[snackType];
for (salonName in salons) {
salon = salons[salonName];
if (person.age >= salon.ageLimit) {
for (snackName in snackList) {
snack = snackList[snackName];
if (!person.dislikes || person.dislikes !== snackName) {
salon.trash = salon.trash || {};
salon.trash[snack.leftover] = salon.trash[snack.leftover] ?
salon.trash[snack.leftover] + 1 : 1;
console.log(firstname, "goes to", salonName ,"and sees", salon.movie, "and consume the tasty", snackType, snackName,
"and will then feel", snack.effect, "and leave a", snack.leftover, "on the floor");
}
}
}
}
}
}