Как (fmap. Fmap) typechecks
Я просматриваю статью (http://comonad.com/reader/2012/abstracting-with-applicatives/) и нашел следующий фрагмент кода:
newtype Compose f g a = Compose (f (g a)) deriving Show
instance (Functor f, Functor g) => Functor (Compose f g) where
fmap f (Compose x) = Compose $ (fmap . fmap) f x
Как на самом деле (fmap . fmap)
typechecks?
Их типы:
(.) :: (a -> b) -> (r -> a) -> (r -> b)
fmap :: (a -> b) -> f a -> f b
fmap :: (a -> b) -> f a -> f b
Теперь отсюда я не вижу никакого способа, в котором fmap . fmap
будет typecheck?
Ответы
Ответ 1
Сначала измените имена типов переменных на уникальные:
(.) :: (a -> b) -> (r -> a) -> (r -> b)
fmap :: Functor f => (c -> d) -> f c -> f d
fmap :: Functor g => (x -> y) -> g x -> g y
Теперь первый параметр .
имеет тип a -> b
и мы приводим аргумент типа (c -> d) -> (f c -> f d)
, поэтому a
- c -> d
и b
- f c -> f d
. Итак, до сих пор мы имеем:
(.) :: Functor f => -- Left operand
((c -> d) -> (f c -> f d)) ->
-- Right operand
(r -> (c -> d)) ->
-- Result
(r -> (f c -> f d))
Второй параметр .
имеет тип r -> a
aka r -> (c -> d)
, а аргумент, который мы приводим, имеет тип (x -> y) -> (g x -> g y)
, поэтому r
становится x -> y
, c
становится g x
и d
становится g y
. Итак, теперь мы имеем:
(.) :: (Functor f, Functor g) => -- Left operand
((g x -> g y) -> (f (g x) -> f (g y))) ->
-- Right operand
((x -> y) -> (g x -> g y)) ->
-- Result
(x -> y) -> f (g x) -> f (g y)
fmap.fmap :: (Functor f, Functor g) => (x -> y) -> f (g x) -> f (g y)
Ответ 2
Выражение fmap . fmap
имеет два экземпляра fmap
, которые могут, в принципе, иметь разные типы. Так что пусть их типы
fmap :: (x -> y) -> (g x -> g y)
fmap :: (u -> v) -> (f u -> f v)
Наша задача - унифицировать типы (что соответствует появлению отношений равенства между этими переменными типа), так что правая часть первого fmap
совпадает с левой частью второго fmap
. Надеюсь, вы увидите, что если вы установите u = g x
и v = g y
, вы получите
fmap :: ( x -> y) -> ( g x -> g y )
fmap :: (g x -> g y) -> (f (g x) -> f (g y))
Теперь тип компоновки
(.) :: (b -> c) -> (a -> b) -> (a -> c)
Чтобы сделать это, вы можете выбрать a = x -> y
и b = g x -> g y
и c = f (g x) -> f (g y)
, чтобы тип можно было записать
(.) :: ((g x -> g y) -> (f (g x) -> f (g y))) -> ((x -> y) -> (g x -> g y)) -> ((x -> y) -> (f (g x) -> f (g y)))
который довольно громоздкий, но это просто специализация исходной сигнатуры типа для (.)
. Теперь вы можете проверить, что все соответствует такому, что fmap . fmap
typechecks.
Альтернативой является подход к нему с противоположного направления. Скажем, что у вас есть объект, имеющий два уровня функториальности, например
>> let x = [Just "Alice", Nothing, Just "Bob"]
и у вас есть функция, которая добавляет удары в любую строку
bang :: String -> String
bang str = str ++ "!"
Вы хотите добавить удар по каждой из строк в x
. Вы можете перейти от String -> String
к Maybe String -> Maybe String
с одним уровнем fmap
fmap bang :: Maybe String -> Maybe String
и вы можете перейти к [Maybe String] -> [Maybe String]
с другим приложением fmap
fmap (fmap bang) :: [Maybe String] -> [Maybe String]
Это делает то, что мы хотим? Ад, да!
>> fmap (fmap bang) x
[Just "Alice!", Nothing, Just "Bob!"]
Пусть напишите функцию утилиты fmap2
, которая принимает любую функцию f
и применяет к ней fmap
дважды, чтобы мы могли просто написать fmap2 bang x
. Это будет выглядеть как
fmap2 f x = fmap (fmap f) x
Вы можете отказаться от x
с обеих сторон
fmap2 f = fmap (fmap f)
Теперь вы понимаете, что шаблон g (h x)
совпадает с (g . h) x
, поэтому вы можете написать
fmap2 f = (fmap . fmap) f
чтобы теперь вы могли отбросить f
с обеих сторон
fmap2 = fmap . fmap
которая является интересующей вас функцией. Таким образом, вы видите, что fmap . fmap
просто принимает функцию и дважды применяет к ней fmap
, чтобы ее можно было снять с двух уровней функториальности.
Ответ 3
Старый вопрос, но для меня, концептуально, fmap
представляет "принятие a -> b
и приведение его" на один уровень вверх "на f a -> f b
".
Итак, если у меня есть a -> b
, я могу fmap
дать мне f a -> f b
.
Если у меня был f a -> f b
, я могу fmap
снова дать мне g (f a) -> g (f a)
. Поднимите функцию f a -> f b
на новые высоты - новый уровень.
Итак, "fmapping" один раз поднимает функцию один раз. fmapping дважды лифты, которые подняли функцию... так что двойной подъем.
Вставьте язык синтаксиса haskell:
f :: a -> b
fmap f :: f a -> f b
fmap (fmap f) :: g (f a) -> g (f b)
fmap (fmap (fmap f)) :: h (g (f a)) -> h (g (f b))
Обратите внимание, как каждый следующий fmap
поднимает исходный a -> b
на новый уровень. Таким образом,
fmap :: (a -> b) -> ( f a -> f b )
fmap . fmap :: (a -> b) -> ( g (f a) -> g (f b) )
fmap . fmap . fmap :: (a -> b) -> (h (g (f a)) -> h (g (f a)))
Любая "функция более высокого порядка", которая возвращает функцию той же степени, что и ее вход, может это сделать. Возьмите zipWith :: (a -> b -> c) -> ([a] -> [b] -> [c])
, который принимает функцию, берущую два аргумента, и возвращает новую функцию с двумя аргументами. Мы можем цепью zipWith
так же:
f :: a -> b -> c
zipWith f :: [a] -> [b] -> [c]
zipWith (zipWith f) :: [[a]] -> [[b]] -> [[c]]
Итак,
zipWith :: (a -> b -> c) -> ( [a] -> [b] -> [c] )
zipWith . zipWith :: (a -> b -> c) -> ([[a]] -> [[b]] -> [[c]])
liftA2
работает практически так же:
f :: a -> b -> c
liftA2 f :: f a -> f b -> f c
liftA2 (liftA2 f) :: g (f a) -> g (f b) -> g (f c)
Один довольно удивительный пример, который широко используется в современной реализации библиотеки объективов, traverse
:
f :: a -> IO b
traverse f :: f a -> IO ( f b )
traverse (traverse f) :: g (f a) -> IO ( g (f b) )
traverse (traverse (traverse f)) :: h (g (f a)) -> IO (h (g (f b)))
Итак, вы можете иметь такие вещи, как:
traverse :: (a -> m b) -> ( f a -> m ( f b ))
traverse . traverse :: (a -> m b) -> (g (f a) -> m (g (f b)))