Как fmap fmap применяется к функциям (в качестве аргументов)?

Я пытаюсь понять как fmap fmap относится к такой функции, как, скажем, (*3),

Тип fmap fmap:

(fmap fmap):: (Functor f1, Functor f) => f (a -> b) -> f (f1 a -> f1 b)

Тип (*3):

(*3) :: Num a => a -> a

Что означает, что подпись a -> a соответствует f (a -> b), право?

Prelude> :t (fmap fmap (*3))
(fmap fmap (*3)):: (Num (a -> b), Functor f) => (a -> b) -> f a -> f b

Я попытался создать простой тест:

test :: (Functor f) => f (a -> b) -> Bool 
test f = True

И кормление (*3) в это, но я получаю:

*Main> :t (test (*3))

<interactive>:1:8:
    No instance for (Num (a0 -> b0)) arising from a use of ‘*’
    In the first argument of ‘test’, namely ‘(* 3)’
    In the expression: (test (* 3))

Почему это происходит?

1 ответ

Полиморфизм опасен, когда вы не знаете, что делаете. И то и другое fmap а также (*) являются полиморфными функциями, и их слепое использование может привести к очень запутанному (и, возможно, неправильному) коду. Я ответил на аналогичный вопрос раньше:

Что происходит, когда я пишу * с + в Haskell?

В таких случаях я считаю, что рассмотрение типов значений может помочь вам выяснить, где вы идете не так и как устранить проблему. Давайте начнем с подписи типа fmap:

fmap :: Functor f => (a -> b) -> f a -> f b
                     |______|    |________|
                         |            |
                      domain      codomain

Тип подписи fmap это легко понять. Это поднимает функцию от a в b в контекст функтора, каким бы ни был этот функтор (например, список, может быть, любой и т. д.).

Слова "домен" и "кодомен" просто означают "вход" и "выход" соответственно. Во всяком случае, давайте посмотрим, что происходит, когда мы применяем fmap в fmap:

fmap :: Functor f => (a -> b) -> f a -> f b
                     |______|
                         |
                       fmap :: Functor g => (x -> y) -> g x -> g y
                                            |______|    |________|
                                                |            |
                                                a    ->      b

Как вы видете, a := x -> y а также b := g x -> g y, В дополнение Functor g ограничение добавлено. Это дает нам тип подписи fmap fmap:

fmap fmap :: (Functor f, Functor g) => f (x -> y) -> f (g x -> g y)

Итак, что же fmap fmap делать? Первый fmap поднял второй fmap в контексте функтора f, Скажем так f является Maybe, Отсюда по специализации:

fmap fmap :: Functor g => Maybe (x -> y) -> Maybe (g x -> g y)

следовательно fmap fmap должны быть применены к Maybe значение с функцией внутри него. Какие fmap fmap делает то, что он поднимает функцию внутри Maybe значение в контексте другого функтора g, Скажем так g является [], Отсюда по специализации:

fmap fmap :: Maybe (x -> y) -> Maybe ([x] -> [y])

Если мы подаем заявку fmap fmap в Nothing тогда мы получим Nothing, Однако, если мы применим его к Just (+1) тогда мы получаем функцию, которая увеличивает каждый номер списка, завернутый в Just конструктор (т.е. мы получаем Just (fmap (+1))).

Тем не мение, fmap fmap является более общим. Что он на самом деле делает это, что он выглядит внутри функтора f (без разницы f может быть) и поднимает функцию (и) внутри f в контексте другого функтора g,

Все идет нормально. Так в чем проблема? Проблема в том, когда вы подаете заявление fmap fmap в (*3), Это глупо и опасно, как пьянство за рулем. Позвольте мне рассказать вам, почему это глупо и опасно. Посмотрите на тип подписи (*3):

(*3) :: Num a => a -> a

Когда вы подаете заявку fmap fmap в (*3) тогда функтор f специализируется на (->) r (т.е. функция). Функция является допустимым функтором. fmap функция для (->) r это просто функция композиции. Следовательно, тип fmap fmap по специализации это:

fmap fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y

-- or

(.)  fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
                          |___________|
                                |
                              (*3) :: Num a => a ->    a
                                               |       |
                                               |    ------
                                               |    |    |
                                               r -> x -> y

Вы понимаете, почему это глупо и опасно?

  1. Это глупо, потому что вы применяете функцию, которая ожидает входную функцию с двумя аргументами (r -> x -> y) функции с одним аргументом, (*3) :: Num a => a -> a,
  2. Это опасно, потому что выход (*3) полиморфный. Следовательно, компилятор не говорит вам, что вы делаете что-то глупое. К счастью, поскольку выход ограничен, вы получаете ограничение типа Num (x -> y) что должно указывать на то, что вы где-то ошиблись.

Разработка типов, r := a := x -> y, Следовательно, мы получаем следующую сигнатуру типа:

fmap . (*3) :: (Num (x -> y), Functor g) => (x -> y) -> g x -> g y

Позвольте мне показать вам, почему это неправильно, используя значения:

  fmap . (*3)
= \x -> fmap (x * 3)
             |_____|
                |
                +--> You are trying to lift a number into the context of a functor!

Что вы действительно хотите сделать, это применить fmap fmap в (*), которая является двоичной функцией:

(.) fmap :: Functor g => (r -> x -> y) -> r -> g x -> g y
                         |___________|
                               |
                              (*) :: Num a => a -> a -> a
                                              |    |    |
                                              r -> x -> y

Следовательно, r := x := y := a, Это дает вам подпись типа:

fmap . (*) :: (Num a, Functor g) => a -> g a -> g a

Это имеет еще больший смысл, когда вы видите значения:

  fmap . (*)
= \x -> fmap (x *)

Следовательно, fmap fmap (*) 3 это просто fmap (3*),

Наконец, у вас та же проблема с вашим test функция:

test :: Functor f => f (a -> b) -> Bool

По специализации функтор f в (->) r мы получаем:

test :: (r -> a -> b) -> Bool
        |___________|
              |
            (*3) :: Num x => x ->    x
                             |       |
                             |    ------
                             |    |    |
                             r -> a -> b

Следовательно, r := x := a -> b, Таким образом мы получаем сигнатуру типа:

test (*3) :: Num (a -> b) => Bool

Так как ни a ни b появляются в типе вывода, ограничение Num (a -> b) должны быть решены немедленно. Если a или же b появились в типе вывода, то они могут быть специализированными и другой экземпляр Num (a -> b) может быть выбран. Однако, поскольку они не отображаются в типе вывода, компилятор должен решить, какой экземпляр Num (a -> b) выбирать сразу; и потому что Num (a -> b) это глупое ограничение, у которого нет экземпляра, компилятор выдает ошибку.

Если вы попытаетесь test (*) тогда вы не получите никакой ошибки по той же причине, о которой я упоминал выше.

Другие вопросы по тегам