Because of the fact that in OOP you can encapsulate (hide) a lot of details as private fields in a class, you can hide most of the details. So when you want to change something (refactoring), it is "generally" easier because the scope of the change will be limited, most of the time.
On the other hand in functional programming, if you want to change something (add a field or change a function input/output), you must look for every occurrence of that element in the whole software and update them and sometimes (in case of software frameworks, where users are outside current codebase), this might be impossible and cause a backward-incompatible change.
Starting point
We'll start with a little program that depends on two independent contracts – Pair and List. The implementation of these contracts could virtually be anything as long as the contract is fulfilled
For example, the pair contract gives cons
, head
, and tail
– head(cons(a,b))
must return a
– likewise, tail(cons(a,b))
must return b
.
This technique of creating a set of functions to interact with your data is called data abstraction – if you're interested in the topic in general, I have several other answers here on the site that talk about it – links at the bottom of this post
// -------------------------------------------------
// pair contract
// head(cons(a,b)) == a
// tail(cons(a,b)) == b
const cons = (x,y) =>
[x,y]
const head = pair =>
pair[0]
const tail = pair =>
pair[1]
// -------------------------------------------------
// list contract
// list() == empty()
// list(a,b,c) == cons(a, cons(b, cons(c, empty())))
const empty = () =>
null
const list = (x,...xs) =>
x === undefined ? empty() : cons(x, list(...xs))
// -------------------------------------------------
// demo
const sum = xs =>
xs === empty()
? 0
: head(xs) + sum(tail(xs))
console.log(sum(list(1,2,3))) // 6
First refactor: Pair
Now I'm going to refactor the Pair code by reimplementing cons
, head
and tail
– notice we didn't touch the List code empty
or list
, and the demo code sum
doesn't need to change
// -------------------------------------------------
// pair contract
// head(cons(a,b)) == a
// tail(cons(a,b)) == b
const cons = (x,y) =>
f => f(x,y)
const head = pair =>
pair((x,y) => x)
const tail = pair =>
pair((x,y) => y)
// -------------------------------------------------
// list contract
// list() == empty()
// list(a,b,c) == cons(a, cons(b, cons(c, empty())))
const empty = () =>
null
const list = (x,...xs) =>
x === undefined ? empty() : cons(x, list(...xs))
// -------------------------------------------------
// demo
const sum = xs =>
xs === empty()
? 0
: head(xs) + sum(tail(xs))
console.log(sum(list(1,2,3))) // 6
Second refactor: List
Now I'm going to change the List implementation but still making sure that the contract is fulfilled – notice that I didn't have to change the Pair implementation and the demo code remains unchanged
// -------------------------------------------------
// pair contract
// head(cons(a,b)) == a
// tail(cons(a,b)) == b
const cons = (x,y) =>
f => f(x,y)
const head = pair =>
pair((x,y) => x)
const tail = pair =>
pair((x,y) => y)
// -------------------------------------------------
// list contract
// list() == empty()
// list(a,b,c) == cons(a, cons(b, cons(c, empty())))
const __EMPTY__ = Symbol()
const empty = () =>
__EMPTY__
const list = (...xs) =>
xs.reduceRight((acc,x) => cons(x,acc), empty())
// -------------------------------------------------
// demo
const sum = xs =>
xs === empty()
? 0
: head(xs) + sum(tail(xs))
console.log(sum(list(1,2,3))) // 6
An on and on...
The contracts we implement effectively encapsulate implementation details, just like private data/methods in an OO program. Notice how cons
returns an Array in the first example, but returns a lambda (Function) in the second – this detail doesn't matter because the user of cons
is still guaranteed the correct data if the corresponding head
and tail
accessors are used.
We can continue to change the implementation details as many times as necessary provided the contracts remain fulfilled. We can even introduce new data/code like we did with __EMPTY__
in the last example. The user is still only meant to use list
and empty
to guarantee correct behaviour.
More answers about data abstraction
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