функции как аппликативные функторы (Haskell / LYAH)

Глава 11 книги Learn You a Haskell вводит следующее определение:

instance Applicative ((->) r) where
    pure x = (\_ -> x)
    f <*> g = \x -> f x (g x)

Здесь автор нехарактерно машет рукой («Реализация экземпляра для‹ *> немного загадочна, поэтому лучше всего, если мы просто [покажем это в действии, не объясняя это] »). Я надеюсь, что кто-нибудь здесь поможет мне разобраться.

Согласно определению аппликативного класса (<*>) :: f (a -> b) -> f a -> f b

В данном случае замена ((->)r) на f: r->(a->b)->(r->a)->(r->b)

Итак, первый вопрос: как мне перейти от этого типа к f <*> g = \x -> f x (g x)?

Но даже если я принимаю эту последнюю формулу как должное, мне трудно согласовать ее с примерами, которые я даю GHCi. Например:

Prelude Control.Applicative> (pure (+5)) <*> (*3) $ 4
17

Вместо этого это выражение соответствует f <*> g = \x -> f (g x) (обратите внимание, что в этой версии x не появляется после f.

Я понимаю, что это грязно, так что спасибо за терпение.


person planarian    schedule 04.08.2012    source источник
comment
В этом примере помните, что pure (+5) отбрасывает свой первый аргумент, поэтому это const (+5) 4 $ (4 * 3) или 4 * 3 + 5, что согласуется с (+5) . (*3) $ 4. Кроме того, f <*> g = \x -> f (g x) относится к типу (b -> c) -> (a -> b) -> (a -> c), который не проверяется ни с помощью pure (+ 5) <*> (* 3) $ 4, ни с объявлением класса Applicative.   -  person schuelermine    schedule 24.06.2018


Ответы (4)


Прежде всего, запомните, как fmap определяется для аппликативов:

fmap f x = pure f <*> x

Это означает, что ваш пример такой же, как (fmap (+ 5) (* 3)) 4. Функция fmap для функций - это просто композиция, поэтому ваше точное выражение такое же, как ((+ 5) . (* 3)) 4.

Теперь давайте подумаем, почему экземпляр написан именно так. По сути, <*> применяет функцию в функторе к значению в функторе. Специализируясь на (->) r, это означает, что он применяет функцию, возвращаемую функцией из r, к значению, возвращаемому функцией из r. Функция, возвращающая функцию, - это просто функция двух аргументов. Итак, настоящий вопрос заключается в следующем: как бы вы применили функцию двух аргументов (r и a, возвращающую b) к значению a, возвращаемому функцией из r?

Первое, что следует отметить, это то, что вы должны вернуть значение типа (->) r, что означает, что результатом также должна быть функция из r. Для справки вот функция <*>:

f <*> g = \x -> f x (g x)

Поскольку мы хотим вернуть функцию, принимающую значение типа r, x :: r. Возвращаемая функция должна иметь тип r -> b. Как мы можем получить значение типа b? Итак, у нас есть функция f :: r -> a -> b. Поскольку r будет аргументом функции результата, мы получаем его бесплатно. Итак, теперь у нас есть функция из a -> b. Итак, пока у нас есть какое-то значение типа a, мы можем получить значение типа b. Но как получить значение типа a? Что ж, у нас есть еще одна функция g :: r -> a. Итак, мы можем взять наше значение типа r (параметр x) и использовать его для получения значения типа a.

Итак, окончательная идея проста: мы используем параметр, чтобы сначала получить значение типа a, вставив его в g. Параметр имеет тип r, g имеет тип r -> a, поэтому у нас есть a. Затем мы вставляем параметр и новое значение в f. Нам нужны оба, потому что f имеет тип r -> a -> b. Как только мы подключим и r, и a, у нас будет b1. Поскольку параметр находится в лямбда-выражении, результат имеет тип r -> b, что нам и нужно.

person Tikhon Jelvis    schedule 04.08.2012
comment
Ух ты, воспоминание о классе абстрактной алгебры, который чистил мои часы десять лет назад! Я действительно ценю терпеливую детализацию этого ответа. Вы одаренный учитель, и я надеюсь, что вы, по крайней мере, рассмотрите возможность учиться в аспирантуре. - person planarian; 05.08.2012
comment
Интересный факт: определение <*> для ((->) r) можно получить бесплатно. Попробуйте вставить тип ((r -> a -> b) -> (r -> a) -> (r -> b)) в Djinn! - person Rein Henrichs; 14.04.2014
comment
Я нашел приведенное выше объяснение действительно полезным, но немного трудным для понимания. Если кто-то хочет поработать с другими, вот объяснение того, как я применил вышеизложенное к реальному примеру, чтобы я мог лучше его понять: wjdhamilton.blogspot.co.uk/2016/08/ffxgx.html - person James Hamilton; 05.08.2016

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

(+) <$> (+3) <*> (*100) $ 5

Это то же самое, что:

pure (+) <*> (+3) <*> (*100) $ 5

Ключевым моментом здесь является pure перед (+), который имеет эффект бокса (+) как аппликативного. Если вы посмотрите, как определяется pure, вы увидите, что для его распаковки вам нужно предоставить дополнительный аргумент, который может быть любым. Применяя <*> к (+) <$> (+3), получаем

\x -> (pure (+)) x ((+3) x)

Обратите внимание, что в (pure (+)) x мы применяем x к pure для распаковки (+). Итак, теперь у нас есть

\x -> (+) ((+3) x)

Добавляя (*100), чтобы получить (+) <$> (+3) <*> (*100) и снова применять <*>, мы получаем

\y -> (\x -> (+) ((+3) x)) y ((*100) y) {Since f <*> g = f x (g x)}

5  -> (\x -> (+) ((+3) x)) 5 ((*100) 5)

(\x -> (+) ((+3) x)) 5 (500)

5 -> (+) ((+3) 5) (500)

(+) 8 500

508

Итак, в заключение, x после f НЕ является первым аргументом нашего бинарного оператора, он используется для UNBOX оператора внутри pure.

person Yi Li    schedule 14.06.2015
comment
Это абсолютно критический момент! Это объясняет как роль x, так и то, почему количество параметров, которые принимает первая функция, должно равняться количеству применяемых функций. - person James Hamilton; 05.08.2016
comment
@JamesHamilton Это ответ, который попадает в самую точку. Это должен быть ответ. - person Olumide; 14.05.2018
comment
Вы правы, но, учитывая, что прошло несколько лет с тех пор, как я задал вопрос, я не уверен, что смогу изменить свой предпочтительный ответ - person James Hamilton; 14.05.2018
comment
В описании LYAH это звучит так, будто (+3) <*> (*100) - это правильно сформированное выражение Haskell, и путаница с ассоциативностью между $ и <$> только усугубляет ситуацию. Написание без инфикса упрощает понимание: (<*>) ((+) <$> (+3)) (*100) или (<*>) ((<*>) (pure (+)) (+3)) (*100) $ 5. - person Marcel Besixdouze; 29.07.2020

В данном случае замена ((->)r) на f: r->(a->b)->(r->a)->(r->b)

Да ведь это неправильно. На самом деле это (r->(a->b)) -> (r->a) -> (r->b), и это то же самое, что (r->a->b) -> (r->a) -> r -> b. То есть мы сопоставляем инфикс и функцию, которая возвращает правый аргумент инфикса, с функцией, которая принимает только LHS инфикса и возвращает ее результат. Например,

Prelude Control.Applicative> (:) <*> (\x -> [x]) $ 2
[2,2]
person leftaroundabout    schedule 04.08.2012

Как понять Function as Functor и Function as Applicative?

Во-первых, как понимать функцию как функтор?

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

instance Functor Maybe where 
    fmap :: (a -> b) -> f a -> f b
    fmap f (Just x) = Just (f x) 
    fmap f Nothing = Nothing

там Maybe тип можно увидеть как пустое поле с одним слотом, которое принимает тип для создания конкретного типа Maybe a. В функции fmap:

  • Первый параметр - это функция, которая отображает от a до b;
  • Второй параметр - это значение типа с заполненным слотом (конкретный тип), этот конкретный тип создается конструктором типа и имеет тип fa (f равно Maybe, поэтому fa равно Maybe a).

Когда мы реализуем функциональные функторы, для функциональных функторов должны быть два параметра, чтобы создать тип a -> b, если мы хотим, чтобы наш функциональный функтор имел ровно один слот, мы должны сначала заполнить слот, поэтому конструктор типа функционального функтора будет ((->) р):

instance Functor ((->) r) where 
    fmap f g = (\x -> f (g x))

Так же, как функция fmap в Maybe Functor, мы должны рассматривать второй параметр g как значение конкретного типа, который генерируется с помощью f (f < / em> равно (->) r), поэтому fa равно (->) r a, что можно увидеть как r -> a. Наконец, нетрудно понять, что g x в функции fmap нельзя рассматривать как r -> x, это просто приложение-функция, которое можно рассматривать как (r -> a) x, также (x -> a).

Наконец, нетрудно понять, что функция ‹*> в Аппликативной функции (->) r может быть реализована следующим образом:

<*> :: f (a -> b) -> f a -> f b
<*> :: (r -> a -> b) -> (r -> a) -> (r -> b)
<&> :: (a -> b) -> (r -> a) -> (r -> b)
f <*> g = \r -> f r (g r)

для gr отобразит r на a, fra отобразит r, a на b, поэтому всю лямбда-функцию можно увидеть как r -> b, также как f b. Например:

((+) <*> (+3)) 5

результат 5 + (5 + 3) = 13.

Как понять в функциях как аппликативы, (+) <$> (+3) <*> (*100) $ 5 = 508?

Мы знаем, что (+) имеет тип: Num a, a -> a -> a;

Мы также знаем, что (+3) и (*100) имеют тип: Num r, a, r -> a;

(+) <$> (+3) равно pure (+) <*> (+3), где :t pure (+) равно Num _, a, _ -> a -> a -> a

Другими словами, pure (+) просто принимает параметр _ и возвращает оператор +, параметр _ не влияет на окончательное возвращаемое значение. pure (+) также отображает возвращаемое значение функции (+3) на функцию. Теперь для

f <*> g = \r -> f r (g r)

мы можем применить операторы и получить:

pure (+) <*> (+3) = 
    \r -> f r (gr) =
    \r -> + (gr) =
    \r -> + (r + 3) =
    \r x -> x + (r + 3)

он имеет тип r -> x -> a. Затем мы вычисляем pure (+) <*> (+3) <*> (*100), используя определение ‹*>, и получаем:

pure (+) <*> (+3) <*> (*100) = 
    \r -> f r (gr) =
    \r -> (r + 3) + (gr)
    \r -> (r + 3) + (r * 100)

затем применяем эту функцию с параметром 5, получаем:

(5 + 3) + (5 * 100) = 508 

мы можем просто думать об этом аппликативном стиле как о первом вычислении значения после <$> и суммировании их с помощью оператора до <$>. В последнем примере этот оператор является двоичным оператором, равным (+), мы можем заменить его тройным оператором (\x y z -> [x,y,z]), так что выполняется следующее уравнение:

(\x y z -> [x,y,z]) <$> (+3) <*> (*2) <*> (/2) $ 5 = [8.0,10.0,2.5]
person Tri    schedule 27.02.2020