Ответ 1
- Что они означают, говоря, что
($) = id
?
Функция ($)
может быть определена как
($) f x = f x
то есть. взяв функцию и аргумент и вернув результат приложения. Эквивалентно, благодаря каррированию, мы можем интерпретировать это как взятие только f
и возврат функции x
.
($) f = \x -> f x
Здесь мы можем заметить, что функция \x -> f x
отображает любой вход x
в f x
- что также делает f
! Итак, мы можем упростить определение
($) f = f
Теперь это то же самое, что и определение функции идентификации id y = y
, поэтому мы могли бы написать
($) = id
- В каких случаях это утверждение истинно? Означает ли это, что я могу использовать один вместо другого?
Уравнение всегда имеет место, но есть два оговорки.
Первый заключается в том, что ($)
имеет более ограниченный тип, чем тот, который имеет id
, так как ($) f = f
выполняется только для функции f
, а не для любого значения f
. Это означает, что вы можете заменить ($)
на id
, но не наоборот. При написании
($) = id
можно было бы сделать неявный аргумент типа явным и записать, что для любых типов a
и b
($) @ a @ b = id @ (a->b)
Другой оговоркой является то, что при наличии функции с более высоким рангом ($)
получает специальную обработку во время проверки типа, а id
- нет.
Это удар по пункту 3.
- В основе подразумевается, что
($)
является "слегка магическим (он может возвращать неактивные типы)" и "подключен"?
Обычно в полиморфных функциях, таких как
id :: forall a . a -> a
переменная типа a
может быть создана для других типов, но только при некоторых ограничениях. Например, a
может быть создан в Int
, но не может быть создан другим полиморфным типом forall b . ...
. Это сохраняет предикатив системы типов, что очень помогает при выводе типа.
Обычно это не проблема в повседневном программировании. Однако некоторые функции используют типы ранга 2, например. runST
runST :: (forall s. ST s x) -> x
Если мы напишем
runST (some polymorphic value here)
система типа может ввести проверку. Но если мы используем общую идиому
runST $ some polymorphic value here
тогда механизм вывода типа должен принять тип ($)
($) :: forall a b . (a -> b) -> a -> b
и выберите a ~ forall s. ST s x
, который является полиморфным, поэтому запрещен.
Поскольку эта идиома слишком распространена, разработчики GHC решили добавить специальный случай для ввода ($)
, чтобы это разрешить. Поскольку это бит ad-hoc, если вы определяете свой собственный ($)
(возможно, под другим именем) и пытаетесь ввести check runST $ ...
, это не удастся, так как он не использует специальный случай.
Кроме того, a
не может быть создан для распакованных типов, таких как Int#
, или unboxed tuples (# Int#, Int# #)
. Это расширения GHC, которые позволяют записывать функции, которые передают "необработанное" целое число, без обычной оболочки. Это может изменить семантику, например. делая функции более строгими, чем они есть. Если вы не хотите сжимать больше производительности от некоторого числового кода, вы можете игнорировать их.
(Я оставляю эту часть здесь, но Лисйарус уже более точно ее осветил).
f $ x = f x
-- i.e.
($) f x = f x
-- i.e.
($) f x = id f x
-- eta contraction
($) f = id f
-- eta contraction
($) = id
В принципе, ($)
- id
, но с более ограничительным типом. id
может использоваться для аргументов любого типа, вместо этого ($)
принимает аргумент функции.
($) :: (a -> b) -> a -> b
-- is better read as
($) :: (a -> b) -> (a -> b)
-- which is of the form (c -> c) with c = (a -> b)
Обратите внимание, что id :: c -> c
, поэтому тип ($)
действительно является особым случаем.
А как насчет "разных типов в разных сценариях"? Я думал, что, поскольку Haskell является строго типизированным языком, как только вы определяете подпись типа, эта подпись сохраняется до конца Времени. Разве это не так? Существуют ли случаи, когда можно изменить тип функции?
В коде библиотек, которые определяют ($)
, GHC должен сыграть некоторые трюки, чтобы заставить специальные правила ввода работать для других библиотек и программ. Для этого, по-видимому, требуется не использовать ($)
в указанной библиотеке. Если вы не разрабатываете модуль GHC или base
, который определяет ($)
, вы можете игнорировать это. Это часть реализации, внутренняя для компилятора, а не то, что должен знать пользователь компилятора и библиотек.