Оператор композиции функции Хаскеля типа (c → d) → (a → b → c) → (a → b → d)
Обычная композиция функций имеет тип
(.) :: (b -> c) -> (a -> b) -> a -> c
Я полагаю, что это должно быть обобщено на такие типы, как:
(.) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
Конкретный пример: вычисление разностного квадрата. Мы могли бы написать diffsq a b = (a - b) ^ 2
, но мне кажется, что я должен написать (-)
и (^2)
, чтобы написать что-то вроде diffsq = (^2) . (-)
.
Я не могу, конечно. Единственное, что я могу сделать, это использовать кортеж вместо двух аргументов в (-)
, преобразуя его с помощью uncurry
, но это не то же самое.
Можно ли делать то, что я хочу? Если нет, то я не понимаю, что заставляет меня думать, что это возможно?
Примечание: Это уже было предложено здесь, но ответ (который, как я подозреваю, должен существовать) не был указан.
Ответы
Ответ 1
Непонятно, что вы думаете о функции типа a -> b -> c
как функции двух аргументов с типом возврата c
, тогда как на самом деле это функция одного аргумента с типом возврата b -> c
, потому что тип функции (т.е. то же, что и a -> (b -> c)
, что делает невозможным использование стандартного оператора композиции функций.
Чтобы понять, почему, попробуйте применить оператор (.)
, который имеет тип оператора (y -> z) -> (x -> y) -> (x -> z)
, к двум функциям, g :: c -> d
и f :: a -> (b -> c)
. Это означает, что мы должны объединить y
с c
, а также с b -> c
. Это не имеет большого смысла. Как y
может быть как c
, так и функция, возвращающая c
? Это должно быть бесконечным типом. Так что это не работает.
Просто потому, что мы не можем использовать стандартный оператор композиции, это не мешает нам определять наш собственный.
compose2 :: (c -> d) -> (a -> b -> c) -> a -> b -> d
compose2 g f x y = g (f x y)
diffsq = (^2) `compose2` (-)
Обычно лучше избегать использования стиля без точек в этом случае и просто пойти с
diffsq a b = (a-b)^2
Ответ 2
Моя предпочтительная реализация для этого -
fmap . fmap :: (Functor f, Functor f1) => (a -> b) -> f (f1 a) -> f (f1 b)
Если только потому, что его довольно легко запомнить.
При создании экземпляров f и f1 до (->) c
и (->) d
соответственно вы получаете тип
(a -> b) -> (c -> d -> a) -> c -> d -> b
который является типом
(.) . (.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c
но немного легче справиться с версией fmap . fmap
, и она обобщается на другие функторы.
Иногда это записывается fmap fmap fmap
, но записывается как fmap . fmap
, его можно более легко расширить, чтобы разрешить больше аргументов.
fmap . fmap . fmap
:: (Functor f, Functor g, Functor h) => (a -> b) -> f (g (h a)) -> f (g (h b))
fmap . fmap . fmap . fmap
:: (Functor f, Functor g, Functor h, Functor i) => (a -> b) -> f (g (h (i a))) -> f (g (h (i b))
и др.
В общем fmap
, состоящий из себя n раз, можно использовать для fmap
n уровней глубоко!
А так как функции образуют a Functor
, это обеспечивает сантехнику для n аргументов.
Дополнительные сведения см. в разделе Коналл Эллиот Комбинаторы семантического редактора.
Ответ 3
Я не знаю стандартную библиотечную функцию, которая делает это, но шаблон без точек, который его выполняет, состоит в том, чтобы составить композиционную функцию:
(.) . (.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c
Ответ 4
Я собирался написать это в комментарии, но он немного длинный, и он рисуется как из могучего, так и из хаммар.
Я предлагаю стандартизировать вокруг операторов, таких как .*
для compose2
и .**
для compose3
. Использование определения могучего байта:
(.*) :: (c -> d) -> (a -> b -> c) -> (a -> b -> d)
(.*) = (.) . (.)
(.**) :: (d -> e) -> (a -> b -> c -> d) -> (a -> b -> c -> e)
(.**) = (.) . (.*)
diffsq :: (Num a) => a -> a -> a
diffsq = (^2) .* (-)
modminus :: (Integral a) => a -> a -> a -> a
modminus n = (`mod` n) .* (-)
diffsqmod :: (Integral a) => a -> a -> a -> a
diffsqmod = (^2) .** modminus
Да, modminus
и diffsqmod
- очень случайные и бесполезные функции, но они были быстрыми и показывают суть. Обратите внимание, насколько устрашающе легко определить следующий уровень, составив в другой функции компоновки (аналогично цепочке fmap
, упомянутой Эдвардом).
(.***) = (.) . (.**)
С практической точки зрения, от compose12
вверх, короче написать имя функции, а не оператор
f .*********** g
f `compose12` g
Хотя подсчет звездочек утомителен, поэтому мы можем остановить соглашение в 4 или 5.
[edit] Еще одна случайная идея, мы могли бы использовать .:
для compose2, .:.
для compose3, .::
для compose4, .::.
для compose5, .:::
для compose6, давая количество точек (после первоначальный) визуально обозначить, сколько аргументов развернуться. Мне кажется, мне нравятся звезды, хотя.
Ответ 5
Здесь я думаю, что это элегантный способ добиться того, чего вы хотите. Класс типа Functor
дает возможность "нажимать" функцию вниз в контейнер, чтобы вы могли применить ее к каждому элементу с помощью fmap
. Вы можете представить функцию a -> b
как контейнер b
с каждым элементом, индексированным элементом a
. Поэтому естественно сделать этот экземпляр:
instance Functor ((->) a) where
fmap f g = f . g
(я думаю, вы можете получить это import
в подходящей библиотеке, но я не могу вспомнить, какой из них.)
Теперь обычный состав f
с g
тривиально a fmap
:
o1 :: (c -> d) -> (b -> c) -> (b -> d)
f `o1` g = fmap f g
Функция типа a -> b -> c
представляет собой контейнер контейнеров элементов типа c
. Поэтому нам просто нужно дважды нажать нашу функцию f
. Вот вы:
o2 :: (c -> d) -> (a -> (b -> c)) -> a -> (b -> d)
f `o2` g = fmap (fmap f) g
На практике вам может показаться, что вам не нужны o1
или o2
, просто fmap
. И если вы можете найти библиотеку, местоположение которой я забыл, вы можете найти, что можете просто использовать fmap
без письменного разрешения
любой дополнительный код.
Ответ 6
Как Max указано в comment:
diffsq = ((^ 2) .) . (-)
Вы можете думать о f . g
как применении одного аргумента к g
, а затем передать результат в f
. (f .) . g
применяет два аргумента к g
, затем передает результат на f
. ((f .) .) . g
применяет три аргумента к g
и т.д.
\f g -> (f .) . g :: (c -> d) -> (a -> b -> c) -> a -> b -> d
Если в левом сечении оператор композиции с некоторой функцией f :: c -> d
(частичное приложение с f
слева), получим:
(f .) :: (b -> c) -> b -> d
Итак, у нас есть эта новая функция, которая ожидает функцию от b -> c
, но наша g
равна a -> b -> c
или, что то же самое, a -> (b -> c)
. Нам нужно применить a
, прежде чем мы сможем получить то, что нам нужно. Ну, повторите итерацию:
((f .) .) :: (a -> b -> c) -> a -> b -> d