Как согласовать Javascript с каррированием и композицией функций

Я люблю каррирование, но есть несколько причин, по которым многие разработчики Javascript отвергают эту технику:

  1. эстетические опасения по поводу типичного рисунка карри: f(x) (y) (z)
  2. опасения по поводу снижения производительности из-за увеличения количества вызовов функций
  3. опасения по поводу проблем с отладкой из-за множества вложенных анонимных функций
  4. опасения по поводу удобочитаемости бесточечного стиля (каррирование в связи с композицией)

Есть ли способ смягчить эти опасения, чтобы мои коллеги не ненавидели меня?


comment
Вы забыли про тестирование. Тестировать простые функции легко, тестировать сложные функции сложно. Тестировать конвейер простых функций по-прежнему легко, тестировать комбинаторный взрыв взаимодействий монолитных функций по-прежнему сложно.   -  person Jared Smith    schedule 10.02.2017
comment
Действительно классный вопрос. Мы часто думаем, что наш код находится в пузыре, но на самом деле он часто живет в дикой природе, где с ним взаимодействует множество других людей. Хорошо осознавать это и думать об улучшениях, которые мы можем сделать, чтобы помочь всем в долгосрочной перспективе.   -  person Mulan    schedule 10.02.2017


Ответы (2)


Примечание. @ftor ответил на свой вопрос. Это прямое дополнение к этому ответу.

Вы уже гений

Я думаю, вы могли переосмыслить функцию partial — по крайней мере, частично!

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

и его аналог, partialRight

const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);

partial принимает функцию, некоторые аргументы (xs) и всегда возвращает функцию, которая принимает еще несколько аргументов (ys), затем применяет f к (...xs, ...ys).


Первоначальные замечания

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

  • то, что функция может возвращать функцию, не означает, что она каррирована — добавление _ для обозначения того, что функция ожидает дополнительных аргументов, сбивает с толку. Напомним, что каррирование (или частичное применение функции) абстрагирует арность, поэтому мы никогда не знаем, когда вызов функции приведет к значению вычисления или другой функции, ожидающей вызова.

  • curry следует не переворачивать аргументы; это вызовет серьезные WTF-моменты для вашего коллеги-кодировщика

  • если мы собираемся создать оболочку для reduce, обертка reduceRight должна быть последовательной — например, ваш foldl использует f(acc, x, i), а ваш foldr использует f(x, acc, i) — это вызовет много проблем у коллег, которые не знакомы с этими вариантами.

В следующем разделе я заменю composable на partial, удалю суффиксы _ и исправлю оболочку foldr.


Составные функции

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);

const comp = (f, g) => x => f(g(x));

const foldl = (f, acc, xs) => xs.reduce(f, acc);

const drop = (xs, n) => xs.slice(n);

const add = (x, y) => x + y;

const sum = partial(foldl, add, 0);

const dropAndSum = comp(sum, partialRight(drop, 1));

console.log(
  dropAndSum([1,2,3,4]) // 9
);


Программное решение

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

// restore consistent interface
const foldr = (f, acc, xs) => xs.reduceRight(f, acc);

const comp = (f,g) => x => f(g(x));

// added this for later
const flip = f => (x,y) => f(y,x);

const I = x => x;

const inc = x => x + 1;

const compn = partial(foldr, flip(comp), I);

const inc3 = compn([inc, inc, inc]);

console.log(
  inc3(0) // 3
);


Более серьезная задача

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

const filter = (f, xs) => xs.filter(f);

const comp2 = (f, g, x, y) => f(g(x, y));

const len = xs => xs.length;

const odd = x => x % 2 === 1;

const countWhere = f => partial(comp2, len, filter, f);

const countWhereOdd = countWhere(odd);

console.log(
   countWhereOdd([1,2,3,4,5]) // 3
);


Частичное питание!

partial можно применять столько раз, сколько необходимо

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)

const p = (a,b,c,d,e,f) => a + b + c + d + e + f

let f = partial(p,1,2)
let g = partial(f,3,4)
let h = partial(g,5,6)

console.log(p(1,2,3,4,5,6)) // 21
console.log(f(3,4,5,6))     // 21
console.log(g(5,6))         // 21
console.log(h())            // 21

Это делает его незаменимым инструментом и для работы с вариативными функциями.

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)

const add = (x,y) => x + y

const p = (...xs) => xs.reduce(add, 0)

let f = partial(p,1,1,1,1)
let g = partial(f,2,2,2,2)
let h = partial(g,3,3,3,3)

console.log(h(4,4,4,4))
// 1 + 1 + 1 + 1 +
// 2 + 2 + 2 + 2 +
// 3 + 3 + 3 + 3 +
// 4 + 4 + 4 + 4 => 40

Наконец, демонстрация partialRight

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);

const p = (...xs) => console.log(...xs)

const f = partialRight(p, 7, 8, 9);
const g = partial(f, 1, 2, 3);
const h = partial(g, 4, 5, 6);

p(1, 2, 3, 4, 5, 6, 7, 8, 9) // 1 2 3 4 5 6 7 8 9
f(1, 2, 3, 4, 5, 6)          // 1 2 3 4 5 6 7 8 9
g(4, 5, 6)                   // 1 2 3 4 5 6 7 8 9
h()                          // 1 2 3 4 5 6 7 8 9


Сводка

Хорошо, так что partial в значительной степени является заменой composable, но также решает некоторые дополнительные угловые случаи. Давайте посмотрим, как это отразится на вашем первоначальном списке.

  1. эстетические проблемы: избегает f (x) (y) (z)
  2. производительность: не уверен, но я подозреваю, что производительность примерно такая же
  3. отладка: все еще проблема, потому что partial создает новые функции
  4. читабельность: я думаю, что читаемость здесь довольно хорошая, на самом деле. partial достаточно гибок, чтобы удалять точки во многих случаях

