dan-lee
dan-lee

Reputation: 14492

Introducing functional programming in Javascript

I am trying to get my head around functional programming. Let's assume I have those two functions:

/**
 * Input like: [[key1, value1], [key2, value2] ...]
 * Output like: { key1: value1, key2: value2, ... }
 */
function flattenSomeArray(arr) {
  return arr.reduce(function(prev, curr) { prev[curr[0]] = curr[1]; return prev; }, {});
}

/**
 * Input like: 'a=b;c=d;...'
 * Output like: { a: 'b', c: 'd', ... }
 */
function expandSomeString(str) {
  return str.split(';').reduce(function(prev, curr) {
    var split = curr.split('=');
    prev[split[0]] = split[1];
    return prev;
  }, {});
}

I can see that that they have kind of a duplicated code like prev[curr[0]] = curr[1]; return prev; or something.reduce. But in my expandSomeString there needs to be some mixin for the extra split. And also in my flattenSomeArray I can directly use reduce, but in expandSomeString I need to split the string first.

I really have no clue how to proceed, as this functional concept is completely new to me. But I'm sure there's a way for these simple functions.

I think the procedure I am looking for is currying, but I don't know how to apply this concept to my functions.

Upvotes: 1

Views: 80

Answers (2)

Maroshii
Maroshii

Reputation: 4017

In functional programming you add abstraction by creating functions. So the question you should be asking yourself is How can create a function to add abstraction to this part of my code? The less a function knows about what's going on outside of its scope the more reusable it will be.

So:

    var pairToObject = function(obj,pair) {
        obj = obj || {};
        obj[pair[0]] = pair[1];
        return obj;
    };

    var expandStringWith = function(separator) {
        return function(str,fn) {
            return str.split(separator).reduce(fn, {});     
        };
    };

    var reduceArrayFactory = function(fn) {
        return function(arr) {
            return arr.reduce(fn, {});
        };
    };


    var expandStringWithSemicolon = expandStringWith(';');

    /**
     * Input like: [[key1, value1], [key2, value2] ...]
     * Output like: { key1: value1, key2: value2, ... }
     */
    var flattenSomeArray = reduceArrayFactory(pairToObject);

    /**
     * Input like: 'a=b;c=d;...'
     * Output like: { a: 'b', c: 'd', ... }
     */
    var expandSomeString = function (str) {
        return expandStringWithSemicolon(str,function(obj,arr) {
            return pairToObject(obj,arr.split('='));
        });
    };

Upvotes: 1

Victor
Victor

Reputation: 9269

I don't really know what your goal is, but it sounds like fun anyway, let me try and refactor everything I can in that code. Fun!

I agree with you, there's some repetition in those functions, let's create another function that does only that:

// Add key/value pair from array to object, return the object
function addKeyFromArray(obj, keyValueArray) { 
    obj[keyValueArray[0]] = keyValueArray[1]; 
    return obj; 
}

Now we can use it directly in your first function, yay!

/**
 * Input like: [[key1, value1], [key2, value2] ...]
 * Output like: { key1: value1, key2: value2, ... }
 */
function flattenSomeArray(arr) {
  return arr.reduce(addKeyFromArray, {});
}

Also in the second one!

/**
 * Input like: 'a=b;c=d;...'
 * Output like: { a: 'b', c: 'd', ... }
 */
function expandSomeString(str) {
  return str.split(';').reduce(function(prev, curr) {
    var split = curr.split('=');
    return addKeyFromArray(prev, split);
  }, {});
}

And now that split variable doesn't do much anymore:

/**
 * Input like: 'a=b;c=d;...'
 * Output like: { a: 'b', c: 'd', ... }
 */
function expandSomeString(str) {
  return str.split(';').reduce(function(prev, curr) {
    return addKeyFromArray(prev, curr.split('='));
  }, {});
}

But that still doesn't feel super nice, let's do first things first. We'll use map to get an array of the same shape we can use with the first function. Step by step:

/**
 * Input like: 'a=b;c=d;...'
 * Output like: { a: 'b', c: 'd', ... }
 */
function expandSomeString(str) {
  var arrKeyValuePairArrays = str.split(';').map(function(strPair){
      return strPair.split('=');
  });
  return arrKeyValuePairArrays.reduce(function(prev, curr) {
    return addKeyFromArray(prev, curr);
  }, {});
}

Now with the flatten function:

/**
 * Input like: 'a=b;c=d;...'
 * Output like: { a: 'b', c: 'd', ... }
 */
function expandSomeString(str) {
  var arrKeyValuePairArrays = str.split(';').map(function(strPair){
      return strPair.split('=');
  });
  return flattenSomeArray(arrKeyValuePairArrays);
}

Untested, but i think that looks nicer. Is that what you were after?

Soooo, currying and composing! Let's use underscore, though, don't feel like reinventing it:

_.partial does the currying
_.compose does... the composing

Let's define a new function to create the array

/**
 * Input like: 'a=b;c=d;...'
 * Output like: [ ['a', 'b'], ['c', 'd'], ... ]
 */
function keyValueArrayFromStr(str) {
  return str.split(';').map(function(strPair){
      return strPair.split('=');
  });
}

Then:

function functionalSplit = function(separator, str){
    return str.split(separator);
};
var splitBySemicolon = _.partial(functionalSplit, ';');
var splitByEqual = _.partial(functionalSplit, '=');
var mapToKeyValueArray = function(strArray){ return strArray.map(splitByEqual)};
var keyValueArrayFromStr = _.compose(mapToKeyValueArray, splitBySemicolon);

Then

/**
 * Input like: 'a=b;c=d;...'
 * Output like: { a: 'b', c: 'd', ... }
 */
var expandSomeString = _.compose(flattenSomeArray, keyValueArrayFromStr);

I'm sure you can also do away with those two ugly functions, mapToKeyValueArray and functionalSplit. Probably some clever arrangement of bind, partial and call.

Upvotes: 1

Related Questions