Ответ 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
).
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
, чтобы увеличить повторное использование кода.
Преобразователи позволяют создавать (уменьшать/преобразовывать) функции, не полагаясь на промежуточные массивы. Следовательно, мы можем разделить filterN
на две различные функции filter
и takeN
и, следовательно, увеличить их повторное использование. К сожалению, я не нашел краткой реализации преобразователей, которые были бы подходящими для приемлемого и исполняемого примера. Я попытаюсь разработать собственное, упрощенное решение для преобразователей, а затем привести здесь соответствующий пример.
Заключение
Как вы можете видеть, эти реализации могут быть не такими эффективными, как императивное решение. Берги уже указал, что скорость выполнения не является наиболее актуальной проблемой функционального программирования. Если для вас важны микрооптимизации, вы должны продолжать полагаться на императивный стиль.