Почему я могу передать функции в поднятый 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!