Как рассуждать о частично примененной цепочке методов

Я пытаюсь понять, как рассуждать о типах для частичной цепочки примененных методов. Я не понимаю, почему :
:t (+)(+2) это (a->a)->a->a
или почему:
:t (+)(+) это (a->a->a)->a->a->a

я имею в виду для первого примера Я не понимаю, когда я смотрю на (+), мне нужно смотреть на то, что ему нужно a->a->a или какой метод перед ним (+2) (который нуждается в a).

Во втором примере я знаю, что первому (+) нужны a->a->a, но как только я заполнил первый метод, почему второму снова нужны те же параметры?

Вы понимаете, что (+) (+) — это не композиция двух функций, но приложение?

max taldykin 03.04.2019 11:58

Я это понимаю.

Bercovici Adrian 03.04.2019 13:23

Это не методы, это функции. Использование правильной терминологии, даже в вашей собственной голове, поможет развеять любые неверные представления, которые вы можете привнести в Haskell из других языков.

chepner 03.04.2019 18:21
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
72
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы на самом деле немного неправильно указали тип. Там есть несколько важных ограничений класса типов:

(+)(+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) тоже, и все?

Bercovici Adrian 03.04.2019 13:28

В общих чертах, но похоже, что вы мыслите слишком сложными терминами. (+) принимает 2 аргумента одного типа, возвращает еще один аргумент того же типа, и этот тип может быть любым экземпляром Num. Когда вы предоставляете ему функцию, тип этой функции должен быть экземпляром Num, а частично примененная функция принимает функцию того же типа и возвращает другую функцию этого типа.

Robin Zigmond 03.04.2019 13:36

Но, например, если я скажу: (+) (2+) (+(+2)) по моим рассуждениям тип должен быть (+) (a->a) (a->(a->a)->a). Компилятор показывает (+) (a->a)->a->a

Bercovici Adrian 03.04.2019 13:38

можете ли вы объяснить свои рассуждения, которые приходят к этому типу? (Это даже не допустимый тип, где-то опечатка?)

Robin Zigmond 03.04.2019 13:44

в любом случае, это довольно запутанный пример, основанный на том факте, что литерал 2 может обозначать Любые экземпляр Num. В вашем примере 2 в (2+) фактически рассматривается как функция. (По крайней мере, я так думаю. Как я уже сказал, это довольно запутанный пример, и почти наверняка ничего подобного не возникнет в реальном практическом коде.)

Robin Zigmond 03.04.2019 13:45

Первым аргументом (+) будет (2+), который является (a->a). Вторым аргументом будет (+) (+2), поэтому +2 будет (a->a) Следовательно, (+) (+2) будет (a->a)->a->a. Таким образом, большой + будет (+) (a->a) ((+) (a->a) a). Таким образом, применив ко второму аргументу +, мы получим a->(a->a)->a.

Bercovici Adrian 03.04.2019 15:16

Ключевым моментом является то, что (+) требует, чтобы оба его аргумента были одного типа, и возвращает значение того же типа. Здесь аргумент (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 (с ограничениями, которые я устраняю для краткости) - таким образом, это также должно быть типом результата.

Robin Zigmond 03.04.2019 15:25

«Итак, вы говорите, что я могу думать об аргументах + как slots, которые могут быть заполнены чем угодно (неважно, насколько глубоко они вложены, если в какой-то момент они соответствуют начальным ограничениям)». Нет, аргумент в целом должен соответствовать ограничениям, вложенность не рассматривается. + принимает два аргумента любого типа, ограниченного классом типов Num. Причина, по которой функции передаются + проверкам типов, заключается в том, что Haskell не может узнать, собираетесь ли вы позже создавать экземпляр Num для функций (что вполне возможно, но не рекомендуется).

DarthFennec 03.04.2019 21:53

Другие вопросы по теме