Haskell - Оценка (+) <$> (+3) <*> (* 100) $5

Из главы о функторах, в которой вы узнаете о Haskell для большого блага, Липовача заявляет:

"Когда мы делаем (+) <$> (+3) <*> (*100), мы создаем функцию, которая будет использовать + для результатов (+3) и (*100) и вернет это. Чтобы продемонстрировать на реальном примере, когда мы сделали (+) <$> (+3) <*> (*100) $ 5, сначала 5 был применен к (+3) и (*100), в результате получились 8 и 500. Затем + вызывается с 8 и 500, в результате чего 508."

Однако, если я попытаюсь оценить функцию самостоятельно, учитывая это определение для Аппликативного на функторе ((- > ) r):

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

Я прочитал оценку вышеуказанного выражения как:

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

Но я не вижу, как мы можем составлять две частично применяемые двоичные функции как единую лямбду (на самом деле GHCi генерирует бесконечную ошибку типа, пытающуюся привязать ее к переменной). Кроме того, к рабочей интерпретации, если мы посмотрим на определение типа для <$>, получим:

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

или, более конкретно, мы можем посмотреть на его подъем как:

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

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

(\x -> a + b) где a= (+ 3) и b= (* 100). Это функция, которая должна быть возвращена. Однако правильно ли я предполагаю, что это окончательная (грубая) форма?

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

..., что дает 508.

Я нахожу описание Lipovača более понятным с точки зрения того, как работает выражение, но мой кишок говорит мне, что это не совсем верно для подробных деталей под капотом Haskell. Мне легче думать, что fmap из (+) произошел первым, в результате получилась функция с двумя функторами, которые частично применяют функции, которые принимают общий ввод, а затем мы применили к нему значение. Мы можем сделать это из-за ленивой оценки. Это неправильно?

Ответ 1

Во-первых, обратите внимание, что оба <$> и <*> связаны слева. Ничего волшебного не происходит внутри, и мы можем видеть трансформацию, по существу, серию расширений eta и бета-сокращений. Постепенно это выглядит так:

(((+) <$> (+3))         <*> (*100)) $ 5        -- Add parens
((fmap (+) (+3))        <*> (*100)) $ 5        -- Prefix fmap
(((+) . (+3))           <*> (*100)) $ 5        -- fmap = (.)
((\a -> (+) ((+3) a))   <*> (*100)) $ 5        -- Definition of (.)
((\a -> (+) (a+3))      <*> (*100)) $ 5        -- Infix +
((\a b -> (+) (a+3) b)) <*> (*100)) $ 5        -- Eta expand
(\x -> (\a b -> (+) (a+3) b) x ((*100) x)) $ 5 -- Definition of (<*>)
(\x -> (\a b -> (+) (a+3) b) x (x*100)) $ 5    -- Infix *
(\a b -> (+) (a + 3) b) 5 (5*100)              -- Beta reduce
(\a b -> (a + 3) + b)   5 (5*100)              -- Infix +
(5 + 3) + (5*100)                              -- Beta reduce (twice)
508                                            -- Definitions of + and *

Немного смутно, тот факт, что $ ассоциируется справа, имеет меньшее отношение к тому, что происходит здесь, чем тот факт, что его фиксированность равна 0. Мы можем видеть это, если мы определяем новый оператор:

(#) :: (a -> b) -> a -> b
f # a = f a
infixl 0 #

и в GHCi:

λ> (+) <$> (+3) <*> (*100) # 5
508