Состав составной функции: Как работает (.). (.)?

(.) принимает две функции, которые принимают значение one и возвращают значение:

(.) :: (b -> c) -> (a -> b) -> a -> c

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

(.).(.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c

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

Ответ 1

Пусть сначала сыграет 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)

он "поднимает" функцию вверх до a 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 похоже на несколько независимых типов глобального неизменяемого состояния.

Или как иметь несколько входных аргументов, на которые не влияет fmap ing. Похоже на составление новой функции продолжения на "просто результат" функции ( > 1) arity.


Наконец, стоит отметить, что если вы считаете, что этот материал интересен, он формирует основную интуицию за создавая объективы в Control.Lens.

Ответ 2

Позволяет игнорировать типы на мгновение и просто использовать лямбда-исчисление.

  • Десуарная инфиксная нотация:
    (.) (.) (.)

  • 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, вы увидите, что у этого есть ожидаемый тип. Зачем? Поскольку стрелка функции является право-ассоциативной для поддержки currying: тип (b -> c) -> (a -> b) -> a -> c действительно означает (b -> c) -> ((a -> b) -> (a -> c)). В то же время переменная типа b может стоять для любого типа, включая тип функции. См. Подключение?

Ответ 3

Вот более простой пример того же явления:

id :: a -> a
id x = x

Тип id указывает, что id должен принимать один аргумент. И действительно, мы можем назвать это одним аргументом:

> 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" и (.) . (.).

Ответ 4

Это один из тех опрятных случаев, когда я думаю, что сначала проще понять более общий случай, а затем подумать о конкретном случае. Поэтому подумайте о функторах. Мы знаем, что функторы обеспечивают способ отображения функций по структуре -

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 ((->) 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)

что может помочь немного демистифицировать (.).(.).

Ответ 5

Вы правы, (.) принимает только два аргумента. Вы просто смущены синтаксисом haskell. В выражении (.).(.) это фактически точка в середине, которая принимает две другие точки как аргумент, как в выражении 100 + 200, который может быть записан как (+) 100 200.

(.).(.) === (number the dots)
(1.)2.(3.) === (rewrite using just syntax rules)
(2.)(1.)(3.) === (unnumber and put spaces)
(.) (.) (.) ===

И из (.) (.) (.) должно быть еще более ясно, что первый (.) принимает в качестве аргументов второй (.) и третий (.).

Ответ 6

Да, это из-за карри. (.), поскольку все функции в Haskell принимают только один аргумент. То, что вы составляете, является первым частичным вызовом каждого соответствующего скомпонованного (.), который принимает свой первый аргумент (первая функция композиции).

Ответ 7

(сначала прочитайте мой ответ о композиции функций, $operator и point-free style.)

Представьте, что у вас простая функция: она добавляет 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

(.).(.) эквивалентен (.)(.)(.), так как в первом выражении точка в середине может перемещаться в префиксном положении и окружена скобками, что приводит к 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