Является ли функциональное программирование менее эффективным для этого случая?

Я читаю книгу "Функциональное программирование в Javascript".

В главе 2 приведено сравнение между императивным/функциональным кодом для поиска первых четырех слов, содержащих только буквы в строке:

Императив

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++;
  }
}

Функциональные

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

Я рассуждал о том, что для любого случая, когда длина text больше четырех, императивная версия будет быстрее, поскольку она подходит только для поиска первых четырех слов, соответствующих критериям, в то время как функциональная версия сначала фильтрует весь массив и только затем срезает первые четыре элемента.

Мои вопросы: правильно ли я принимаю это?

Ответы

Ответ 1

Сравнение этих двух фрагментов кода имеет смысл - как часть учебника. Функциональное программирование требует, и если автор не сталкивается с его читателями с наиболее эффективными функциональными реализациями, то для упрощения примеров.

Почему требуется функциональное программирование? Потому что он следует математическим принципам (и это не всегда человеческая логика) и потому, что новички регулярно привыкают к императивному стилю. В FP поток данных имеет приоритет, а фактические алгоритмы остаются в фоновом режиме. Требуется время, чтобы привыкнуть к этому стилю, но если вы сделали это один раз, вы, вероятно, никогда не оглянетесь!

Как вы можете реализовать этот пример более эффективно функциональным способом? Есть несколько возможностей, из которых я иллюстрирую два. Обратите внимание, что обе реализации избегают промежуточных массивов:

Javascript строго оценивается. Однако ленивую оценку можно эмулировать с помощью thunks (нулевые функции). Кроме того, в качестве итеративной функции, из которой получается filterN:

требуется foldR (свернуть вправо)
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"]

Эта реализация имеет тот недостаток, что filterN не является чистым, поскольку он является stateful (n).

  1. Продолжение стиля перехода

CPS допускает чистый вариант 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"]

Немного смущает, как отличаются foldR и foldL. Разница заключается не в коммутативности, а в ассоциативности. Реализация CPS все еще остается недостатком. filterN следует разделить на filter и takeN, чтобы увеличить повторное использование кода.

  1. Transducers

Преобразователи позволяют создавать (уменьшать/преобразовывать) функции, не полагаясь на промежуточные массивы. Следовательно, мы можем разделить filterN на две различные функции filter и takeN и, следовательно, увеличить их повторное использование. К сожалению, я не нашел краткой реализации преобразователей, которые были бы подходящими для приемлемого и исполняемого примера. Я попытаюсь разработать собственное, упрощенное решение для преобразователей, а затем привести здесь соответствующий пример.

Заключение

Как вы можете видеть, эти реализации могут быть не такими эффективными, как императивное решение. Берги уже указал, что скорость выполнения не является наиболее актуальной проблемой функционального программирования. Если для вас важны микрооптимизации, вы должны продолжать полагаться на императивный стиль.

Ответ 2

В некоторых случаях (например, ваш) да, но не всегда. Многие функциональные языки, такие как Haskell или Scala, построили в лень. Это означает, что функции не оцениваются сразу, а только при необходимости.

Если вы знакомы с Java 8, их API Streams также ленив, что означает что-то вроде этого, 3 раза не будет проходить весь поток.

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

Это очень интересная концепция, и вы можете проверить документацию для класса Scala Stream здесь http://www.scala-lang.org/api/2.10.0/index.html#scala.collection.immutable.Stream