Аппликативная оценка функтора мне не понятна - PullRequest
0 голосов
/ 02 марта 2019

Я сейчас читаю «Learn You a Haskell for Great Good»!и спотыкаюсь о объяснении для оценки определенного кодового блока.Я прочитал объяснения несколько раз и начинаю сомневаться, понимает ли даже автор, что делает этот фрагмент кода.

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

Аппликативный функтор применяет функцию в некотором контексте к значению в некотором контекстечтобы получить некоторый результат в некотором контексте.Я потратил несколько часов на изучение этого блока кода и придумал несколько объяснений того, как оценивается это выражение, и ни одно из них не является удовлетворительным.Я понимаю, что (5 + 3) + (5 * 100) составляет 508, но проблема заключается в том, чтобы получить это выражение.У кого-нибудь есть четкое объяснение этого куска кода?

Ответы [ 4 ]

0 голосов
/ 02 марта 2019

Для любого аппликативного,

        a <$> b <*> c  =  liftA2 a b c

Для функций,

    liftA2 a  b     c      x 
=          a (b x) (c x)          -- by definition;
=       (a . b) x  (c x)
=     ((a <$> b) <*> c)    x

Таким образом

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

(длинная версия этогоответ следует.)

У чистой математики нет времени.Чистый Хаскелл не имеет времени.Говорить в глаголах («аппликативный функтор применяется » и т. Д.) Может сбивать с толку (« применяется ... когда? ...»).

Вместо (<*>) - это комбинатор, который объединяет «вычисление» (обозначаемое аппликативным функтором), несущее функцию ( в некотором контексте ), и «вычисление» того же типа, несущее значение ( вкак контекст ), в одно объединенное «вычисление», которое выполняет применение этой функции к этому значению ( в таком контексте ).

«Вычисление» используется для противопоставления этомус чистым Haskell "расчетами".«Вычисления» могут быть или не быть чистыми сами по себе, это ортогональный вопрос.Но в основном это означает, что «вычисления» включают в себя обобщенный протокол вызова функций .Они могут «делать» что-то помимо / как часть / выполнения приложения функции к ее аргументу.Или в типах:

    ( $ ) ::   (a ->   b)  ->    a ->   b
    (<$>) ::   (a ->   b)  ->  f a -> f b
    (<*>) :: f (a ->   b)  ->  f a -> f b
    (=<<) ::   (a -> f b)  ->  f a -> f b

С функциями контекст является приложением ( другой один), а для восстановления значения - будь то функция или аргумент - приложение для общий аргумент .

(потерпите меня, мы почти у цели).Шаблон a <$> b <*> c также можно выразить как liftA2 a b c.Итак, тип «вычисления» аппликативного функтора «functions» определяется как

   liftA2 h x y s  = let x' = x s            -- embellished application of h to x and y
                         y' = y s in         -- in context of functions, or Reader
                      h  x'    y'

-- liftA2 h x y    = let x' = x              -- non-embellished application, or Identity
--                       y' = y   in         
--                    h  x'    y'

