Как 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
Вы понимаете, почему это глупо и опасно?
- Это глупо, потому что вы применяете функцию, которая ожидает входную функцию с двумя аргументами (
r -> x -> y
) функции с одним аргументом,(*3) :: Num a => a -> a
, - Это опасно, потому что выход
(*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 (*)
тогда вы не получите никакой ошибки по той же причине, о которой я упоминал выше.