Подтвердить что ты не робот

Хорошие имена для переворачиваемых версий `lt`,` lte`, `gt` и` gte`?

Я некоторое время работал над библиотекой Javascript FP под названием Ramda, а я "м, имея небольшую проблему с именованием вещей. (Вы слышали старую строку, не так ли?" В "Компьютерной науке" есть только две трудные проблемы: недействительность кеша, именование вещей и ошибки "один за другим".)

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

var div10 = divide(10);

звучит так, как будто это должна быть функция, которая делит свой параметр на 10. Но на самом деле он делит свой параметр на 10, что довольно ясно, если вы посмотрите на определение:

var divide = curry(function(a, b) {
    return a / b;
});

Итак, вместо этого ожидаемый:

div10(50); //=> 5 // NO!!

Фактически вы получаете

div10(50); //=> 0.2 // Correct, but surprising!

Мы справляемся с этим, документируя разницу между возможными ожиданиями людей и создавая divideBy, который является просто flip(divide) и subtractN, который равен flip(subtract). Но мы не нашли хорошего эквивалента для таких функций, как lt:

R.lt = curry(function(a, b) { 
    return a < b;
});

или его кузенов lte, gt и gte.

Моей собственной интуицией было бы то, что

map(lt(5), [8, 6, 7, 5, 3, 0, 9]); 
//=> [false, false, false, false, true, true, false]

Но, конечно, он фактически возвращает

//=> [true, true, true, false, false, false, true]

Итак, я хотел бы сделать ту же самую процедуру с документом и пунктом-альтернативой для lt и ее ilk. Но я не смог найти доброе имя. Единственный реальный кандидат был ltVal, и на самом деле это не работает при вызове с обоими аргументами. Мы сделали обсудить эту проблему, но не имели хороших результатов.

Попробовали ли другие с этим и придумали хорошие решения? Или даже если нет, какие-либо хорошие предложения для имени для перевернутых версий этих функций?


Обновление

Кто-то предположил, что это будет закрыто, потому что "непонятно, что вы спрашивали", и я думаю, что вопрос действительно был немного потерян в объяснении. Простой вопрос:

Что было бы хорошим, интуитивным именем для перевернутой версии lt?

4b9b3361

Ответ 1

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

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

Функции в Haskell могут быть преобразованы в операторы с использованием обратных ссылок. Например, div 6 3 может быть записано как 6 `div` 3. Аналогичным образом операторы могут быть преобразованы в функции с помощью круглых скобок. Например, 2 < 3 может быть записано как (<) 2 3.

Операторы также могут быть частично применены с использованием разделов. Существует два типа разделов: левые секции (например, (2 <) и (6 `div`)) и правые секции (например, (< 3) и (`div` 3)). Левые разделы переводятся следующим образом: (2 <) становится (<) 2. Правые секции: (< 3) становится flip (<) 3.


В JavaScript у нас есть только функции. Нет никакого "хорошего" способа создания операторов в JavaScript. Вы можете написать код типа (2).lt(3), но, по моему скромному мнению, он нечеткий, и я бы настоятельно советовал не писать такой код.

Таким образом, тривиально мы можем записать нормальные функции и операторы как функции:

div(6, 3) // normal function: div 6 3
lt(2, 3) // operator as a function: (<) 2 3

Написание и внедрение инфиксных операторов в JavaScript - это боль. Следовательно, у нас не будет следующего:

(6).div(3) // function as an operator: 6 `div` 3
(2).lt(3) // normal operator: 2 < 3

Однако разделы важны. Начните с правой части:

div(3) // right section: (`div` 3)
lt(3) // right section: (< 3)

Когда я увижу div(3), я ожидаю, что это будет правый раздел (т.е. он должен вести себя как (`div` 3)). Следовательно, согласно принципу наименьшего удивления, так оно и должно быть реализовано.

Теперь возникает вопрос о левых разделах. Если div(3) - это правая секция, то как выглядит левая часть? По моему скромному мнению, это должно выглядеть так:

div(6, _) // left section: (6 `div`)
lt(2, _) // left section: (2 <)

Мне это читается как "делить 6 на что-то" и "на 2 меньше чем что-то"? Я предпочитаю этот путь, потому что он явный. Согласно Zen of Python, "Явный лучше, чем неявный."


Итак, как это влияет на существующий код? Например, рассмотрим функцию filter. Чтобы отфильтровать нечетные числа в списке, мы будем писать filter(odd, list). Для такой функции карри работает так, как ожидалось? Например, как мы будем писать функцию filterOdd?

var filterOdd = filter(odd);    // expected solution
var filterOdd = filter(odd, _); // left section, astonished?

В соответствии с принципом наименьшего удивления он должен быть просто filter(odd). Функция filter не предназначена для использования в качестве оператора. Следовательно, программисту не следует принуждать его использовать как левую часть. Должно быть четкое различие между функциями и "функциональными операторами".

К счастью, различение функций и операторов функций довольно интуитивно. Например, функция filter явно не является оператором функции:

filter odd list -- filter the odd numbers from the list; makes sense
odd `filter` list -- odd filter of list? huh?

С другой стороны, функция elem, очевидно, является функциональным оператором:

list `elem` n -- element n of the list; makes sense
elem list n -- element list, n? huh?

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


Интересно отметить, что при двоичной функции, если вы flip ее аргументы, она становится двоичным оператором и наоборот. Например, рассмотрите перевернутые варианты filter и elem:

