Составление функции композиции: как (.).(.) Работает?
(.)
принимает две функции, которые принимают одно значение и возвращают значение:
(.) :: (b -> c) -> (a -> b) -> a -> c
поскольку (.)
принимает два аргумента, я чувствую, (.).(.)
должен быть недействительным, но это совершенно нормально:
(.).(.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c
Что здесь происходит? Я понимаю, что этот вопрос плохо сформулирован... все функции действительно принимают один аргумент благодаря карри. Возможно, лучший способ сказать, что типы не совпадают.
7 ответов
Давайте сначала поиграем в typechecker для механического доказательства. Я опишу интуитивный способ думать об этом позже.
Я хочу подать заявку (.)
в (.)
а потом я подам заявку (.)
к результату. Первое приложение помогает нам определить некоторые эквивалентности переменных.
((.) :: (b -> c) -> (a -> b) -> a -> c)
((.) :: (b' -> c') -> (a' -> b') -> a' -> c')
((.) :: (b'' -> c'') -> (a'' -> b'') -> a'' -> c'')
let b = (b' -> c')
c = (a' -> b') -> a' -> c'
((.) (.) :: (a -> b) -> a -> c)
((.) :: (b'' -> c'') -> (a'' -> b'') -> a'' -> c'')
Тогда мы начинаем второе, но быстро застрять...
let a = (b'' -> c'')
Это ключ: мы хотим let b = (a'' -> b'') -> a'' -> c''
, но мы уже определили b
Таким образом, вместо этого мы должны попытаться объединить --- чтобы соответствовать нашим двум определениям как можно лучше. К счастью, они совпадают
UNIFY b = (b' -> c') =:= (a'' -> b'') -> a'' -> c''
which implies
b' = a'' -> b''
c' = a'' -> c''
и с этими определениями / унификациями мы можем продолжить применение
((.) (.) (.) :: (b'' -> c'') -> (a' -> b') -> (a' -> c'))
затем расширить
((.) (.) (.) :: (b'' -> c'') -> (a' -> a'' -> b'') -> (a' -> a'' -> c''))
и убери это
substitute b'' -> b
c'' -> c
a' -> a
a'' -> a1
(.).(.) :: (b -> c) -> (a -> a1 -> b) -> (a -> a1 -> c)
что, честно говоря, немного противоречивый результат.
Вот эта интуиция. Сначала взгляните на fmap
fmap :: (a -> b) -> (f a -> f b)
это "поднимает" функцию в Functor
, Мы можем применять его повторно
fmap.fmap.fmap :: (Functor f, Functor g, Functor h)
=> (a -> b) -> (f (g (h a)) -> f (g (h b)))
позволяя нам поднять функцию в более глубокие и глубокие слои Functors
,
Оказывается, что тип данных (r ->)
это Functor
,
instance Functor ((->) r) where
fmap = (.)
который должен выглядеть довольно знакомым. Это означает, что fmap.fmap
переводит на (.).(.)
, Таким образом, (.).(.)
просто позволяет нам преобразовать параметрический тип более глубоких и глубоких слоев (r ->)
Functor
, (r ->)
Functor
на самом деле Reader
Monad
так слоистый Reader
s подобен наличию нескольких независимых видов глобального неизменного состояния.
Или как наличие нескольких входных аргументов, на которые не влияет fmap
ING. Вроде как составление новой функции продолжения для "просто результата" (>1) функции арности.
Наконец, стоит отметить, что если вы считаете, что этот материал интересен, он формирует основную интуицию, лежащую в основе получения линз в Control.Lens.
Давайте на мгновение проигнорируем типы и просто воспользуемся лямбда-исчислением.
Desugar инфиксная запись:
(.) (.) (.)
Eta-расширения:
(\ a b -> (.) a b) (\ c d -> (.) c d) (\ e f -> (.) e f)
Инлайн определение
(.)
:(\ a b x -> a (b x)) (\ c d y -> c (d y)) (\ e f z -> e (f z))
Замена
a
:(\ b x -> (\ c d y -> c (d y)) (b x)) (\ e f z -> e (f z))
Замена
b
:(\ x -> (\ c d y -> c (d y)) ((\ e f z -> e (f z)) x))
Замена
e
:(\ x -> (\ c d y -> c (d y)) (\ f z -> x (f z)))
Замена
c
:(\ x -> (\ d y -> (\ f z -> x (f z)) (d y)))
Замена
f
:(\ x -> (\ d y -> (\ z -> x (d y z))))
Ресугар лямбда нотация:
\ x d y z -> x (d y z)
И если вы спросите GHCi, вы обнаружите, что это имеет ожидаемый тип. Зачем? Поскольку стрелка функции является правой ассоциативной для поддержки карри: тип (b -> c) -> (a -> b) -> a -> c
действительно означает (b -> c) -> ((a -> b) -> (a -> c))
, В то же время переменная типа b
может означать любой тип, в том числе тип функции. Видишь связь?
Это один из тех аккуратных случаев, когда я думаю, что проще сначала понять более общий случай, а затем подумать о конкретном случае. Итак, давайте подумаем о функторах. Мы знаем, что функторы предоставляют способ отображать функции над структурой -
class Functor f where
fmap :: (a -> b) -> f a -> f b
Но что, если у нас есть два слоя функтора? Например, список списков? В этом случае мы можем использовать два слоя fmap
>>> let xs = [[1,2,3], [4,5,6]]
>>> fmap (fmap (+10)) xs
[[11,12,13],[14,15,16]]
Но шаблон f (g x)
точно так же, как (f . g) x
чтобы мы могли написать
>>> (fmap . fmap) (+10) xs
[[11,12,13],[14,15,16]]
Какой тип fmap . fmap
?
>>> :t fmap.fmap
:: (Functor g, Functor f) => (a -> b) -> f (g a) -> f (g b)
Мы видим, что он отображает два слоя функтора, как мы и хотели. Но теперь запомни это (->) r
является функтором (тип функций из r
, который вы можете прочитать как (r ->)
) и его экземпляр функтора
instance Functor ((->) r) where
fmap f g = f . g
Для функции fmap
это просто функциональная композиция! Когда мы сочиняем два fmap
s мы отображаем два уровня функтора функции. У нас изначально есть что-то типа (->) s ((->) r a)
, что эквивалентно s -> r -> a
и мы в конечном итоге с чем-то типа s -> r -> b
так что тип (.).(.)
должно быть
(.).(.) :: (a -> b) -> (s -> r -> a) -> (s -> r -> b)
которая берет свою первую функцию и использует ее для преобразования вывода второй функции (с двумя аргументами). Так, например, функция ((.).(.)) show (+)
является функцией двух аргументов, которая сначала складывает свои аргументы вместе, а затем преобразует результат в String
с помощью show
:
>>> ((.).(.)) show (+) 11 22
"33"
Затем возникает естественное обобщение мышления о более длинных цепях fmap
, например
fmap.fmap.fmap ::
(Functor f, Functor g, Functor h) => (a -> b) -> f (g (h a)) -> f (g (h b))
который отображает три слоя функтора, что эквивалентно составлению с функцией трех аргументов:
(.).(.).(.) :: (a -> b) -> (r -> s -> t -> a) -> (r -> s -> t -> b)
например
>>> import Data.Map
>>> ((.).(.).(.)) show insert 1 True empty
"fromList [(1,True)]"
который вставляет значение True
в пустую карту с ключом 1
, а затем преобразует вывод в строку с show
,
Эти функции могут быть в целом полезны, поэтому вы иногда видите их как
(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(.:) = (.).(.)
так что вы можете написать
>>> let f = show .: (+)
>>> f 10 20
"30"
Конечно, более простое, точное определение (.:)
можно дать
(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(f .: g) x y = f (g x y)
который может помочь демистифицировать (.).(.)
в некотором роде.
Вот более простой пример того же явления:
id :: a -> a
id x = x
Тип идентификатора говорит, что идентификатор должен принимать один аргумент. И действительно, мы можем назвать это одним аргументом:
> id "hello"
"hello"
Но получается, что мы можем также назвать это двумя аргументами:
> id not True
False
Или даже:
> id id "hello"
"hello"
Что здесь происходит? Ключ к пониманию id not True
это сначала посмотреть на id not
, Понятно, что это разрешено, потому что он применяет id к одному аргументу. Тип not
является Bool -> Bool
так что мы знаем, что a
от типа id должно быть Bool -> Bool
Итак, мы знаем, что это вхождение id имеет тип:
id :: (Bool -> Bool) -> (Bool -> Bool)
Или с меньшим количеством скобок:
id :: (Bool -> Bool) -> Bool -> Bool
Так что это вхождение id на самом деле принимает два аргумента.
То же самое рассуждение также работает для id id "hello"
а также (.) . (.)
,
Ты прав, (.)
принимает только два аргумента. Вы просто, кажется, путаете с синтаксисом haskell. В выражении (.).(.)
на самом деле это точка в середине, которая принимает две другие точки в качестве аргумента, как в выражении 100 + 200
, который можно записать как (+) 100 200
,
(.).(.) === (number the dots)
(1.)2.(3.) === (rewrite using just syntax rules)
(2.)(1.)(3.) === (unnumber and put spaces)
(.) (.) (.) ===
И это должно быть еще более ясно из (.) (.) (.)
что первое (.)
берет второй (.)
и третье (.)
как это аргументы.
Да, это связано с карри. (.)
поскольку все функции в Haskell принимают только один аргумент. То, что вы составляете, - это первый частичный вызов каждому соответствующему (.)
который принимает свой первый аргумент (первая функция композиции).
(Сначала прочтите мой ответ о композиции функций, операторе $ и стиле без точек.)
Представьте, что у вас есть простая функция: она складывает 2 числа, а затем сводит на нет результат. Мы назовем это foo
:
foo a b = negate (a + b)
Теперь давайте сделаем это шаг за шагом и посмотрим, чем мы закончим:
foo a b = negate $ a + b
foo a b = negate $ (+) a b
foo a b = negate $ (+) a $ b
foo a b = negate . (+) a $ b
foo a = negate . (+) a -- f x = g x is equivalent to f = g
foo a = (.) negate ((+) a) -- any infix operator is just a function
foo a = (negate.) ((+) a) -- (2+) is the same as ((+) 2)
foo a = (negate.) $ (+) a
foo a = (negate.) . (+) $ a
foo = (negate.) . (+)
foo = ((.) negate) . (+)
foo = (.) ((.) negate) (+) -- move dot in the middle in prefix position
foo = ((.) ((.) negate)) (+) -- add extra parentheses
Теперь давайте проанализируем выражение (.) ((.) negate)
ближе. Это частичное применение (.)
функция, первый аргумент которой ((.) negate)
, Можем ли мы изменить это еще дальше? Да мы можем:
(.) ((.) negate)
(.) . (.) $ negate -- because f (f x) is the same as (f . f) x
(.)(.)(.) $ negate
((.)(.)(.)) negate
(.).(.)
эквивалентно (.)(.)(.)
, потому что в 1-м выражении точка в середине может быть перемещена в позиции префикса и заключена в круглые скобки, что приводит к 2-му выражению.
Теперь мы можем переписать наш foo
функция:
foo = ((.).(.)) negate (+)
foo = ((.)(.)(.)) negate (+) -- same as previous one
foo = negate .: (+)
where (.:) = (.).(.)
Теперь вы знаете, что (.).(.)
эквивалентно (\f g x y -> f (g x y))
:
(\f g x y -> f (g x y)) negate (+) 2 3 -- returns -5
((.).(.)) negate (+) 2 3 -- returns -5