2

I'm just getting into functional programming and i'm having a hard time figuring out how to do this (if it's even worth the trouble). I've looked into currying and am not sure if this is the direction I need to go?? Or pipelines?

I would like to start with a value and then pipe it through different functions. Underscore has the 'chain' method which is similar. However I don't want to use prototypes to do this. I realize the solution might not match my target syntax.

Elm has the |> syntax (below) which is really nice to look at

// what i'd like to do (or similar) in JS *without using prototype*
num = ("(123) 456-7890")
  .removeDashes()
  .removeParens()
  .removeSpaces()

// what elm does
"(123) 456-7890"
  |> removeDashes
  |> removeParens
  |> rem


// functions I wrote so far

removeDashes = function(str) {
  return str.replace(/-/g, '');
};

removeParens = function(str) {
  return str.replace(/\(|\)/g, '');
};

removeSpaces = function(str) {
  return str.replace(/\s/g, '');
};


// what i'm currently doing

num =
  removeDashes(
    removeParens(
      removeSpaces(
        "(123) 456-7890"")));
SkinnyG33k
  • 1,721
  • 4
  • 23
  • 30

8 Answers8

4

If you want to get you're feet wet with functional programming in JavaScript I'd advice you to use a library like Underscore, Lodash or Ramda. Which all have a compose/pipe functionality. Most of the times you'd want to combine it with some form of partial application which those libraries also provide.

Anyway it's a nice exercise to try to implement it yourself. I would solve it like this...

/* Asumes es5 or higher */

function pipe (firstFn /* ...restFns */) {
  var _ = null;
  var _slice = Array.prototype.slice;
  var restFns = _slice.call(arguments, 1) || [];


  return function exec_fns() {
    var args = _slice.call(arguments, 0, 1);

    return restFns.reduce(function(acc, fn) {
      return fn.call(_, acc);
    }, firstFn.apply(_, args));
  }
}

removeDashes = function(str) {
  return str.replace(/-/g, '');
};

removeParens = function(str) {
  return str.replace(/\(|\)/g, '');
};

removeSpaces = function(str) {
  return str.replace(/\s/g, '');
};


console.log(pipe(
  removeDashes,
  removeParens,
  removeSpaces
)("(123) 456-7890") == "1234567890")

Also Functional JavaScript by Fogus is a nice resource to dig deeper into this style of programming

bbz
  • 123
  • 1
  • 9
3

There are different ways to tackle this problem, and you've offered references in underscore and Elm.

In Elm, curried functions are an important part of the equation. As every function receives a single argument, you can build chains with some of them partially applied, waiting for the argument you're weaving in with the pipeline. The same applies to Haskell, PureScript and languages of their ilk.

Reproducing that ipsis literis in JavaScript requires a little bit of sugar — you can use a sweet.js macro to get a source transformation that does it.

Without sugar, it can go many ways. Maybe one way to explore is using generators, passing the bits of the resolved chain down until you get a non-function value.

dodecaphonic
  • 449
  • 3
  • 9
  • those macros are pretty slick! I ended up going with underscore's mixin and chain in my answer below. Close enough and I don't have to use `this` in the fns. – SkinnyG33k Sep 10 '15 at 01:57
2

Like hindmost said, look into using prototypes. The string prototype allows you to add class-level functionality to all strings:

String.prototype.removeParens = function() {
    this = this.replace(/\(|\)/g, '');
}

This lets you do things like this:

var myString = "(test)";

myString.removeParens();

And once you add the other functions to the String prototype you can simply chain the function calls like this:

myString.removeDashes().removeParens().removeSpaces();

etc.

Omar Himada
  • 2,540
  • 1
  • 14
  • 31
  • 1
    Assuming you are going to promote adding methods to the String prototype, you should at least do so with `Object.defineProperty(String.prototype, 'removeParens', ...)` so they don't end up being enumerable. –  Sep 09 '15 at 17:27
  • 1
    This works but I was looking for more of piping rather than chaining (my mistake) – SkinnyG33k Sep 09 '15 at 17:32
2

You can create the pipe function in one line, with good readability:

const pipe = (...fns) => fns.reduce((v, f) => v.constructor === Function ? v() : f(v));

and it would be used in this way:

var numResult = pipe('(123) 456-7890', removeDashes, removeParens, removeSpaces);

var pipe = (...fns) => fns.reduce((v, f) => v.constructor === Function ? v() : f(v));


function removeDashes(str) {
  return str.replace(/-/g, '');
}

function removeParens(str) {
  return str.replace(/\(|\)/g, '');
}

function removeSpaces(str) {
  return str.replace(/\s/g, '');
}

console.log(
 'result:', pipe('(123) 456-7890', removeDashes, removeParens, removeSpaces)
);

Attention: this function needs a platform with support for the spread operator ....

Just in case, i've created a module for this with support for async functions (Promises) and it also works on older/legacy platforms that can't use the spread ...

https://github.com/DiegoZoracKy/pipe-functions

Diego ZoracKy
  • 2,227
  • 15
  • 14
1

The easiest way is to really just add those to the prototype chain, but you can do that with an object. Here's an easy example:

function MyString( str ){
    var value = str.toString();

    return {
        removeDashes: function() {
            value = value.replace(/-/g, '');
            return this;
        },
        removeParens: function() {
            value = value.replace(/\(|\)/g, '');
            return this;
        },
        removeSpaces: function() {
            value = value.replace(/\s/g, '');
            return this;
        },
        toString: function (){
            return value;
        },
        valueOf: function (){
            return value;
        }
    };
}

You can later on do this:

var clean = (new MyString('This \\(Is)\/ Dirty'))
    .removeDashes()
    .removeParens()
    .removeSpaces();

This way, you will keep your prototype clean. To retrieve a non-object value, just run the toStrong() method, toValue() or do anything with the value (contatenating 1, divide it, anything!).

Ismael Miguel
  • 4,185
  • 1
  • 31
  • 42
  • This is interesting but for me it's less readable than just unpacking the parens. Thanks! – SkinnyG33k Sep 09 '15 at 17:33
  • @SkinnyG33k I was just providing a different alternative. The versions using the prototype will work, but if something changes the prototype it can magle stuff up. Other than that, the prototype is superior. – Ismael Miguel Sep 09 '15 at 18:05
1

Here's a solution I found with lodash, it allows you to mixin your own functions and then use them against chain:

...

removeSpaces = function(str) {
  return str.replace(/\s/g, '');
};

_.mixin({
  removeSpaces: removeSpaces,
  removeParens: removeParens,
  removeDashes: removeDashes
});

num = _.chain("(123) 456-7890")
  .removeSpaces()
  .removeParens()
  .removeDashes()
  .value()
SkinnyG33k
  • 1,721
  • 4
  • 23
  • 30
1

Not a very serious suggestions, but one that will work:

var update = pipe()(removeDashes >> removeParens >> removeSpaces);

update("(123) 456-7890"); //=> "1234567890"

This is based upon this implementation of pipe:

var pipe = function() {
    var queue = [];
    var valueOf = Function.prototype.valueOf;
    Function.prototype.valueOf = function() {
        queue.push(this);
        return 1;
    };
    return function() {
        Function.prototype.valueOf = valueOf;
        return function(x) {
            for (var i = 0, val = x, len = queue.length; i < len; i++) {
                val = queue[i](val);
            }
            return val;
        }
    };
};

You can see more in slide 33 of my talk on functional composition in js.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • I suppose this could be altered to allow `pipe('(123) 456-7890')(removeDashes >> removeParens >> removeSpaces); //=> '1234567890'`, or possibly even to autocurry it to allow either style. But this has always felt more like a trick than like production code. – Scott Sauyet Sep 09 '15 at 19:31
0

As the others have said, adding the functions to the String prototype is a valid and short solution. However, if you don´t want to add them to String prototype or if you want to perform in the future more complex functions, another option is to make a wrapper to handle this:

function SpecialString(str){

    this.str = str;

    this.removeDashes = function() {
      this.str=this.str.replace(/-/g, '');
      return this;
    };

    this.removeParens = function() {
      this.str=this.str.replace(/\(|\)/g, '');
      return this;
    };

    this.removeSpaces = function() {
      this.str=this.str.replace(/\s/g, '');
      return this;
    };

    return this;
}

num = new SpecialString("(123) 456-7890").removeDashes().removeParens().removeSpaces();

console.log(num) // 1234567890
juvian
  • 15,875
  • 2
  • 37
  • 38