Подтвердить что ты не робот

Оператор композиции функции Хаскеля типа (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, но это не то же самое.

Можно ли делать то, что я хочу? Если нет, то я не понимаю, что заставляет меня думать, что это возможно?


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

4b9b3361

Ответ 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