14

I am reading the book "Functional Programming in Javascript".

In Chapter 2 there is the following comparison between imperative/functional code for finding the first four words containing only letters in a string:

Imperative

var words = [], count = 0;
text = myString.split(' ');
for (i=0; count<4, i<text.length; i++) {
  if (!text[i].match(/[0-9]/)) {
    words = words.concat(text[i]);
    count++;
  }
}

Functional

var words = [];
var words = myString.split(' ').filter(function(x){
    return (! x.match(/[1-9]+/));
}).slice(0,4);

I reasoned that for any case where the length of text is greater than four, the imperative version will be faster, since it only runs up to finding the first four words that match the criteria, while the functional version first filters the entire array and only then slices apart the first four elements.

My questions is, am I right in assuming this?

Felipe Tavares
  • 311
  • 1
  • 5
  • 4
    This is a pretty fantastic question. My tentative answer is "it depends on the compiler/language." I know Haskell does some insane optimizations because it can make perfect guarantees about a lot of behavior. For Javascript, that's not really the case. – Mike Cluck Feb 11 '16 at 19:36
  • JavaScript does have a pretty good JIT, which makes a bunch of surprisingly strong optimizations. That said, higher-order functions will always have an [overhead in JS](http://jsperf.com/for-vs-array-foreach/25) (eg: adding to the call stack). That is not inherent to functional programming, as most FP languages are compiled and can be optimized, but rather to functional JS (or any interpreted language, for that matter). – Guilherme Feb 11 '16 at 19:45
  • 3
    Look into lazy evaluation. – Bergi Feb 11 '16 at 19:45
  • _"while the functional version first filters the entire array"_ actually you can make it end early by changing the `length` property of the array being filtered (3rd argument passed in the callback), to 0 or an index value that has already been passed – Patrick Evans Feb 11 '16 at 19:45
  • 6
    Whether it might be true or not, notice that efficiency is not the main point of functional programming. There are other, more important features, and oftentimes you are even willing to trade them for execution speed. – Bergi Feb 11 '16 at 19:47
  • 2
    The question (or at least your reasoning) isn't really about function vs imperative programming. It's about your chosen solution for both. You can have a functional approach that also halts after the first 4 items are found. –  Feb 11 '16 at 19:49
  • 1
    Also, the "functional" example, while very readable and clear, is not the best example of performant functional code out there. That operation boils down to one simple `reduce` – Guilherme Feb 11 '16 at 19:50
  • I was under the impression that, generally, functional code is slower (or at best the same speed) than imperative code, because all those functions require inlining, and that isn't always possible. The tradeoff is that functional programming allows you to express some very complex *ideas* with more concise code. I could think of an example, but to post it as an answer would incur the wrath of the functional programmers. – J.J Feb 11 '16 at 20:21

2 Answers2

6

In some cases (like yours) yes, but not always. Lots of functional languages like Haskell or Scala have built in laziness. Which means functions aren't evaluated immediately, but only when needed.

If you're familiar with Java 8, their Streams API is also lazy, which means something like this, will not traverse the whole stream 3 times.

stream.filter(n -> n < 200)
    .filter(n -> n % 2 == 0)
    .filter(n -> n > 15);

It's a very interesting concept and you can check out the documentation for the Scala Stream class here http://www.scala-lang.org/api/2.10.0/index.html#scala.collection.immutable.Stream

Luka Jacobowitz
  • 22,795
  • 5
  • 39
  • 57
  • 3
    I don't think you're interpreting of the concept of "laziness" correctly. What's going on in your Java streams example is that the `filter` operations are **interleaved**—the elements "flow" one element at a time through the pipeline of three filters. That's different from laziness, which would mean that base elements that cannot possibly contribute to the result stream are never visited. For example the `limit(long)` operation in Java 8 streams is one that exhibits laziness—the elements of the base stream past the limit will never be demanded. – Luis Casillas Feb 12 '16 at 00:36
  • You make a very good point! I just wanted to demonstrate how APIs can be optimized to be as performant as imperative constructions and it was the first thing that came to mind. – Luka Jacobowitz Feb 12 '16 at 11:40
4

The comparison of these two code fragments makes perfect sense - as part of a tutorial. Functional programming is demanding and if the author doesn't confront his readers with the most efficient functional implementations, then to keep examples simple.

Why is functional programming demanding? Because it follows mathematical principles (and these don't always human logic) and because novices are accustomed to imperative style regularly. In FP the data flow has priority while the actual algorithms remain in the background. It takes time to get used to this style, but if you've done it once, you'll probably never look back!

How can you implement this example more efficiently in a functional way? There are several possibilities, of which I illustrate two. Note, that both implementations avoid intermediate arrays:

  1. Lazy Evaluation

Javascript is strictly evaluated. However, lazy evaluation can be emulated with thunks (nullary functions). Furthermore, foldR (fold right) is required as iterative function from which filterN is derived:

const foldR = rf => acc => xs => xs.length
 ? rf(xs[0])(() => foldR(rf)(acc)(xs.slice(1)))
 : acc;

const filterN = pred => n => foldR(
  x => acc => pred(x) && --n ? [x].concat(acc()) : n ? acc() : [x]
)([]);

const alpha = x => !x.match(/[0-9]/);
let xs = ["1", "a", "b", "2", "c", "d", "3", "e"];

filterN(alpha)(4)(xs); // ["a", "b", "c", "d"]

This implementation has the disadvantage that filterN isn't pure, because it is stateful (n).

  1. Continuation Passing Style

CPS enables a pure variant of filterN:

const foldL = rf => acc => xs => xs.length
 ? rf(acc)(xs[0])(acc_ => foldL(rf)(acc_)(xs.slice(1)))
 : acc;

const filterN = pred => n => foldL(
  acc => x => cont => pred(x)
   ? acc.length + 1 < n ? cont(acc.concat(x)) : acc.concat(x)
   : cont(acc)
)([]);

const alpha = x => !x.match(/[0-9]/);
let xs = ["1", "a", "b", "2", "c", "d", "3", "e"];

filterN(alpha)(4)(xs); // ["a", "b", "c", "d"]

It is a bit confusing how foldR and foldL differ. The difference is not in the commutativity but in the associativity. The CPS implementation has still a drawback. filterN should be separated into filter and takeN, to increase code reusability.

  1. Transducers

Transducers allow to compose (reducing/transforming) functions, without having to rely on intermediate arrays. Consequently, we can separate filterN into two different functions filter and takeN and thus increase their reusability. Unfortunately I haven't found a concise implementation of transducers that would be suitable for a comprehensible and executable example. I'll try to develop my own, simplified transducer solution and then give an appropriate example here.

Conclusion

As you can see, these implementations may not be as efficient as the imperative solution. Bergi has already pointed out that execution speed is not the most relevant concern of functional programming. If micro optimizations are important for you, you should continue to rely on imperative style.

  • Here is an exemplary implementation of [transducers](http://stackoverflow.com/questions/35506544/how-to-avoid-intermediate-results-when-working-with-arrays/35506608#35506608) –  Feb 19 '16 at 14:19