list `filter` odd -- now filter makes sense an an operator
elem n list -- now elem makes sense as a function

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

  • Если основной аргумент функции находится в конце списка аргументов, функция является нормальной функцией (например, filter odd list, где list является основным аргументом). Для составления функции необходим первичный аргумент в конце списка.
  • Если основной аргумент функции находится в начале списка аргументов, то функция является функциональным оператором (например, list `elem` n, где list является основным аргументом).
  • Операторы аналогичны методам в ООП, а первичный аргумент аналогичен объекту метода. Например, list `elem` n будет записываться как list.elem(n) в ООП. Цепочные методы в ООП аналогичны целям функционального состава в FP [1].
  • Основной аргумент функции может быть либо в начале, либо в конце списка аргументов. Не было бы смысла, чтобы это было где-то еще. Это свойство нечетно верно для двоичных функций. Следовательно, переворачивание двоичных функций делает их операторами и наоборот.
  • Остальные аргументы вместе с функцией образуют неделимый атом, называемый основой списка аргументов. Например, в filter odd list стержень filter odd. В list `elem` n стержень (`elem` n).
  • Порядок и элементы стебля должны оставаться неизменными, чтобы выражение имело смысл. Вот почему odd `filter` list и elem list n не имеют никакого смысла. Однако list `filter` odd и elem n list имеют смысл, потому что стержень не изменяется.

Возвращаясь к основной теме, поскольку функции и операторы функций являются взаимоисключающими, вы можете просто рассматривать функциональные операторы иначе, чем то, как вы относитесь к нормальным функциям.

Мы хотим, чтобы операторы имели следующее поведение:

div(6, 3) // normal operator: 6 `div` 3
div(6, _) // left section: (6 `div`)
div(3)    // right section: (`div` 3)

Мы хотим определить операторы следующим образом:

var div = op(function (a, b) {
    return a / b;
});

Определение функции op прост:

function op(f) {
    var length = f.length, _; // we want underscore to be undefined
    if (length < 2) throw new Error("Expected binary function.");
    var left = R.curry(f), right = R.curry(R.flip(f));

    return function (a, b) {
        switch (arguments.length) {
        case 0: throw new Error("No arguments.");
        case 1: return right(a);
        case 2: if (b === _) return left(a);
        default: return left.apply(null, arguments);
        }
    };
}

Функция op аналогична использованию обратных циклов для преобразования функции в оператор в Haskell. Следовательно, вы можете добавить его как стандартную библиотечную функцию для Ramda. Также упоминайте в документах, что основным аргументом оператора должен быть первый аргумент (т.е. Он должен выглядеть как ООП, а не FP).


[1] На стороне примечания было бы замечательно, если бы Ramda разрешил вам создавать функции, как если бы они были целыми методами в обычном JavaScript (например, foo(a, b).bar(c) вместо compose(bar(c), foo(a, b))). Это сложно, но выполнимо.

Ответ 2

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

  • он вводит функциональные операторы с левыми/правыми секциями в Javascript, которые не являются частью языка
  • для этого требуется отвратительный заполнитель или undefined взломать

Javascript не имеет операторов в карри и, следовательно, не имеет левых или правых разделов. Идиоматическое решение Javascript должно учитывать это.

Причиной проблемы

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

const add = y => x => x + y;

const add2 = add(2); // partial application

add(2)(3); // complete application

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

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

const map = f => xs => xs.map(x => f(x));

const inc = x => x + 1;

const sqr = x => x * x;


comp(map(inc)) (map(sqr)) ([1,2,3]); // [2,5,10]

Функции оператора являются особыми в этом отношении. Это двоичные функции, которые сводят два аргумента к одному возвращаемому значению. Поскольку не каждый оператор является коммутативным (a - b! == b - a), имеет значение порядок аргументов. По этой причине функции оператора не имеют в основном аргумента. Но люди привыкли читать выражения с ними определенным образом в зависимости от типа приложения:

const concat = y => xs => xs.concat(y);

const sub = y => x => x - y;


// partial application:

const concat4 = concat(4);

const sub4 = sub(4);

concat4([1,2,3]); // [1,2,3,4] - OK

sub4(3); // -1 - OK


// complete application:

concat([1,2,3]) (4); // [4,1,2,3] - ouch!

sub(4) (3); // -1 - ouch!

Мы определили concat и sub с переворачиваемыми аргументами, так что частичное приложение работает так, как ожидалось. Это, очевидно, не распространяется на полное приложение.

Ручное решение

const flip = f => y => x => f(x) (y);

const concat_ = flip(concat);

const sub_ = flip(sub);


concat_(xs) (4); // [1,2,3,4] - OK

sub_(4) (3); // 1 - OK

concat_ и sub_ соответствуют левым разделам в Haskell. Обратите внимание, что функциональные операторы, такие как add или lt, не нуждаются в левой версии, потому что первые являются коммутативными, а последние являются предикатами, которые имеют логические копии:

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

const map = f => xs => xs.map(x => f(x));

const flip = f => y => x => f(x) (y);

const not = x => !x;

const notf2 = comp2(not);


const lt = y => x => x < y;

const gt = flip(lt);

const lte = notf2(gt);

const gte = notf2(lt);


map(lt(5)) ([8, 6, 7, 5, 3, 0, 9]);
// [false, false, false, false, true, true, false]

map(gte(5)) ([8, 6, 7, 5, 3, 0, 9]);
// [true, true, true, true, false, false, true]

Заключение

Мы должны решить эту проблему с именами, а не с помощью соглашения об именах, тогда с программным решением, которое расширяет Javascript не-идиоматическим способом. Соглашения об именах не идеальны... ну, как Javascript.