Я пытаюсь понять, как рассуждать о типах для частичной цепочки примененных методов.
Я не понимаю, почему ::t (+)(+2) это (a->a)->a->a
или почему::t (+)(+) это (a->a->a)->a->a->a
я имею в виду для первого примера
Я не понимаю, когда я смотрю на (+), мне нужно смотреть на то, что ему нужно a->a->a или какой метод перед ним (+2) (который нуждается в a).
Во втором примере я знаю, что первому (+) нужны a->a->a, но как только я заполнил первый метод, почему второму снова нужны те же параметры?
Я это понимаю.
Это не методы, это функции. Использование правильной терминологии, даже в вашей собственной голове, поможет развеять любые неверные представления, которые вы можете привнести в Haskell из других языков.





Вы на самом деле немного неправильно указали тип. Там есть несколько важных ограничений класса типов:
(+)(+2) :: (Num a, Num (a -> a)) => (a -> a) -> a -> a
Так откуда это взялось? Это на самом деле довольно просто. Во-первых, подпись типа (+)
(+) :: Num a => a -> a -> a
Или, переписав, чтобы сделать каррирование явным:
(+) :: Num a => a -> (a -> a)
Между тем, тип (+2) (который является результатом выполнения именно этого частичного применения):
(+2) :: Num a => a -> a
Теперь, когда вы делаете (+)(+2), вы (частично) применяете функцию (+) к функции (+2). То есть мы рассматриваем (+2) как первый аргумент (+). Чтобы это работало, его тип — Num a => a -> a — должен быть экземпляром Num. Вот почему у нас есть еще одно ограничение типа, что a -> a должен быть экземпляром Num. (По умолчанию это не так, но вы можете определить свой собственный экземпляр для числовых функций — обычно он применяет обе функции к входным данным и добавляет результаты.)
Итак, давайте специализируем сигнатуру типа (+) для случая, когда она применяется к функции (a -> a) (которая, как я только что сказал, сама должна быть экземпляром Num, а также самой a). Мы получили:
(+) :: (Num a, Num (a -> a)) => (a -> a) -> (a -> a) -> (a -> a)
или с явным каррированием:
(+) :: (Num a, Num (a -> a)) => (a -> a) -> ((a -> a) -> (a -> a))
То есть он принимает функцию a -> a и возвращает «функцию более высокого порядка» типа (a -> a) -> (a -> a). Так что когда мы применим это к (+2), мы получим именно такую функцию высшего порядка:
(+)(+2) :: (Num a, Num (a -> a)) => (a -> a) -> (a -> a)
Это именно то, что сообщается, так как последняя пара скобок не нужна. (Это снова из-за каррирования.)
Второй случай полностью аналогичен, за исключением того, что вы применяете функцию a -> a -> a, а не a -> a.
Итак, вы говорите, что я могу думать об аргументах + как slots, которые могут быть заполнены чем угодно (неважно, насколько глубоко вложенные, если в какой-то момент они соответствуют начальным ограничениям). Итак, для (+) (+2) (+(+3))+3 должно соответствовать ограничениям и +(+3) тоже, и все?
В общих чертах, но похоже, что вы мыслите слишком сложными терминами. (+) принимает 2 аргумента одного типа, возвращает еще один аргумент того же типа, и этот тип может быть любым экземпляром Num. Когда вы предоставляете ему функцию, тип этой функции должен быть экземпляром Num, а частично примененная функция принимает функцию того же типа и возвращает другую функцию этого типа.
Но, например, если я скажу: (+) (2+) (+(+2)) по моим рассуждениям тип должен быть (+) (a->a) (a->(a->a)->a). Компилятор показывает (+) (a->a)->a->a
можете ли вы объяснить свои рассуждения, которые приходят к этому типу? (Это даже не допустимый тип, где-то опечатка?)
в любом случае, это довольно запутанный пример, основанный на том факте, что литерал 2 может обозначать Любые экземпляр Num. В вашем примере 2 в (2+) фактически рассматривается как функция. (По крайней мере, я так думаю. Как я уже сказал, это довольно запутанный пример, и почти наверняка ничего подобного не возникнет в реальном практическом коде.)
Первым аргументом (+) будет (2+), который является (a->a). Вторым аргументом будет (+) (+2), поэтому +2 будет (a->a) Следовательно, (+) (+2) будет (a->a)->a->a. Таким образом, большой + будет (+) (a->a) ((+) (a->a) a). Таким образом, применив ко второму аргументу +, мы получим a->(a->a)->a.
Ключевым моментом является то, что (+) требует, чтобы оба его аргумента были одного типа, и возвращает значение того же типа. Здесь аргумент (2+) имеет тип Num a => a -> a, а (+(+2)) — это (Num a, Num (a -> a)) => (a -> a) -> a -> a. Чтобы сделать их одного типа, Haskell принимает a -> a как a для (2+) (что возможно из-за ограничения Num (a -> a). Таким образом, оба аргумента, которые вы указали (+), имеют тип (a -> a) -> a -> a (с ограничениями, которые я устраняю для краткости) - таким образом, это также должно быть типом результата.
«Итак, вы говорите, что я могу думать об аргументах + как slots, которые могут быть заполнены чем угодно (неважно, насколько глубоко они вложены, если в какой-то момент они соответствуют начальным ограничениям)». Нет, аргумент в целом должен соответствовать ограничениям, вложенность не рассматривается. + принимает два аргумента любого типа, ограниченного классом типов Num. Причина, по которой функции передаются + проверкам типов, заключается в том, что Haskell не может узнать, собираетесь ли вы позже создавать экземпляр Num для функций (что вполне возможно, но не рекомендуется).
Вы понимаете, что
(+) (+)— это не композиция двух функций, но приложение?