Есть ли связь между знаками доллара ($) и id в Haskell?

На днях я читал комментарии к Monad Challenge (которую я очень рекомендую любому новичку в Haskell, как и я), и Я закончил на этот поток, где я читал, что ($) = id.

Я не знаю о пугающих людях, но многие языки программирования имеют концепции, которые лучше всего демонстрируются небольшими примерами, которые заставляют людей говорить "whoa".

     

Например, удивительно, что append() в Prolog можно запустить "назад" из конкатенированного результата, чтобы получить все списки, которые могут быть объединены для его создания. Или что оператор монадического связывания в Haskell ( → =) может быть определен в терминах join и fmap, или что ($) = id.

($) = id !?

< пробует это в Raskell/Ghci >

Теперь я вижу, почему это правда, но все же... Whoah!! Спасибо за это! (...)

Затем я проверил код base -4.10.0.0, ища определения ($) и id, но правильно наверху я читал это:

NOTA BENE: Do NOT use ($) anywhere in this module! The type of ($) is
slightly magical (it can return unlifted types), and it is wired in.
But, it is also *defined* in this module, with a non-magical type.
GHC gets terribly confused (and *hangs*) if you try to use ($) in this
module, because it has different types in different scenarios.

This is not a problem in general, because the type ($), being wired in, is not
written out to the interface file, so importing files don't get confused.
The problem is only if ($) is used here. So don't!

И их реализации:

-- | Identity function.
id                      :: a -> a
id x                    =  x

-- | Application operator.
{-# INLINE ($) #-}
($)                     :: (a -> b) -> a -> b
f $ x                   =  f x

Я попытался поменять местами один на другой на GHCi, и все, что у меня было, было ошибкой типа (как я и ожидал). Теперь у меня больше вопросов, чем с того, с чего я начал:

  • Что они означают, говоря, что ($) = id?
  • В каких случаях это утверждение истинно? Означает ли это, что я могу использовать один вместо другого?
  • В base подразумевается, что ($) является "слегка магическим (он может возвращать неперекрытые типы)" и "подключен к сети"?
  • А как насчет "разных типов в разных сценариях"? Я думал, что, поскольку Haskell является строго типизированным языком, как только вы определяете подпись типа, эта подпись сохраняется до конца Времени. Разве это не так? Существуют ли случаи, когда можно изменить тип функции?

Ответы

Ответ 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
  1. В каких случаях это утверждение истинно? Означает ли это, что я могу использовать один вместо другого?

Уравнение всегда имеет место, но есть два оговорки.

Первый заключается в том, что ($) имеет более ограниченный тип, чем тот, который имеет id, так как ($) f = f выполняется только для функции f, а не для любого значения f. Это означает, что вы можете заменить ($) на id, но не наоборот. При написании

($) = id

можно было бы сделать неявный аргумент типа явным и записать, что для любых типов a и b

($) @ a @ b = id @ (a->b)

Другой оговоркой является то, что при наличии функции с более высоким рангом ($) получает специальную обработку во время проверки типа, а id - нет. Это удар по пункту 3.

  1. В основе подразумевается, что ($) является "слегка магическим (он может возвращать неактивные типы)" и "подключен"?

Обычно в полиморфных функциях, таких как

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

Ответ 2

Хаскелл действительно сильно типизирован; проблема связана с некоторыми хакерами, специфичными для оператора ($). К сожалению, я понятия не имею, о чем это: надеюсь, кто-то ответит на ваш вопрос 3 (и вопрос 4 автоматически).

Что касается вопроса 1, посмотрите на типы:

id :: a -> a
($) :: (a -> b) -> (a -> b)

Переименуйте c = a -> b, и вы получите ($) :: c -> c, что означает, что тип ($) является спецификацией типа id, так что по крайней мере типы позволяют нам использовать id для реализации ($) > .

Теперь рассмотрим определение ($):

f $ x = f x

Немного перепишите его:

($) f = \x -> f x

И примените eta-reduction:

($) f = f

Теперь ясно видно, что ($) - это просто id с более конкретным типом (так что f всегда является функцией).

Обратите внимание, что это не работает по-другому, так как тип ($) более ограничительный. Например, вы можете вызвать id 5 и получить 5 в результате, но ($) 5 не будет проверяться typecheck: 5 не имеет типа формы a -> b.

Точка ($) заключается в том, что она имеет очень низкий приоритет и позволяет избежать фигурных скобок вокруг аргументов f и может использоваться как

someFunction $ whatever complex computation you dont need braces around