Почему я могу передать функции в поднятый R.divide?

Учитывая следующее:

var average = R.lift(R.divide)(R.sum, R.length)

Почему это работает как бессмысленная реализация average? Я не понимаю, почему я могу пройти R.sum а также R.length когда они являются функциями и, следовательно, я не могу отобразить R.divide над функциями R.sum а также R.length в отличие от следующего примера:

var sum3 = R.curry(function(a, b, c) {return a + b + c;});
R.lift(sum3)(xs)(ys)(zs)

В приведенном выше случае значения в xs, ys а также zs суммируются в недетерминированном контексте, и в этом случае поднятая функция применяется к значениям в данном вычислительном контексте.

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

R.ap(R.ap(R.ap([tern], [1, 2, 3]), [2, 4, 6]), [3, 6, 8])
R.lift(tern)([1, 2, 3], [2, 4, 6], [3, 6, 8])

Проверяя документацию, он говорит:

"поднимает" функцию arity > 1, чтобы она могла "отобразить" список, функцию или другой объект, который удовлетворяет спецификации FantasyLand Apply.

И это не кажется очень полезным описанием, по крайней мере, для меня. Я пытаюсь построить интуицию относительно использования lift, Я надеюсь, что кто-то может это предоставить.

2 ответа

Решение

Первая крутая вещь в том, что a -> b может поддержать map, Да, функции - это функторы!

Давайте рассмотрим тип map:

map :: Functor f => (b -> c) -> f b -> f c

Давай заменим Functor f => f с Array дать нам конкретный тип:

map :: (b -> c) -> Array b -> Array c

Давай заменим Functor f => f с Maybe этот раз:

map :: (b -> c) -> Maybe b -> Maybe c

Корреляция ясна. Давай заменим Functor f => f с Either a, чтобы проверить двоичный тип:

map :: (b -> c) -> Either a b -> Either a c

Мы часто представляем тип функции из a в b как a -> b но это действительно просто сахар для Function a b, Давайте использовать длинную форму и заменить Either в подписи выше с Function:

map :: (b -> c) -> Function a b -> Function a c

Таким образом, отображение функции дает нам функцию, которая будет применять b -> c функция в исходное значение, возвращаемое функцией. Мы могли бы переписать подпись, используя a -> b сахар:

map :: (b -> c) -> (a -> b) -> (a -> c)

Заметьте что-нибудь? Какой тип compose?

compose :: (b -> c) -> (a -> b) -> a -> c

Так compose просто map специализируется на типе функции!

Вторая крутая вещь в том, что a -> b может поддержать ap, Функции также являются аппликативными функторами! Они известны как Apply в спецификации Fantasy Land.

Давайте рассмотрим тип ap:

ap :: Apply f => f (b -> c) -> f b -> f c

Давай заменим Apply f => f с Array:

ap :: Array (b -> c) -> Array b -> Array c

Теперь с Either a:

ap :: Either a (b -> c) -> Either a b -> Either a c

Теперь с Function a:

ap :: Function a (b -> c) -> Function a b -> Function a c

Что такое Function a (b -> c)? Это немного сбивает с толку, потому что мы смешиваем два стиля, но это функция, которая принимает значение типа a и возвращает функцию из b в c, Давайте перепишем, используя a -> b стиль:

ap :: (a -> b -> c) -> (a -> b) -> (a -> c)

Любой тип, который поддерживает map а также ap можно "поднять". Давайте посмотрим на lift2:

lift2 :: Apply f => (b -> c -> d) -> f b -> f c -> f d

Помни что Function a удовлетворяет требованиям Apply, поэтому мы можем заменить Apply f => f с Function a:

lift2 :: (b -> c -> d) -> Function a b -> Function a c -> Function a d

Который более четко написано:

lift2 :: (b -> c -> d) -> (a -> b) -> (a -> c) -> (a -> d)

Давайте вернемся к вашему первоначальному выражению:

//    average :: Number -> Number
const average = lift2(divide, sum, length);

Что значит average([6, 7, 8]) делать? a ([6, 7, 8]) дается a -> b функция (sum), производя b (21). a также дается a -> c функция (length), производя c (3). Теперь, когда у нас есть b и c мы можем накормить их b -> c -> d функция (divide) произвести d (7), что является окончательным результатом.

Итак, потому что тип функции может поддерживать map а также ap, мы получаем converge бесплатно (через lift, lift2, а также lift3). Я бы на самом деле хотел удалить converge от Рамды как то не надо.


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

Поскольку мне трудно понять ту же проблему, я решил взглянуть на исходный код Рамды. Напишу в блоге об этом в будущем. Между тем - я прокомментировал, как Рамда lift работать шаг за шагом.

от: https://gist.github.com/philipyoungg/a0ab1efff1a9a4e486802a8fb0145d9e

// Let's make an example function that takes an object and return itself.
// 1. Ramda's lift level
lift(zipObj)(keys, values)({a: 1}) // returns {a: 1}

// this is how lift works in the background
module.exports = _curry2(function liftN(arity, fn) {
  var lifted = curryN(arity, fn);
  return curryN(arity, function() {
    return _reduce(ap, map(lifted, arguments[0]), Array.prototype.slice.call(arguments, 1)); // found it. let's convert no 1 to no 2
  });
});

// 2. Ramda's reduce level
reduce(ap, map(zipObj, keys))([values])
// first argument is the function, second argument is initial value, and the last one is lists of arguments. If you don't understand how reduce works, there's a plenty of resources on the internet

// 3. Ramda's ap level
ap(map(zipObj, keys), values)

// how ap works in the background
module.exports = _curry2(function ap(applicative, fn) {
  return (
    typeof applicative.ap === 'function' ?
      applicative.ap(fn) :
    typeof applicative === 'function' ? // 
      function(x) { return applicative(x)(fn(x)); } : // because the first argument is a function, ap return this.
    // else
      _reduce(function(acc, f) { return _concat(acc, map(f, fn)); }, [], applicative)
  );
});

// 4. Voilà. Here's the final result.
map(zipObj, keys)({a: 1})(values({a: 1}))

// Hope it helps you and everyone else!
Другие вопросы по тегам