Что происходит, когда я сочиняю * С + в 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 -> yNum (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)не выполняется.