In the advanced laboratory of Jurassic Park, we don't always know whether a DNA sample exists, whether the analysis succeeded, or whether the data is complete. Instead of checking conditions at every step (if/else), we can use elegant patterns from functional programming - functors and monads. They are like intelligent sample containers that know how to handle missing or damaged data on their own.
A functor is an object (container) that implements a
map method. It allows applying transformations to the value inside the container without having to extract it.1// The simplest functor - Box (container)
2class Box {
3 constructor(value) {
4 this.value = value;
5 }
6
7 map(fn) {
8 return new Box(fn(this.value));
9 }
10
11 toString() {
12 return 'Box(' + this.value + ')';
13 }
14}
15
16// Usage - chain of transformations
17const result = new Box(8000)
18 .map((weight) => weight / 1000) // Box(8)
19 .map((tons) => tons + ' tons') // Box('8 tons')
20 .map((label) => 'Weight: ' + label); // Box('Weight: 8 tons')
21
22console.log(result.value); // 'Weight: 8 tons'The Maybe monad solves the problem of null/undefined values. Instead of checking
if (x !== null) everywhere, we wrap the value in a Maybe and let the monad handle missing data:1class Maybe {
2 constructor(value) {
3 this.value = value;
4 }
5
6 static of(value) {
7 return new Maybe(value);
8 }
9
10 isNothing() {
11 return this.value === null || this.value === undefined;
12 }
13
14 map(fn) {
15 return this.isNothing() ? this : Maybe.of(fn(this.value));
16 }
17
18 flatMap(fn) {
19 return this.isNothing() ? this : fn(this.value);
20 }
21
22 getOrElse(defaultValue) {
23 return this.isNothing() ? defaultValue : this.value;
24 }
25}
26
27// Without Maybe - full of ifs
28function getDinoZoneOld(park, dinoId) {
29 const dino = park.dinosaurs.find((d) => d.id === dinoId);
30 if (!dino) return 'Unknown';
31 const zone = park.zones.find((z) => z.id === dino.zoneId);
32 if (!zone) return 'Unknown';
33 return zone.name;
34}
35
36// With Maybe - elegant chain
37function getDinoZone(park, dinoId) {
38 return Maybe.of(park.dinosaurs.find((d) => d.id === dinoId))
39 .map((dino) => dino.zoneId)
40 .flatMap((zoneId) => Maybe.of(park.zones.find((z) => z.id === zoneId)))
41 .map((zone) => zone.name)
42 .getOrElse('Unknown');
43}Either represents a value that can be a success (Right) or a failure (Left). It's a functional alternative to try/catch:
1class Left {
2 constructor(value) { this.value = value; }
3 map(fn) { return this; } // Ignores transformations
4 flatMap(fn) { return this; }
5 getOrElse(defaultValue) { return defaultValue; }
6 isLeft() { return true; }
7 isRight() { return false; }
8}
9
10class Right {
11 constructor(value) { this.value = value; }
12 map(fn) { return new Right(fn(this.value)); }
13 flatMap(fn) { return fn(this.value); }
14 getOrElse(defaultValue) { return this.value; }
15 isLeft() { return false; }
16 isRight() { return true; }
17}
18
19// Usage in DNA sample validation
20function validateSample(sample) {
21 if (!sample.id) return new Left('Missing sample ID');
22 if (sample.purity < 0.5) return new Left('Purity too low: ' + sample.purity);
23 if (!sample.species) return new Left('Unknown species');
24 return new Right(sample);
25}
26
27function processSample(sample) {
28 return validateSample(sample)
29 .map((s) => ({ ...s, processed: true }))
30 .map((s) => ({ ...s, label: s.species + '-' + s.id }));
31}
32
33// Success
34const good = processSample({ id: 'S001', purity: 0.95, species: 'T-Rex' });
35good.getOrElse('Error'); // { id: 'S001', purity: 0.95, species: 'T-Rex', processed: true, label: 'T-Rex-S001' }
36
37// Failure
38const bad = processSample({ id: 'S002', purity: 0.3, species: 'Raptor' });
39bad.getOrElse('Error'); // 'Error' (error: 'Purity too low: 0.3')