5

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.

2
  • 2
    That depends on the functional programming language used: access control has nothing to do with functional programming itself. And for instance using Haskells export mechanism, you can hide things. Commented Jun 17, 2017 at 9:45
  • 2
    They would be equally difficult to refactor if both implementations are equally DRY. A breaking change in a method will affect all child classes and all the uses of that method. In a non encapsulating functional version you need to change all the places where its been used which woudl correspond to all method use of derived classes in OO. Noter that FP and OO are not mutually exclusive. Commented Jun 17, 2017 at 12:07

1 Answer 1

4

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 tailhead(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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.