Beginner JavaScript OOP vs Functional

23,809

Solution 1

There's no correct definition for what is and isn't "functional", but generally functional languages have an emphasis on simplicity where data and functions are concerned.

Most functional programming languages don't have concepts of classes and methods belonging to objects. Functions operate on well-defined data structures, rather than belonging to the data structures.

The first style _.map is a function in the _ namespace. It's a standalone function and you could return it or pass it to another function as an argument.

function compose(f, g) {
  return function(data) {
    return f(g(data));
  }
}

const flatMap = compose(_.flatten, _.map);

It's not possible to do the same for the second style, because the method instance is intrinsically tied to the data used to construct the object. So I'd say that the first form is more functional.

In either case, the general functional programming style is that data should be the final argument to the function, making it easier to curry or partially apply the earlier arguments. Lodash/fp and ramda address this by having the following signature for map.

_.map(func, data);

If the function is curried, you can create specific versions of the function by only passing the first argument.

const double = x => x * 2;
const mapDouble = _.map(double);

mapDouble([1, 2, 3]);
// => [2, 4, 6]

Solution 2

Functional: You pass an object to the function and do stuff

_.map([1, 2, 3], function(n){ return n * 2; });

OOP: You call function on the object and do stuff

_([1, 2, 3]).map(function(n){ return n * 2; });

In both examples [1,2,3] (array) is an object.

Example OOP reference: http://underscorejs.org/#times

Solution 3

FP

In FP, a function takes inputs and produces output with the guarantee that the same inputs will yield the same outputs. In order to do this, a function must always have parameters for the values it operates on and cannot rely on state. Ie, if a function relies on state, and that state changes, the output of the function could be different. FP avoids this at all costs.

We'll show a minimum implementation of map in FP and OOP. In this FP example below, notice how map operates only on local variables and does not rely on state -

const _ = {
                 // 👇🏽has two parameters
  map: function (arr, fn) {
      // 👇🏽local
    if (arr.length === 0)
      return []
    else
            // 👇🏽local               
                // 👇🏽local           // 👇🏽local    // 👇🏽local
      return [ fn(arr[0]) ].concat(_.map(arr.slice(1), fn))
  }
}

const result =
  // 👇🏽call _.map with two arguments
  _.map([1, 2, 3], function(n){ return n * 2; })


console.log(result)
// [ 2, 4, 6 ]

In this style, it doesn't matter that map was stored in the _ object - that doesn't make it "OOP" because an object was used. We could have just as easily written -

function map (arr, fn) {
  if (arr.length === 0)
    return []
  else
    return [ fn(arr[0]) ].concat(map(arr.slice(1), fn))
}

const result =
  map([1, 2, 3], function(n){ return n * 2; })

console.log(result)
// [ 2, 4, 6 ]

This is the basic recipe for a call in FP -

// 👇🏽function to call
             // 👇🏽argument(s)
someFunction(arg1, arg2)

The notable thing for FP here is that map has two (2) parameters, arr and fn, and the output of map depends solely on these inputs. You'll see how this changes dramatically in the OOP example below.


OOP

In OOP, objects are used to store state. When an object's method is called, the context of the method (function) is dynamically bound to the receiving object as this. Because this is a changing value, OOP cannot guarantee any method will have the same output, even if the same input is given.

NB how map only takes only one (1) argument below, fn. How can we map using just a fn? What will we map? How do I specify the target to map? FP considers this a nightmare because the output of the function no longer depends solely on its inputs - Now the output of map is harder to determine because it depends on the dynamic value of this -

            // 👇🏽constructor
function _ (value) {
         // 👇🏽returns new object
  return new OOP(value)
}

function OOP (arr) {
  // 👇🏽dynamic
  this.arr = arr
}
                           // 👇🏽only one parameter
OOP.prototype.map = function (fn) {
     // 👇🏽dynamic
  if (this.arr.length === 0)
    return []
  else         // 👇🏽dynamic           // 👇🏽dynamic
    return [ fn(this.arr[0]) ].concat(_(this.arr.slice(1)).map(fn))
}

const result =
  // 👇🏽create object
             // 👇🏽call method on created object
                    // 👇🏽with one argument
  _([1, 2, 3]).map(function(n){ return n * 2; })


console.log(result)
// [ 2, 4, 6 ]

This is the basic recipe for a dynamic call in OOP -

// 👇🏽state
       // 👇🏽bind state to `this` in someAction
                  // 👇🏽argument(s) to action
someObj.someAction(someArg)

FP revisited

In the first FP example, we see .concat and .slice - aren't these OOP dynamic calls? They are, but these ones in particular do not modify the input array, and so they are safe for use with FP.

That said, the mixture of calling styles can be a bit of an eyesore. OOP favours "infix" notation where the methods (functions) are displayed between the function's arguments -

// 👇🏽arg1
     // 👇🏽function
                       // 👇🏽arg2
user .isAuthenticated (password)

This is how JavaScript's operators work, too -

// 👇🏽arg1
   // 👇🏽function
      // 👇🏽arg2
   1  +  2

FP favours "prefix" notation where the function always comes before its arguments. In an ideal world, we would be able to call OOP methods and operators in any position, but unfortunately JS does not work this way -

// 👇🏽invalid use of method
.isAuthenticated(user, password)

// 👇🏽invalid use of operator
+(1,2)

By converting methods like .conat and .slice to functions, we can write FP programs in a more natural way. Notice how consistent use of prefix notation makes it easier to imagine the how the computation carries out -

function map (arr, fn) {
  if (isEmpty(arr))
    return []
  else
    return concat(
      [ fn(first(arr)) ]
      , map(rest(arr), fn)
    )
}

map([1, 2, 3], function(n){ return n * 2; })
// => [ 2, 4, 6 ]

The methods are converted as follows -

function concat (a, b) {
  return a.concat(b)
}

function first (arr) {
  return arr[0]
}

function rest (arr) {
  return arr.slice(1)
}

function isEmpty (arr) {
  return arr.length === 0
}

This begins to show other strengths of FP where functions are kept small and focus on one task. And because these functions only operate on their inputs, we can easily reuse them in other areas of our program.

Your question was originally asked in 2016. Since then, modern JS features allow you to write FP in more elegant ways -

const None = Symbol()

function map ([ value = None, ...more ], fn) {
  if (value === None)
    return []
  else
    return [ fn(value), ...map(more, fn) ]
}

const result =
  map([1, 2, 3], function(n){ return n * 2; })

console.log(result)
// [ 2, 4, 6 ]

A further refinement using expressions instead of statements -

const None = Symbol()

const map = ([ value = None, ...more ], fn) =>
  value === None
    ? []
    : [ fn(value), ...map(more, fn) ]
    
const result =
  map([1, 2, 3], n => n * 2)

console.log(result)
// [ 2, 4, 6 ]

Statements rely on side effects whereas expressions evaluate directly to a value. Expressions leave less potential "holes" in your code where statements can do anything at anytime, such as throwing an error or exiting a function without returning a value.


FP with objects

FP does not mean "don't use objects" - it's about preserving the ability to easily reason about your programs. We can write the same map program that gives the illusion that we're using OOP, but in reality it behaves more like FP. It looks like a method call, but the implementation relies only on local variables and not on dynamic state (this).

JavaScript is a rich, expressive, multi-paradigm language that allows you to write programs to suit your needs and preferences -

function _ (arr) {
  function map (fn) {
      // 👇🏽local
    if (arr.length === 0)
      return []
    else
            // 👇🏽local
                // 👇🏽local         // 👇🏽local      // 👇🏽local
      return [ fn(arr[0]) ].concat(_(arr.slice(1)).map(fn))
  }
         // 👇🏽an object!
  return { map: map }
}

const result =
            // 👇🏽OOP? not quite!
  _([1, 2, 3]).map(function(n){ return n * 2; })

console.log(result)
// [ 2, 4, 6 ]
Share:
23,809
inthenameofmusik
Author by

inthenameofmusik

Updated on July 09, 2021

Comments

  • inthenameofmusik
    inthenameofmusik almost 3 years

    I'm just starting to research different programming styles (OOP, functional, procedural).

    I'm learning JavaScript and starting into underscore.js and came along this small section in the docs. The docs say that underscore.js can be used in a Object-Oriented or Functional style and that both of these result in the same thing.

    _.map([1, 2, 3], function(n){ return n * 2; });
    _([1, 2, 3]).map(function(n){ return n * 2; });
    

    I don't understand which one is functional and which one is OOP, and I don't understand why, even after some research into these programming paradigms.

  • Revenant
    Revenant almost 4 years
    Your anwser is BRILLIANT!!
  • Mulan
    Mulan almost 4 years
    I noticed there was a character encoding error in the post. I fixed it and the code snippets are easier to read now. Thanks for you comment, Revenant :D
  • Deepak Garg
    Deepak Garg almost 4 years
    how did you write these hands symbols in comments of code?
  • Mulan
    Mulan almost 4 years
    @ErDeepakGarg I used an emoji keyboard. or just copy/paste if you don't have access to one of those