简体   繁体   中英

How to convert OOP to FP in JavaScript

I'm having this code in JavaScript

class StringFormatter {

 init(locale) {
  this.locale = locale;
 }

 getA() {
  return 'A' + this.locale
 }

 getB() {
  return 'B' + this.locale
 }

 getC() {
  return 'C' + this.locale
 }

}


// when page just render
let stringManager = new StringFormatter();
stringManager.init('enUS');
// call it in other components 
let result = stringManager.getA();

so when I'm using this OOP class, I only need to set the locale once by init it when page first render. And then all of the later code where I call methods getA, getB or getC will adapt to this locale .

Now as I want to move this to a more Functional Programming way, how can I write it but still keep the same concept. Right now I can only come up with this solution

 getA(locale) {
  return 'A' + locale
 }

 getB(locale) {
  return 'B' + locale
 }

 getC(locale) {
  return 'C' + locale
 }

let result = getA('enUS');

But this one required me to keep passing the locale every time I call the function getA, getB or getC.

Is there anyway that we somehow still can config the locale but only once , don't need to pass it every time to getA, getB or getC function, and don't need to write in OOP way ?

Use a closure:

let getA;
let getB;
let getC;

function initStringFormatter (x) {
    let locale = x;

    getA = function () { return 'A' + locale }
    getB = function () { return 'B' + locale }
    getC = function () { return 'C' + locale }
}

initStringFormatter('enUS');

getA();

In theory, closures can provide exactly the same features as object properties/variables/members in OOP. There's an exact one-to-one relation between closures and objects. The only difference is that closures use scope as the mechanism to attach variables to functions whereas objects use bindings to attach variables to methods.

Of course, there's nothing wrong mixing OOP with FP. Even Lisp has an OOP library. So a common javascript idiom is to return an object instead of having the function names as global variables:

function initStringFormatter (locale) { // another trick is to just use
                                        // the argument directly instead of
                                        // creating another variable.

    function a () { return 'A' + locale }
    function b () { return 'B' + locale }
    function c () { return 'C' + locale }

    return {
        getA: a,
        getB: b,
        getC: c
    }
}

let stringManager = initStringFormatter('enUS');

stringManager.getA();

Indeed. It is common to see FP programmers in javascript use objects this way: simply as namespaces for functions. State management can be done 100% using closures instead of object properties.

Side note: FP is powerful enough that languages like Lisp don't need OOP to be built-in to the language, instead OOP is a design pattern and/or a library - the standard OOP library for Lisp is CLOS: the Common Lisp Object System

The only downside of using closures is that all closures are essentially private (technically they're not private, they're just local variables to the functions - private/public are binding concepts global/local are scope concepts). But if you've done any significant amount of OOP you would recognize this as good practice in the OOP world to not ever give public access to variables. When using closures 100% of access to the variable need to be done via functions - again, in the OOP world this would be the common getter/setter design pattern which is considered good practice.

Note however this is not 100% pure FP. But that's OK. There are other FP languages that are not pure. Javascript still allows enclosed variables to be modified. However the above function is pure FP since the locale variable cannot be modified.

To make javascript 100% pure FP is simple. Simply ban all uses of let and var in your code and always use const . It feels odd at first but you can write any program using only const and function arguments (which are constants by default). Languages like javascript and Go gives you an escape to use variables to manage state based logic in a straightforward manner.

You might want a thunk?

A thunk delays a calculation until its result is needed, providing lazy evaluation of arguments. ( ramda's thunk defination )

 const thunkGetFromLocale = (locale) => () => (toGet) => toGet + locale let thunk = thunkGetFromLocale('en-US')() console.log(thunk('A')) console.log(thunk('B')) console.log(thunk('C')) thunk = thunkGetFromLocale('vi-VN')() console.log(thunk('A')) console.log(thunk('B')) console.log(thunk('C'))

You can easily create state outside of the function:

let locale = 'en-US';

function getA() {
 return 'A' + locale
}

But even though you are using functions now, I'm not sure if it can be called 'functional'.

If you modify state outside of the function, you pretty much have similar behavior as a class again, except now it's outside the class.

If you embrace functional programming, ideally the result of a function should not depend on side-effects.

So another way to handle this, is using a higher-order function:

function getAForLocale(locale) {
  return () => {
    return 'A' + locale;
  }
}

Now we can call:

const getA = getAForLocale('en-US');

And then we can re-use getA multiple times.

I think this is the most functional approach, but I can't say that this is better than using classes.

Expanding on @slebetman's answer, we can use modern Javascript features to make our closure more concise.

const initStringFormatter = locale => ({
  getA: () => 'A' + locale,
  getB: () => 'B' + locale,
  getC: () => 'C' + locale
});

let stringManager = initStringFormatter('enUS');

stringManager.getA();

You can set a global default that you can pass as a default value for the locale in each function.

Update: If you want to change the defaultLocale ; declare it as let , else keep it const .

 let defaultLocale = 'enUS'; const getA = (locale = defaultLocale) => 'A' + locale; const getB = (locale = defaultLocale) => 'B' + locale; const getC = (locale = defaultLocale) => 'C' + locale; let result = getA(); console.log(result); // Output: AenUS defaultLocale = 'fr'; // Change the default locale console.log(getB()); // Output: Bfr


Update: If you want to go the thunk-route as suggested by hgb123, you can pass the function into the thunk.

 const getA = (locale) => 'A' + locale; const getB = (locale) => 'B' + locale; const getC = (locale) => 'C' + locale; const thunkFormatLocale = locale => () => func => func(locale); let thunk = thunkFormatLocale('enUS')(); console.log(thunk(getA)); console.log(thunk(getB)); console.log(thunk(getC)); thunk = thunkFormatLocale('fr')(); console.log(thunk(getA)); console.log(thunk(getB)); console.log(thunk(getC));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM