Как я могу передавать функции приподнятому 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])

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

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

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


person Chad    schedule 17.09.2016    source источник


Ответы (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 специализировано для типа Function!

Вторая интересная вещь заключается в том, что 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), что является конечным результатом.

Итак, поскольку тип Function может поддерживать map и ap, мы получаем converge бесплатно (через lift, lift2 и lift3). На самом деле я бы хотел удалить converge из Ramda, так как в этом нет необходимости.


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

person davidchambers    schedule 17.09.2016
comment
Очень хорошо! Я бы хотел, чтобы этот ответ был поднят в блоге! - person Scott Sauyet; 17.09.2016
comment
Когда (b -> c -> d) -> (a -> b) -> (a -> c) -> (a -> d) реализован как комбинатор, он выглядит как bird = f => g => h => x => f(g(x)) (h(x)). Похоже на psi = f => g => x => y => f(g(x)) (g(y)). Интересно, как зовут эту птицу? - person ; 17.09.2016
comment
Чтобы ответить на мой собственный вопрос: это starling', который является композицией аппликативных ap функций const ap = f => g => x => f(x) (g(x)) и композицией const comp = f => g => x => f(g(x)): const starling_ = comp(comp(ap)) (comp). - person ; 18.09.2016

Поскольку мне трудно понять ту же проблему, я решил заглянуть в исходный код Ramda. В будущем напишу об этом в блоге. Между тем — я сделал комментарий, как шаг за шагом работает Ramda 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!
person Philip Young    schedule 31.12.2016