-- liftA2 h x y s  = let (x',s')  = x s      -- embellished application of h to x and y
--                       (y',s'') = y s' in  -- in context of
--                   (h  x'    y', s'')      -- state-passing computations, or State

-- liftA2 h x y    = let (x',w)  = x         -- embellished application of h to x and y
--                       (y',w') = y  in     -- in context of
--                   (h  x'    y', w++w')    -- logging computations, or Writer

-- liftA2 h x y    = [h  x'    y' |          -- embellished application of h to x and y
--                             x' <- x,      -- in context of 
--                             y' <- y ]     -- nondeterministic computations, or List

--  ( and for Monads we define `liftBind h x k =` and replace `y` with `k x'`
--      in the bodies of the above combinators; then liftA2 becomes liftBind: )
--    liftA2   ::    (a -> b -> c) -> f a ->       f b  -> f c
--    liftBind ::    (a -> b -> c) -> f a -> (a -> f b) -> f c
--    (>>=) = liftBind (\a b -> b) :: f a -> (a -> f b) -> f b

Действительно,

> :t let liftA2 h x y r = h (x r) (y r) in liftA2
       :: (a -> b -> c)  -> (t -> a)  -> (t -> b)  -> (t -> c)

> :t liftA2   -- the built-in one
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

, то есть типы совпадают, когда мы берем f a ~ (t -> a) ~ (->) t a, то есть f ~ (->) t.

Итак, мы уже там :

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

Это просто, как liftA2 определено для этого типа ,Applicative ((->) t) => ...:

instance Applicative ((->) t) where
    pure     x   t =    x
    liftA2 h x y t = h (x t) (y t)

Нет необходимости определять (<*>). Исходный код говорит :

Минимальное полное определение

pure, ((<*>) | liftA2)

Итак, вы былижелая долго спрашивать, почему это то, что a <$> b <*> c эквивалентно liftA2 a b c?

Краткий ответ, это просто так.Один может быть определен в терминах другого - т.е. (<*>) может быть определен через liftA2,

    g <*> x    = liftA2 id  g     x        -- i.e. (<*>) = liftA2 id = liftA2 ($)

-- (g <*> x) t = liftA2 id  g     x t 
--               =      id (g t) (x t) 
--               =    (id . g) t (x t)     -- = (id <$> g <*> x) t
--               =          g  t (x t)

(что в точности соответствует определению в источнике ),

и каждый законный функтор должен следовать закону, который h <$> g = pure h <*> g.

И наконец,

liftA2 h g x  ==  pure h <*> g <*> x
--     h g x  ==      (h     g)    x

, поскольку <*> ассоциируется слева:это infixl 4 <*>.

0 голосов
/ 02 марта 2019

Давайте сначала посмотрим, как fmap и (<*>) определены для функции:

instance Functor ((->) r) where
    fmap = (.)

instance Applicative ((->) a) where
    pure = const
    (<*>) f g x = f x (g x)
    liftA2 q f g x = q (f x) (g x)

Выражение, которое мы стремимся оценить:

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

или более многословно:

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

Если мы таким образом оценим (<$>),который является инфиксным синонимом для fmap, мы видим, что это равно:

(+) . (+3)

, что означает, что наше выражение эквивалентно:

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

Далее мы можем применить последовательное применение .Здесь f, таким образом, равно (+) . (+3), а g равно (*100).Таким образом, это означает, что мы создаем функцию, которая выглядит следующим образом:

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

Теперь мы можем упростить это и переписать это в:

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

, а затем переписать в:

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

Таким образом, мы создали функцию, которая выглядит следующим образом:

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

или проще:

\x -> 101 * x + 3

Если мы затем вычислим:

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

тогда мы, конечно, получим:

101 * 5 + 3

и, таким образом:

505 + 3

, что ожидается:

508
0 голосов
/ 02 марта 2019

Два других ответа дали подробности того, как это рассчитывается - но я подумал, что мог бы дать более «интуитивный» ответ, чтобы объяснить, как, не пройдя подробный расчет, можно «увидеть», что результатдолжно быть 508.

Как вы и предполагали, каждый Applicative (фактически, даже каждый Functor) может рассматриваться как особый вид "контекста", который содержит значения данного типа.В качестве простых примеров:

  • Maybe a - это контекст, в котором значение типа a может существовать, но может и не существовать (обычно это результат вычисления, которое по какой-то причине может завершиться неудачей)
  • [a] - это контекст, который может содержать ноль или более значений типа a, без верхнего предела числа - представляя все возможные результаты конкретного вычисления
  • IO a - этоконтекст, в котором значение типа a доступно в результате взаимодействия с «внешним миром» каким-либо образом.(Хорошо, что не все так просто ...)

И, что относится к этому примеру:

  • r -> a - это контекст, в котором значение типаa доступно, но его конкретное значение пока неизвестно, поскольку оно зависит от некоторого (пока неизвестного) значения типа r.

Методы Applicative могут быть очень хорошимипонимается на основе ценностей в таких контекстах.pure встраивает «обычное значение» в «контекст по умолчанию», в котором оно ведет себя как можно ближе в этом контексте к «контекстно-свободному».Я не буду описывать это для каждого из 4 приведенных выше примеров (большинство из них очень очевидны), но отмечу, что для функций pure = const, то есть «чистое значение» a представленофункция, которая всегда выдает a независимо от исходного значения.

Вместо того, чтобы останавливаться на том, как лучше всего описать <*>, используя метафору «context», я хочу остановиться на конкретном выражении:

f <$> a <*> b

, где f - это функция между 2 «чистыми значениями» и a и b - «значениями в контексте».На самом деле это выражение имеет синоним функции: liftA2 .Хотя использование функции liftA2 обычно считается менее идиоматичным, чем «аппликативный стиль» с использованием <$> и <*>, название подчеркивает, что идея состоит в том, чтобы «поднять» функцию на «обычных значениях» на одну на «значениях»в контексте ".И когда я думаю об этом, я думаю, что это обычно очень интуитивно понятно, что это делает, учитывая определенный «контекст» (т. Е. Конкретный Applicative экземпляр).

Итак, выражение:

(+) <$> a <*> b

для значений a и b типа скажем f Int для Аппликативного f, ведет себя следующим образом для разных экземпляров f:

  • , если f = Maybe,тогда результатом, если a и b являются значениями Just, является сложение базовых значений и упаковка их в Just.Если a или b равняется Nothing, тогда все выражение равно Nothing.
  • , если f = [] (экземпляр списка), тогда приведенное выше выражение представляет собой список, содержащий все суммыформа a' + b', где a' в a и b' в b.
  • , если f = IO, то приведенное выше выражение является действием ввода-вывода, которое выполняет все эффекты ввода-выводаиз a, за которыми следуют те из b, и в результате получается сумма Int s, произведенная этими двумя действиями.

Так что, в конце концов, это делает, если fтакое экземпляр функции?Поскольку a и b являются обеими функциями, описывающими, как получить заданный Int при произвольном (Int) вводе, естественно, что поднятие над ними функции (+) должно быть функцией, которая при заданномinput, получает результат функций a и b, а затем добавляет результаты.

И это, конечно, то, что он делает - и явный путь, которым он делает, который был очень умело намечен другими ответами.Но причина, по которой это так работает, - действительно, та самая причина, по которой у нас есть случай, f <*> g = \x -> f x (g x), который в противном случае может показаться довольно произвольным (хотя на самом деле это одна из очень немногих, если не единственная вещь, котораябудет проверять тип), так что экземпляр соответствует семантике «значений, которые зависят от какого-то еще неизвестного другого значения в соответствии с заданной функцией».И вообще, я бы сказал, что часто лучше думать «на высоком уровне», как это, чем быть вынужденным перейти к низкоуровневым деталям того, как именно выполняются вычисления.(Хотя я, конечно, не хочу преуменьшать важность того, чтобы иметь возможность делать последнее.)

[На самом деле, с философской точки зрения, было бы точнее сказать, что определениеэто просто потому, что это «естественное» определение, которое проверяет тип, и это просто счастливое совпадение, что экземпляр тогда приобретает такое приятное «значение».Математика, конечно, полна именно таких счастливых «совпадений», которые, как оказывается, имеют очень глубокие причины.]

0 голосов
/ 02 марта 2019

Используется аппликативный экземпляр для функций.Ваш код

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

оценивается как

( <i>(\a->b->a+b)</i> <b><$></b> <i>(\c->c+3)</i> <*> <i>(\d->d*100)</i> ) 5
( (<b>\x -></b> <i>(\a->b->a+b)</i> <b>(</b><i>(\c->c+3)</i> <b>x)</b>) <*> <i>(\d->d*100)</i> ) 5
( (\x -> <i>(\a->b->a+b)</i> (<b>x</b>+3)) <*> <i>(\d->d*100)</i> ) 5
( (\x -> b -> (x+3)<b>+</b>b) <*> <i>(\d->d*100)</i> ) 5
( <i>(\x->b->(x+3)+b)</i> <b><*></b> <i>(\d->d*100)</i> ) 5
(<b>\y -> (</b><i>(\x->b->(x+3)+b)</i> <b>y) (</b><i>(\d->d*100)</i> <b>y)</b>) 5
(\y -> (b->(<b>y</b>+3)+b) (<b>y</b>*100)) 5
(\y -> (y+3)<b>+</b>(y*100)) 5
(<b>5</b>+3)+(<b>5</b>*100)

, где <$> - fmap или просто композиция функций ., а <*> - ap, если вы знаетекак это ведет себя на монадах.

...