JavaScript allows you to use classes for modularization, but it's not easy to make a function just work with a single type. Let's see an example, taken from NodEO, an evolutionary algorithm library, now in a heavy refactorization phase.
export class StringChromosome extends Chromosome {
constructor(aString, fitness) {
super(fitness);
this.stringChr = aString;
}
// more stuff
}
export class FloatChromosome extends Chromosome {
constructor(aVector, fitness = 0) {
super(fitness);
this.floatVector = aVector;
}
// more stuff here
}
A chromosome has a common fitness, that is, how well it solves the problem, plus a data structure that represents the problem; EAs don't tell you which data structure you should use, so different types of chromosomes will have different data structures; we will use different names for those attributes, reflecting what they actually are.
Chromosomes undergo mutation; they essentially change randomly, looking for a better solution. A first version of a mutation operator would look like this:
// defined within FloatChromosome
static mutationRange = 0.2;
tatic mutate(chromosome) {
const floatVector = chromosome.floatVector;
const mutation_point = Math.floor(Math.random() * floatVector.length);
let temp = [...floatVector];
temp[mutation_point] =
temp[mutation_point] -
this.mutationRange / 2 +
Math.random() * this.mutationRange;
return temp;
}
This works as expected, returning a new data structure that can be used to build a new chromosome (chromosomes are immutable, so it will need something else to compute the fitness before we build one).
However, chromosome might or might not be a FloatVector
, so we would need to add some type checks.
if (chromosome.constructor.name !== "FloatChromosome") {
throw new Error(
`${chromosome.constructor.name} is not a FloatChromosome`
);
}
This will throw if we try to mutate a chromosome that has not been built as a FloatChromosome
. So this code:
const sChrom = new StringChromosome("0001", 1);
console.log(FloatChromosome.mutate(sChrom));
Will throw:
file:///home/jmerelo/Code/js/dev.to-js-types/lib/chromosomes.js:30
throw new Error(
^
Error: StringChromosome is not a FloatChromosome
at FloatChromosome.mutate (file:///home/jmerelo/Code/js/dev.to-js-types/lib/chromosomes.js:30:13)
at file:///home/jmerelo/Code/js/dev.to-js-types/script/mutate.js:7:29
at ModuleJob.run (node:internal/modules/esm/module_job:194:25)
However, these type checks are neither complete (what if it does not have a constructor?) nor precise (what if it does actually have a floatVector
attribute, even if it's not been built with that class?).
const notReallyAChrom = { floatVector: [0, 0, 0, 1] };
console.log(FloatChromosome.mutate(notReallyAChrom));
This will still throw, although we would have been perfectly able to work with it:
file:///home/jmerelo/Code/js/dev.to-js-types/lib/chromosomes.js:30
throw new Error(
^
Error: Object is not a FloatChromosome
at FloatChromosome.mutate (file:///home/jmerelo/Code/js/dev.to-js-types/lib/chromosomes.js:30:13)
at file:///home/jmerelo/Code/js/dev.to-js-types/script/mutate.js:7:29
at ModuleJob.run (node:internal/modules/esm/module_job:194:25)
Destructuring FTW
We can easily fix this using destructuring for the function arguments. Let's define the function so that it extracts from the incoming data structure just what we need, and only what we need:
static mutate({ floatVector }) {
if (floatVector === undefined) {
throw new Error("Incorrect data structure: no floatVector attribute");
}
const mutation_point = Math.floor(Math.random() * floatVector.length);
let temp = [...floatVector];
temp[mutation_point] =
temp[mutation_point] -
this.mutationRange / 2 +
Math.random() * this.mutationRange;
return temp;
}
This script:
const fChrom = new FloatChromosome([0, 0, 0, 0], 0);
console.log(FloatChromosome.mutate(fChrom));
const notReallyAChrom = { floatVector: [0, 0, 0, 1] };
console.log(FloatChromosome.mutate(notReallyAChrom));
const wrongChrom = new StringChromosome("010", 3);
console.log(FloatChromosome.mutate(wrongChrom));
Will work correctly until it finds the last chromosome. In that case, it will produce an error. By using destructuring we find (kind of) type safety, since we will reject all invalid data structures at the same time we accept data structures we can work with, whether they are declared as a class or not.
Coda
If you really want type safety, you should go for the real type-safe JavaScript, that is, TypeScript. If you need to stay with JS for some reason (and there are many good reasons to do so), you can achieve a certain kind of cleanliness using this programming pattern.
You can find the final version of the code used in this paper in this repo. Previous versions can be found in the commit history.
Top comments (1)
I like destructuring a lot 😍😍😍
Lately I am using a bit less in two cases: