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.
Functional programming is better if you have a fixed set of things and you need to add operations to them. Adding functions that perform calculations on existing data types is one example of this. OOP works well when you have a fixed set of operations on things, and you need to add more things.
If you're coding in JavaScript, getting familiar with OOP principles can make your life easier for a few reasons: It's easier to debug your code when you use objects and classes. You're able to use techniques like encapsulation and inheritance.
Each programming paradigm is made of specific features, however your favorite language does not have to provide all of the features to fall into one paradigm. In fact, OOP can live without inheritance or encapsulation, thus we can say that JavaScript (JS) is an OOP language with inheritance and without encapsulation.
As mentioned, functional programming relies on functions, whereas object-oriented programming is based on classes and respective objects. A function is a process that retrieves a data input, processes it, and then returns an output. Therefore, functions are modules of code written to achieve a certain task.
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 ]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With