Как я могу понять "(.). (.)"?
Я считаю, что я понимаю fmap . fmap
для Фунтеров, но по функциям у меня болит голова уже несколько месяцев.
Я видел, что вы можете просто применить определение (.)
к (.) . (.)
, но я забыл, как это сделать.
Когда я сам это пробовал, это всегда получается неправильно:
(.) f g = \x -> f (g x)
(.) (.) (.) = \x -> (.) ((.) x)
\x f -> (.) ((.) x) f
\x f y -> (((.)(f y)) x)
\x f y g-> (((.)(f y) g) x)
\x f y g-> ((f (g y)) x)
\x f y g-> ((f (g y)) x):: t2 -> (t1 -> t2 -> t) -> t3 -> (t3 -> t1) -> t
Если "просто применить определение" - единственный способ сделать это, как кто-нибудь придумал (.) . (.)
?
Должно быть какое-то более глубокое понимание или интуиция, которую мне не хватает.
Ответы
Ответ 1
Вы также можете использовать свое понимание fmap . fmap
.
Если у вас есть два Functor
foo
и bar
, то
fmap . fmap :: (a -> b) -> foo (bar a) -> foo (bar b)
fmap . fmap
берет функцию и производит индуцированную функцию для композиции двух Functor
s.
Теперь для любого типа t
, (->) t
является Functor
, а fmap
для этого Functor
- (.)
.
So (.) . (.)
есть fmap . fmap
для случая, когда два Functor
являются (->) s
и (->) t
, и, следовательно,
(.) . (.) :: (a -> b) -> ((->) s) ((->) t a) -> ((->) s) ((->) t b)
= (a -> b) -> (s -> (t -> a)) -> (s -> (t -> b))
= (a -> b) -> (s -> t -> a) -> (s -> t -> b)
он "составляет" функцию f :: a -> b
с функцией из двух аргументов, g :: s -> t -> a
,
((.) . (.)) f g = \x y -> f (g x y)
Это представление также дает понять, что и как, шаблон распространяется на функции, принимающие больше аргументов,
(.) . (.) . (.) :: (a -> b) -> (s -> t -> u -> a) -> (s -> t -> u -> b)
и др.
Ответ 2
Приступая к (.) . (.)
на самом деле довольно просто, это интуиция за тем, что она делает, что довольно сложно понять.
(.)
позволяет вам очень далеко, переписывая выражение в вычисления типа "pipe" (подумайте о |
в оболочке). Тем не менее, становится неудобно использовать, как только вы пытаетесь создать функцию, которая принимает несколько аргументов с помощью функции, которая принимает только одну. В качестве примера давайте определим concatMap
:
concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f xs = concat (map f xs)
Избавление от xs
- это стандартная операция:
concatMap f = concat . map f
Однако нет "приятного" способа избавиться от f
. Это вызвано тем фактом, что map
принимает два аргумента, и мы хотели бы применить concat
к его окончательному результату.
Вы можете, конечно, применить некоторые трюки с точки зрения и уйти просто с помощью (.)
:
concatMap f = (.) concat (map f)
concatMap f = (.) concat . map $ f
concatMap = (.) concat . map
concatMap = (concat .) . map
Но, увы, читаемость этого кода в основном отсутствует. Вместо этого мы вводим новый комбинатор, который делает именно то, что нам нужно: примените вторую функцию к окончательному результату первого.
-- .: is fairly standard name for this combinator
(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
(f .: g) x y = f (g x y)
concatMap = concat .: map
Хорошо, что это для мотивации. Позвольте перейти к беспроблемному бизнесу.
(.:) = \f g x y -> f (g x y)
= \f g x y -> f ((g x) y)
= \f g x y -> f . g x $ y
= \f g x -> f . g x
Теперь вот интересная часть. Это еще одна из трюков, которые обычно помогают, когда вы застреваете: мы переписываем .
в свою префиксную форму и пытаемся продолжить оттуда.
= \f g x -> (.) f (g x)
= \f g x -> (.) f . g $ x
= \f g -> (.) f . g
= \f g -> (.) ((.) f) g
= \f -> (.) ((.) f)
= \f -> (.) . (.) $ f
= (.) . (.)
Что касается интуиции, там очень хорошая статья, которую вы должны прочитать. Я перефразирую часть (.)
:
Давайте еще раз подумаем о том, что должен сделать наш комбинатор: он должен применить f
к результату результата g
(я использовал конечный результат в части перед целенаправленным, это действительно то, что вы получаете, когда вы полностью применять - модулировать переменные типа с другим типом функции - функцию g
, результатом здесь является просто приложение g x
для некоторого x
).
Что это значит для нас применить f
к результату g
? Ну, как только мы применим g
к некоторому значению, мы возьмем результат и применим к нему f
. Звучит знакомо: что делает (.)
.
result :: (b -> c) -> ((a -> b) -> (a -> c))
result = (.)
Теперь оказывается, что композиция (наше слово) этих комбинаторов - это просто композиция функции, то есть:
(.:) = result . result -- the result of result
Ответ 3
Ваше решение расходится, когда вы вводите y
. Это должно быть
\x f y -> ((.) ((.) x) f) y :: (c -> d) -> (a -> b -> c) -> a -> b -> d
\x f y z -> ((.) ((.) x) f) y z :: (c -> d) -> (a -> b -> c) -> a -> b -> d
\x f y z -> ((.) x (f y)) z :: (c -> d) -> (a -> b -> c) -> a -> b -> d
-- Or alternately:
\x f y z -> (x . f y) z :: (c -> d) -> (a -> b -> c) -> a -> b -> d
\x f y z -> (x (f y z)) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
Что соответствует оригинальной сигнатуре типа: (.) . (.) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
(Проще всего сделать расширение в ghci, где вы можете проверить каждый шаг с помощью :t expression
)
Edit:
Более глубокая интуиция такова:
(.)
просто определяется как
\f g -> \x -> f (g x)
Что мы можем упростить до
\f g x -> f (g x)
Поэтому, когда вы предоставляете ему два аргумента, он берет и все еще нуждается в другом аргументе для разрешения.
Каждый раз, когда вы используете (.)
с двумя аргументами, вы создаете "необходимость" для еще одного аргумента.
(.) . (.)
, разумеется, просто (.) (.) (.)
, поэтому разложим его:
(\f0 g0 x0 -> f0 (g0 x0)) (\f1 g1 x1 -> f1 (g1 x1)) (\f2 g2 x2 -> f2 (g2 x2))
Мы можем бета-уменьшить на f0
и g0
(но у нас нет x0
!):
\x0 -> (\f1 g1 x1 -> f1 (g1 x1)) ((\f2 g2 x2 -> f2 (g2 x2)) x0)
Замените второе выражение для f1
...
\x0 -> \g1 x1 -> ((\f2 g2 x2 -> f2 (g2 x2)) x0) (g1 x1)
Теперь он "откидывается назад"! (бета-сокращение на f2
):
Это интересный шаг - x0
заменяется на f2
. Это означает, что x
, который мог быть данными, вместо этого является функцией.
Это то, что (.) . (.)
предоставляет - "необходимость" для дополнительного аргумента.
\x0 -> \g1 x1 -> (\g2 x2 -> x0 (g2 x2)) (g1 x1)
Это начинает выглядеть нормально...
Пусть бета-сокращение в последний раз (на g2
):
\x0 -> \g1 x1 -> (\x2 -> x0 ((g1 x1) x2))
Итак, мы остались просто
\x0 g1 x1 x2 -> x0 ((g1 x1) x2)
где аргументы хорошо по порядку.
Ответ 4
Итак, это то, что я получаю, когда делаю несколько более инкрементное расширение
(.) f g = \x -> f (g x)
(.) . g = \x -> (.) (g x)
= \x -> \y -> (.) (g x) y
= \x -> \y -> \z -> (g x) (y z)
= \x y z -> (g x) (y z)
(.) . (.) = \x y z -> ((.) x) (y z)
= \x y z -> \k -> x (y z k)
= \x y z k -> x (y z k)
Что, согласно ghci, имеет правильный тип
Prelude> :t (.) . (.)
(.) . (.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c
Prelude> :t \x y z k -> x (y z k)
\x y z k -> x (y z k)
:: (t1 -> t) -> (t2 -> t3 -> t1) -> t2 -> t3 -> t
Prelude>
Пока я не знаю происхождения этого комбинатора, вполне вероятно, что это было
разработанный для использования в комбинаторной логике, где вы строго работаете с комбинаторами,
поэтому вы не можете определить вещи, используя более удобные лямбда-выражения. Может быть
какая-то интуиция, которая идет об этом, но я ее не нашел.
Скорее всего, вы бы разработали некоторый уровень интуиции, если вам нужно было сделать это достаточно.
Ответ 5
Проще всего написать уравнения, комбинатор-стиль, вместо лямбда-выражений: a b c = (\x -> ... body ...)
эквивалентно a b c x = ... body ...
, а порок наоборот, при условии, что x
не появляется среди {a,b,c}
. Таким образом,
-- _B = (.)
_B f g x = f (g x)
_B _B _B f g x y = _B (_B f) g x y
= (_B f) (g x) y
= _B f (g x) y
= f ((g x) y)
= f (g x y)
Вы обнаружите это, если, учитывая f (g x y)
, вы хотите преобразовать его в комбинационную форму (избавиться от всех круглых скобок и переменной повторы). Затем вы применяете шаблоны, соответствующие определениям комбинаторов, надеясь проследить этот вывод назад. Это намного меньше механического/автоматического.