I'm looking into how to apply functional programming in javascript, and I'm experimenting with trying to avoid using the class
keyword.
I'm not a fan of going to extreme measures just for the sake of following some paradigm, but I'm curious to see if it's possible to write good code without using classes.
I've had success so far using mainly functions, but there is one situation that I can't really figure out.
When we have behavior that we want to re-use between different objects, we usually (in OOP) create a class that extends another class.
class FlyingThing {
private let _isFlying = false
fly() {
_isFlying = true
return this
}
land() {
_isFlying = false
return this
}
isFlying() {
return _isFlying
}
}
class Duck extends FlyingThing {
quack() {
return 'Quack!'
}
}
const duck = new Duck()
console.log(duck.fly().quack())
Now to the functional approach...
Example taken from: https://medium.com/javascript-scene/functional-mixins-composing-software-ffb66d5e731c
const flying = o => {
let isFlying = false
return Object.assign({}, o, {
fly () {
isFlying = true
return this
},
isFlying: () => isFlying,
land () {
isFlying = false
return this
}
})
}
const quacking = quack => o => Object.assign({}, o, {
quack: () => quack
})
const createDuck = quack => quacking(quack)(flying({}))
const duck = createDuck('Quack!')
console.log(duck.fly().quack())
Ok, I like this idea; we're using composition and we don't have tight coupling between any parents and children. Cool.
However, normally when we use classes, the child has access to the parent's members, and might need to use it in some methods. For example:
class FlyingThing {
private let _isFlying = false
fly() {
_isFlying = true
return this
}
land() {
_isFlying = false
return this
}
isFlying() {
return _isFlying
}
}
class Duck extends FlyingThing {
quack() {
return 'Quack!'
}
// New method - Depends on 'isFlying' defined in parent
layEgg() {
if(isFlying) return
return 'Laying egg...'
}
}
const duck = new Duck()
console.log(duck.fly().quack())
So the question is, how do we solve this elegantly using only functions?
The mechanics behind a possible solution, the OP is looking for, still is OO as OO can be; after all, what one is dealing with, is object composition (or object/type augmentation) via invoking javascript functions. Eric Elliott - functional mixin - and Douglas Crockford - functional inheritance - each do explain their approach pretty well. They might have missed with the naming/labeling. In my opinion it should be as simple as function based mixin. There will be less confusion amongst JavaScript developers since the term functional will not anymore point to or mislead into »The Land of FP«.
The mighty power of a JavaScript function
comes with each of its abilities of 1stly preserving scope via creating closures and 2ndly accessing context via this
and providing the former via one of its call methods call
or apply
. At 3rd being a first class object itself that can be passed around just rounds off the complete package.
The OP's problem of how to implement a modularized behavior that has dependencies to state that is encapsulated by another behavior could be solved by passing this state around. This state must not necessarily ever be publicly displayed.
Eric's and Douglas' concept will be honored/acknowledged by literally applying it.
In my opinion a modularized composable behavior in JavaScript always should be provided by a function that neither should be invoked via the new
keyword nor should be invoked by the call operator ... ()
.., but it always must be applied to/onto other objects/types via either call
or apply
.
OP's example code with shared but protected (locally scoped) flight state ...
function withFlightStateAlteringFlightCapability(state) {
const flightCapableType = this;
flightCapableType.fly = () => {
state.flying = true;
return flightCapableType;
};
flightCapableType.land = () => {
state.flying = false;
return flightCapableType;
};
flightCapableType.isFlying = () => state.flying;
return flightCapableType;
}
function withFlightStateDependedEggLayingBehavior(state) {
const oviparousType = this;
oviparousType.layEgg = () => {
let returnValue;
// if (!this.isFlying()) {
if (!state.flying) {
returnValue = 'Laying egg...'
}
return returnValue;
};
return oviparousType;
}
function withMetaBehavior(label, behavior) {
this[label] = behavior;
}
class Duck {
constructor() {
// - glue code wrapped by constructor.
// - type will feature a class signature.
// - `state` gets preserved by the closure that is created with each instantiation.
// local state (shared and protected)
const state = {
flying: false
};
const duck = this;
withFlightStateAlteringFlightCapability.call(duck, state);
withFlightStateDependedEggLayingBehavior.call(duck, state);
withMetaBehavior.call(duck, 'quack', () => 'Quaaack...Quaaack...');
}
}
const duck = new Duck;
function createDuckAlikeType() {
// - glue code wrapped by factory function.
// - type will be an augmented but ordinary `Object` type.
// - `state` gets preserved by the closure that is created with each invocation of the factory.
// local state (shared and protected)
const state = {
flying: false
};
const type = {};
withFlightStateAlteringFlightCapability.call(type, state);
withFlightStateDependedEggLayingBehavior.call(type, state);
withMetaBehavior.call(type, 'quack', () => 'Quack!');
return type;
}
const duckAlikeType = createDuckAlikeType();
console.log('composed "real duck" : ', duck);
console.log('composed "duck alike type" : ', duckAlikeType);
console.log('\nduck.fly() ...');
duck.fly();
console.log('\nduck.isFlying() ? ', duck.isFlying());
console.log('duckAlikeType.isFlying() ? ', duckAlikeType.isFlying());
console.log('\nduck.layEgg() ? ', duck.layEgg());
console.log('duckAlikeType.layEgg() ? ', duckAlikeType.layEgg());
console.log('\nduck.land().layEgg() ? ', duck.land().layEgg());
console.log('duckAlikeType.fly().layEgg() ? ', duckAlikeType.fly().layEgg());
console.log('\nduck.isFlying() ? ', duck.isFlying());
console.log('duckAlikeType.isFlying() ? ', duckAlikeType.isFlying());
console.log('\nduck.quack() ? ', duck.quack());
console.log('duckAlikeType.quack() ? ', duckAlikeType.quack());
.as-console-wrapper { max-height: 100%!important; top: 0; }