Что происходит, когда я сочиняю * С + в Haskell?
Я пытаюсь понять результат
(*) . (+)
В Хаскелле. Я знаю, что оператор композиции - это просто стандартная композиция математических функций-так что
(f . g) = f (g x)
Но:
(*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
Я изо всех сил пытаюсь понять этот тип сигнатуры. Я ожидал, что смогу делать такие вещи, как:
((*) . (+)) 1 2 :: Num a => a -> a
= (* (+ 1 2))
Что означает ( * ). (+) тип подписи? Я попробовал поиграть с ним чем-то вроде (просто совпадая с его подписью):
((*) . (+)) 1 (x -> x + 1) 1
Но это не удается скомпилировать. Я пытаюсь пройти через логические шаги при их составлении, но я не совсем понимаю, как это происходит (и что такое результат ).
5 ответов:
Я понимаю, что ты чувствуешь. Я обнаружил, что функциональная композиция поначалу тоже была довольно трудной для понимания. Что помогло мне разобраться в этом вопросе, так это типовые подписи. Рассмотрим:
Теперь, когда вы пишете(*) :: Num x => x -> x -> x (+) :: Num y => y -> y -> y (.) :: (b -> c) -> (a -> b) -> a -> c
(*) . (+)
, это фактически то же самое, что(.) (*) (+)
(т. е.(*)
является первым аргументом для(.)
и(+)
является вторым аргументом для(.)
):(.) :: (b -> c) -> (a -> b) -> a -> c |______| |______| | | (*) (+)
Следовательно, сигнатура типа
(*)
(то естьNum x => x -> x -> x
) объединяется сb -> c
:(*) :: Num x => x -> x -> x -- remember that `x -> x -> x` | |____| -- is implicitly `x -> (x -> x)` | | b -> c (.) (*) :: (a -> b) -> a -> c | | | |‾‾‾‾| Num x => x x -> x (.) (*) :: Num x => (a -> x) -> a -> x -> x
Отсюда сигнатура типа
(+)
(т. е.Num y => y -> y -> y
) объединяется сNum x => a -> x
:Я надеюсь, что это проясняет, откуда берутся(+) :: Num y => y -> y -> y -- remember that `y -> y -> y` | |____| -- is implicitly `y -> (y -> y)` | | Num x => a -> x (.) (*) (+) :: Num x => a -> x -> x | | | | |‾‾‾‾| |‾‾‾‾| Num y => y y -> y y -> y (.) (*) (+) :: (Num (y -> y), Num y) => y -> (y -> y) -> y -> y
Num (y -> y)
иNum y
. Вы остаетесь с очень странной функцией типа(Num (y -> y), Num y) => y -> (y -> y) -> y -> y
.Что делает его настолько странным, так это то, что он ожидает, что и
y
, иy -> y
будут экземплярамиNum
. Понятно, чтоy
должен быть примеромNum
, но какy -> y
? Созданиеy -> y
экземпляраNum
кажется нелогичным. Это не может быть правдой.Однако это имеет смысл, когда вы смотрите на то, какая функция композиция действительно делает:
( f . g ) = \z -> f ( g z) ((*) . (+)) = \z -> (*) ((+) z)
Итак, у вас есть функция
\z -> (*) ((+) z)
. Следовательно,z
явно должен быть экземпляромNum
, потому что к нему применяется(+)
. Таким образом, тип\z -> (*) ((+) z)
- этоNum t => t -> ...
, где...
- это тип(*) ((+) z)
, который мы сейчас выясним.Поэтому
((+) z)
относится к типуNum t => t -> t
, потому что для него требуется еще одно число. Однако, прежде чем он будет применен к другому числу, к нему применяется(*)
.Следовательно,
(*)
ожидает, что((+) z)
является экземпляромNum
, именно поэтомуt -> t
должен быть примеромNum
. Таким образом,...
заменяется(t -> t) -> t -> t
и добавляется ограничениеNum (t -> t)
, что приводит к типу(Num (t -> t), Num t) => t -> (t -> t) -> t -> t
.Способ, которым вы действительно хотите объединить
(*)
и(+)
, заключается в использовании(.:)
:(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d f .: g = \x y -> f (g x y)
Следовательно,
(*) .: (+)
то же самое, что\x y -> (*) ((+) x y)
. Теперь два аргумента даются(+)
, гарантируя, что((+) x y)
действительно простоNum t => t
, а неNum t => t -> t
.Следовательно,
Обратите внимание, что((*) .: (+)) 2 3 5
есть(*) ((+) 2 3) 5
то, что есть(*) 5 5
то, что есть25
, что я считаю тем, что вы хотеть.f .: g
также может быть записано как(f .) . g
, а(.:)
также может быть определено как(.:) = (.) . (.)
. Подробнее об этом можно прочитать здесь: Надеюсь, это поможет.
(*)
и(+)
оба имеют сигнатуру типаNum a => a -> a -> a
Теперь, если вы их сочините, вы получите что-то обалденное.(*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
Это потому, что
(*)
и(+)
ожидают два "аргумента".(+) с одним аргументом получает функцию. Оператор
.
ожидает эту функцию (a -> a
, которую вы видите).Вот значение
(*) . (+)
x f y (*) . (+) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
На самом деле, эта функция вообще не имеет смысла.
(*) . (+)
отображениеx f y
в((x +) * f) y
, гдеf
- функция отa
доa
, которая также является числом. Причина(*)
функция ожидает, что типы будут совпадать, пока она ожидает два аргумента, но эта функция должна быть числом, потому что(*)
работает только с числами.
Сначала некоторые расширения:
{-# LANGUAGE FlexibleContexts, FlexibleInstances, TypeSynonymInstances #-}
Как показывают другие ответы, ваша функция -
Но эта функция имеет не странную семантику.weird :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a weird x g = (x +) * g
Существует понятие списков различий. Соответственно, существует понятие разностных целых чисел. Я видел, что они используются только в зависимости от типизации (например, здесь , но это не единственный случай). Соответствующая часть определения является
instance Enum DiffInt where toEnum n = (n +) fromEnum n = n 0 instance Num DiffInt where n + m = n . m n * m = foldr (+) id $ replicate (fromEnum n) m
В этом нет особого смысла. Haskell, но может быть полезен с зависимыми типами.
Теперь мы можем написать
test :: DiffInt test = toEnum 3 * toEnum 4
Или
test :: DiffInt test = weird 3 (toEnum 4)
В обоих случаях
fromEnum test == 12
.EDIT
Можно избежать использования расширения
TypeSynonymInstances
:{-# LANGUAGE FlexibleContexts, FlexibleInstances #-} weird :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a weird x g = (x +) * g instance (Enum a, Num a) => Enum (a -> a) where toEnum n = (toEnum n +) fromEnum n = fromEnum $ n (toEnum 0) instance (Enum a, Num a) => Num (a -> a) where n + m = n . m n * m = foldr (+) id $ replicate (fromEnum n) m type DiffInt = Int -> Int
Как и прежде, мы можем написать
test' :: DiffInt test' = weird 3 (toEnum 4)
Но теперь мы можем также написать
-- difference ints over difference ints type DiffDiffInt = DiffInt -> DiffInt test'' :: DiffDiffInt test'' = weird (toEnum 3) (toEnum (toEnum 4))
И
main = print $ fromEnum $ fromEnum test'
Отпечатки
12
.EDIT2 добавлены лучшие ссылки.
Пусть:
m = (*) a = (+)
Затем
(m.a) x = (m (a x)) = m (a x)
Теперь
m
ожидаетNum a
в качестве параметра, с другой стороны(a x)
, т. е.(x +)
является унарной функцией(a -> a)
по определению(+)
. Я предполагаю, что произошло то, что GHC пытается объединить эти два типа так, чтобы, если у вас есть тип, который является одновременно числом и унарной функцией,m
может взять число и унарную функцию и вернуть унарную функцию, так как они считаются одним и тем же типом.Как указал @Сид, это объединение не сделает смысл для любых нормальных типов чисел, таких как целые числа и числа с плавающей запятой.
Здесь есть хорошие ответы, но позвольте мне быстро указать на несколько шагов, где вы ошиблись.
Во-первых, правильное определение состава функций -
(f . g) x = f (g x)
Вы пропустили
x
на LHS. Далее, вы должны помнить, что в Haskellh x y
это то же самое, что и(h x) y
. Итак, вопреки тому, что вы ожидали,И теперь вы видите, почему это не удается. Кроме того,((*) . (+)) 1 2 = (((*) . (+)) 1) 2 = ((*) ((+) 1)) 2 = ((+) 1) * 2,
((*) . (+)) 1 (\x -> x + 1) 1
Не работает, потому что ограничение
Num (Int -> Int)
не выполняется.