Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is refactoring more difficult in functional programming than in OOP?

Tags:

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.

like image 461
Mahdi Avatar asked Jun 17 '17 09:06

Mahdi


1 Answers

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

  • Map array structure
  • Why do functional languages make such heavy use of lists
  • Take a list and return new list with count of positive, negative, and zeros
  • and more...
like image 162
Mulan Avatar answered Oct 11 '22 14:10

Mulan