How to properly replace 'extends', using functional programming?
Asked Answered
F

1

3

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?

Futurity answered 5/4, 2020 at 17:5 Comment(5)
Your mistake is trying to rebuild OO idioms with functional programming means. Use FP means to express functional idioms.Lionfish
@bob This is what I was suspecting. Do you have any advice on how to think in more FP terms in this situation?Futurity
Don't try to avoid the class keyword. Try to avoid mutable object properties (i.e. make everything readonly, given this looks like Typescript). Then, once you are familiar with immutable objects, you will find patterns that might make alternatives to class syntax more appealing.Necolenecro
FP uses algebraic data types to model the domain. It is a completely different approach and rather unfamiliar if you have an FP background.Lionfish
"When we have behavior that we want to re-use between different objects, we usually (in OOP) create a class that extends another class." no, we don't. At least we shouldn't. If you want to re-use code then make it a reusable component. Use composition. Inheritance is not for code reuse, it's for building your class hieararchies - "what is what", not "what does what".Pernell
C
2

preface

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.

approach

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; }
Cordage answered 8/5, 2020 at 21:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.