В Haskell как «применить» функции во вложенном контексте к значению в контексте?

#function #haskell #apply #applicative

Вопрос:

 nestedApply :: (Applicative f, Applicative g) => g (f (a -> b)) -> f a -> g (f b)
 

Как указывает тип, как это (a->b) применить к этому a в контексте f ?

Спасибо за помощь.

Комментарии:

1. Подсказка: вам нужно только (Applicative f, Functor g) .

2. Сначала вам нужно поднять аппликатор над внешней структурой g , чтобы попасть f (a -> b) внутрь f a -> fb . ПОДСКАЗКА: fmap (<*>) gfab , pure fa ~ g(fa)

3. @Khundragpan Благодарим вас за помощь. Я не уверен, что понял, что ты сказал. Если я перейду (<*>) в gfab, то я получу <*><*> применен к fab, верно? Но что мне делать дальше? Почему мне нужно добавить g к fa?

4. @Chaot1c nestedApply gfab fa = (<*>) <$> gfab <*> pure fa

5. @Khundragpan Большое спасибо за вашу помощь! Теперь я понимаю. Я новичок в Хаскелле, поэтому концепция здесь для меня чрезвычайно запутана.

Ответ №1:

Это один из тех случаев, когда полезно сосредоточиться на типах. Я постараюсь сделать это простым и объясню причины.

Давайте начнем с описания задачи. У нас есть gfab :: g(f(a->b)) и fa :: f a , и мы хотим иметь g(f b) .

 gfab :: g (f (a -> b))
fa   :: f a
??1  :: g (f b)
 

Поскольку g это функтор, для получения типа g T мы можем начать со значения ??2 типа g U и применить fmap к ??3 :: U -> T нему . В нашем случае у нас есть T = f b , поэтому мы ищем:

 gfab :: g (f (a -> b))
fa   :: f a
??2  :: g U
??3  :: U -> f b
??1 = fmap ??3 ??2  :: g (f b)
 

Теперь, похоже, нам следует выбрать ??2 = gfab . В конце концов,это единственное значение типа g Something , которое у нас есть. Мы получаем U = f (a -> b) .

 gfab :: g (f (a -> b))
fa   :: f a
??3  :: f (a -> b) -> f b
??1 = fmap ??3 gfab :: g (f b)
 

Давайте превратим ??3 в лямбду, (x :: f (a->b)) -> ??4 с. ??4 :: f b (Тип x можно опустить, но я решил добавить его, чтобы объяснить, что происходит)

 gfab :: g (f (a -> b))
fa   :: f a
??4  :: f b
??1 = fmap ( (x :: f (a->b)) -> ??4) gfab :: g (f b)
 

Как создавать ??4 . Ну, у нас есть значения типов f (a->b) и f a , поэтому мы можем <*> их получить f b . Мы, наконец, получаем:

 gfab :: g (f (a -> b))
fa   :: f a
??1 = fmap ( (x :: f (a->b)) -> x <*> fa) gfab :: g (f b)
 

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

 nestedApply gfab fa = fmap (<*> fa) gfab
 

Сейчас это не самый элегантный способ сделать это, но понимание процесса важно.

Ответ №2:

С

 nestedApply :: (Applicative f, Applicative g) 
            => g (f (a -> b))  
            ->    f  a 
            -> g (f       b )
 

чтобы (a->b) применить это к этому a в контексте f , нам нужно действовать в контексте g .

И это просто fmap .

Это становится яснее с перевернутой подписью, сосредоточив внимание на ее последней части

 flip nestedApply :: (Applicative f, Applicative g) 
            =>    f  a 
            -> g (f (a -> b))     --- from here
            -> g (f       b )     --- to here
 

Итак, что мы имеем здесь, так это

 nestedApply gffun fx = fmap (bar fx) gffun
 

с bar fx применением под g покровом fmap для нас. Который

 bar fx ::         f (a -> b)
            ->    f       b   
 

т.е.

 bar ::            f  a
            ->    f (a -> b)
            ->    f       b   
 

и это просто <*> не так, снова перевернуто. Таким образом, мы получаем ответ,

 nestedApply gffun fx  =  fmap (<*> fx) gffun
 

Как мы видим, используются только fmap возможности g , поэтому нам нужно только

 nestedApply :: (Applicative f, Functor g) => ...
 

в подписи типа.


Это легко, когда пишешь это на листе бумаги, в 2D. Который мы имитируем здесь с диким отступом, чтобы получить это вертикальное выравнивание.

Да, мы, люди, научились сначала писать на бумаге, а печатать на пишущей машинке гораздо позже. Последнее поколение или два были вынуждены печатать линейным шрифтом с помощью современных устройств с юных лет, но теперь каракули и разговоры (а также жесты и указывания), надеюсь, снова возьмут верх. Изобретательные режимы ввода в конечном итоге будут включать 3D-рабочие процессы, и это будет определенным прогрессом. 1D плохо, 2D хорошо, 3D лучше. Например, многие диаграммы теории категорий гораздо легче проследить (и, по крайней мере, представить), когда они нарисованы в 3D. Эмпирическое правило таково: это должно быть легко, а не сложно. Если он слишком занят, ему, вероятно, нужно другое измерение.

Просто играем, соединяем провода под обертками. Несколько самоочевидных диаграмм, и все готово.


Вот вам несколько типов мандал (опять же, перевернутых):

 -- <

gt; -- <*> -- =<<
f a f a f a
(a -> b) f (a -> b) (a -> f b)
f b f b f ( f b) -- fmapped, and
f b -- joined

и, конечно же, мать всех приложений,

 --      $
      a
      a -> b
           b
 

он же Modus Ponens (да, тоже перевернутый).

Комментарии:

1. Вам нужно только Functor g , как я указал в комментарии.

2. @dfeuer возможно, мы можем добавить это в качестве заключительного замечания… Я больше играл с операционным кодом, пытаясь показать им, как это делается. Я не чувствую, что это очень важно, хотя, возможно, это подпись, которую им дали, поэтому, естественно, приложение уже является функтором.

3. @dfeuer, конечно, это лишнее. может быть, я отредактирую его позже (мне не нравится, когда в ленте говорится «…отредактировано» вместо «…ответил», я чувствую, что с «ответил» больше шансов, что кто-то вообще посмотрит на это, а с «отредактировано» меньше шансов на это … просто у меня такое чувство)