Я согласен с вами, что нет замены полностью каррированным функциям. Лично мне было легко принять новый стиль, как только я перестал осуждать «уродство» синтаксиса — он просто другой, и людям не нравится другой.

person Mulan    schedule 10.02.2017
comment
Спасибо за ответ, он вдохновляет. Мне нужно больше думать о частичном применении. Я полностью игнорировал это до сих пор. AFAIK acc в reduceRight следует добавлять справа. По-видимому, Javascript reduceRight добавляет его. Это странно и дает странные результаты для некоммутативных операций. Если бы curry не переворачивал аргументы, функция, подобная drop, должна была бы быть определена как (n, xs) => xs.slice(n), что было довольно необычно. - person ; 10.02.2017
comment
Если вы создаете обертки, я полагаю, вам решать, как они себя ведут. Что касается порядка аргументов drop, я вижу веские аргументы для обоих случаев; однако, если порядок нельзя изменить с drop = (xs,n) => ..., здесь может быть полезен partialRightconst partialRight = (f,...xs) => (...ys) => f(...ys, ...xs) – затем partialRight(drop, 1) ([1,2,3]) // => [2,3] - person Mulan; 11.02.2017
comment
Я обновил код, оставив ваш drop нетронутым, и вместо этого продемонстрировал использование partialRight. Позже в ответе я привел еще один пример partialRight. Рад снова видеть тебя на SO, кстати ^_^ - person Mulan; 11.02.2017

Текущий преобладающий подход предусматривает, что каждая функция с несколькими аргументами заключена в динамическую функцию карри. Хотя это помогает с проблемой № 1, остальные остаются нетронутыми. Вот альтернативный подход.

Составные функции

Составная функция каррируется только в своем последнем аргументе. Чтобы отличить их от обычных функций с несколькими аргументами, я называю их символом подчеркивания в конце (называть имена сложно).

const comp_ = (f, g) => x => f(g(x)); // composable function

const foldl_ = (f, acc) => xs => xs.reduce((acc, x, i) => f(acc, x, i), acc);

const curry = f => y => x => f(x, y); // fully curried function

const drop = (xs, n) => xs.slice(n); // normal, multi argument function

const add = (x, y) => x + y;

const sum = foldl_(add, 0);

const dropAndSum = comp_(sum, curry(drop) (1));

console.log(
  dropAndSum([1,2,3,4]) // 9
);

За исключением drop, dropAndSum состоит исключительно из многоаргументных или составных функций, и все же мы достигли той же выразительности, что и с полностью каррированными функциями - по крайней мере, в этом примере.

Вы можете видеть, что каждая компонуемая функция принимает в качестве аргументов либо некаррированные, либо другие компонуемые функции. Это увеличит скорость, особенно для приложений с итеративными функциями. Однако это также является ограничительным, поскольку результат составной функции снова является функцией. Посмотрите пример countWhere ниже для получения дополнительной информации.

Программное решение

Вместо того, чтобы определять составные функции вручную, мы можем легко реализовать программное решение:

// generic functions

const composable = f => (...args) => x => f(...args, x);

const foldr = (f, acc, xs) =>
 xs.reduceRight((acc, x, i) => f(x, acc, i), acc);

const comp_ = (f, g) => x => f(g(x));

const I = x => x;

const inc = x => x + 1;


// derived functions

const foldr_ = composable(foldr);

const compn_ = foldr_(comp_, I);

const inc3 = compn_([inc, inc, inc]);


// and run...

console.log(
  inc3(0) // 3
);

Операторные функции и функции высшего порядка

Возможно, вы заметили, что curry (из первого примера) меняет местами аргументы, а composable — нет. curry предназначен для применения только к операторным функциям, таким как drop или sub, которые имеют разный порядок аргументов в форме с карри и без карри соответственно. Операторная функция — это любая функция, которая принимает только нефункциональные аргументы. В этом смысле...

const I = x => x;
const eq = (x, y) => x === y; // are operator functions

// whereas

const A = (f, x) => f(x);
const U = f => f(f); // are not operator but a higher order functions

Функции высшего порядка (HOF) не нуждаются в перестановке аргументов, но вы будете регулярно сталкиваться с ними с арностью выше двух, поэтому функция composbale полезна.

HOF — один из самых замечательных инструментов в функциональном программировании. Они абстрагируются от применения функции. Именно по этой причине мы используем их все время.

Более серьезная задача

Можем решить и более сложные задачи:

// generic functions

const composable = f => (...args) => x => f(...args, x);

const filter = (f, xs) => xs.filter(f);

const comp2 = (f, g, x, y) => f(g(x, y));

const len = xs => xs.length;

const odd = x => x % 2 === 1;


// compositions

const countWhere_ = f => composable(comp2) (len, filter, f); // (A)

const countWhereOdd = countWhere_(odd);

// and run...

console.log(
   countWhereOdd([1,2,3,4,5]) // 3
);

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

Вывод

Создание компонуемых функций устраняет следующие проблемы:

  1. эстетические проблемы (менее частое использование рисунка карри f(x) (y) (z)
  2. штрафы за производительность (гораздо меньше вызовов функций)

Однако пункт № 4 (читабельность) лишь немного улучшен (менее бесточечный стиль), а пункт № 3 (отладка) — совсем нет.

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

person Community    schedule 10.02.2017
comment
отличный анализ сложной проблемы - спасибо, что поделились своей работой ^_^ - person Mulan; 10.02.2017