Разница между $и()
Я начал изучать Haskell, и я столкнулся с проблемой, которую я не могу просто понять. У меня есть метод, используемый для поиска значения из списка списка значений ключа (от этой страницы):
let findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs
Я попытался немного поиграть и решил избавиться от знака $таким образом:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) ( xs )
Однако он даже не анализирует (фильтр применяется к слишком большому числу ошибок argumens). Я читал, что знак $используется, чтобы просто заменить скобки, и я не могу понять, почему это простое изменение кода плохое. Может кто-нибудь объяснить это мне?
Ответы
Ответ 1
Инфиксный оператор ($)
- это просто "приложение функции". Другими словами
f x -- and
f $ x
совпадают. Поскольку в круглых скобках Haskell используются только для устранения неоднозначности (и для обозначения кортежей и нескольких других второстепенных мест, см. Комментарии), мы также можем написать выше несколько других способов
f x
f $ x
(f) x
f (x)
(f) (x) -- and even
(f) $ (x)
В каждом случае указанные выше выражения означают одно и то же: "примените функцию f
к аргументу x
".
Итак, почему весь этот синтаксис? ($)
полезен по двум причинам.
- У него действительно низкий приоритет, поэтому иногда он может вмещать много скобок.
- Приятно иметь явное имя для действия приложения-приложения
В первом случае рассмотрим следующее глубоко-вложенное функциональное приложение
f (g (h (i (j x))))
Это может быть немного трудно прочитать, и немного сложно понять, что у вас есть правильное число круглых скобок. Тем не менее, это "просто" множество приложений, поэтому должно быть представление этой фразы с помощью ($)
. Действительно, существует
f $ g $ h $ i $ j $ x
Некоторые люди считают, что это легче читать. Более современный стиль также включает (.)
, чтобы подчеркнуть, что вся левая часть этой фразы - всего лишь сложный конвейер функций
f . g . h . i . j $ x
И эта фраза, как мы видели выше, идентична
(f . g . h . i . j) x
который иногда лучше читать.
Также есть моменты, когда мы хотим, чтобы мы могли передать идею приложения функции. Например, если у нас есть список функций
lof :: [Int -> Int]
lof = [ (+1), (subtract 1), (*2) ]
нам может понадобиться сопоставить приложение по значению над ними, например, применить число 4
к каждой функции
> map (\fun -> fun 4) lof
[ 5, 3, 8 ]
Но поскольку это просто приложение-функция, мы можем также использовать синтаксис раздела над ($)
, чтобы быть более явным
> map ($ 4) lof
[ 5, 3, 8 ]
Ответ 2
Оператор $
имеет самый низкий приоритет, поэтому
snd . head . filter (\(k,v) -> key == k) $ xs
читается как
(snd . head . filter (\(k,v) -> key == k)) xs
в то время как ваше второе выражение скорее
snd . head . ( filter (\(k,v) -> key == k) xs )
Ответ 3
Знак $
не является малым синтаксисом для замены скобок. Это обычный оператор инфикса, всякий раз, что оператор, подобный +
, есть.
Вставка скобок вокруг одного имени, такого как ( xs )
, всегда эквивалентна просто xs
1. Итак, если это то, что сделал $
, тогда вы получите ту же ошибку в любом случае.
Попытайтесь представить, что произойдет, если у вас есть другой оператор, с которым вы знакомы, например +
:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) + xs
Игнорируйте тот факт, что +
работает с числами, поэтому это не имеет смысла и просто задумайтесь о структуре выражения; какие термины распознаются как функции и какие термины передаются им в качестве аргументов.
Фактически, используя +
, действительно выполняется синтаксический анализ и проверка typecheck! (Он дает вам функцию с ограничениями типа бессмысленного типа, но если вы их выполняете, это что-то значит). Давайте рассмотрим, как решаются операторы infix:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) + xs
Наивысшим приоритетом всегда является обычное функциональное приложение (просто записывая термины рядом друг с другом, без задействованных операторов инфикса). Там только один пример, который здесь, filter
применяется к определению лямбда. Это становится "разрешенным" и становится единственным подвыражением, если рассматривать разбор остальных операторов:
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
in snd . head . fileterExp + xs
Следующим наивысшим приоритетом является оператор .
. У нас есть несколько, чтобы выбрать здесь, все с одинаковым приоритетом. .
является правильным ассоциативным, поэтому сначала мы берем самый правый первый (но он фактически не изменит результат, какой бы мы ни выбрали, поскольку значение .
является ассоциативной операцией, но синтаксический анализатор не имеет никакого способа зная, что):
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
in snd . dotExp1 + xs
Обратите внимание, что .
сразу же схватил слова слева и справа. Вот почему приоритет так важен. Остается еще .
слева, который по-прежнему имеет более высокий приоритет, чем +
, поэтому он идет следующим образом:
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
dotExp2 = snd . dotExp1
in dotExp2 + xs
И все готово! +
имеет самый низкий приоритет операторов здесь, поэтому он возвращает свои аргументы последним и в конечном итоге становится самым верхним вызовом во всем выражении. Обратите внимание, что низкий приоритет +
помешал xs
быть заявленным в качестве аргумента любым из приложений с более высоким приоритетом слева. И если у кого-то из них был более низкий приоритет, они бы взяли все выражение dotExp2 + xs
в качестве аргумента, так что они все равно не могли добраться до xs
; помещая оператор инфикса до xs
(любой оператор инфикса), не позволяет ему претендовать как аргумент чем-либо слева.
Это фактически точно так же, как $
анализируется в этом выражении, потому что .
и $
имеют тот же относительный приоритет, что .
и +
; $
имеет чрезвычайно низкий приоритет, поэтому он будет работать таким образом практически с любыми другими операторами, задействованными слева и справа.
Если мы не помещаем оператор инфикса между вызовом filter
и xs
, тогда это происходит:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) xs
Первое приложение для нормальной работы. Здесь у нас есть три слова просто рядом друг с другом: filter
, (\(k,v) -> key == k)
и xs
. Функция-функция оставлена ассоциативной, поэтому сначала берем левую пару:
let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
in snd . head . filterExp1 xs
Осталось еще одно нормальное приложение, которое по-прежнему имеет более высокий приоритет, чем .
, поэтому мы делаем это:
let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
filterExp2 = filterExp1 xs
in snd . head . filterExp2
Теперь первая точка:
let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
filterExp2 = filterExp1 xs
dotExp = head . filterExp2
in snd . dotExp
И мы закончили, самый верхний вызов во всем выражении на этот раз был самым левым .
. На этот раз xs
втянут в качестве второго аргумента filter
; это то, что мы хотим, так как filter
принимает два аргумента, но оставляет результат filter
в цепочке композиций функций, а filter
, примененный к двум аргументам, не может вернуть функцию. Мы хотели бы применить его к одному аргументу для предоставления функции, чтобы эта функция была частью цепочки составных функций, а затем применила эту целую функцию к xs
.
С $
там, окончательная форма отражает это, когда мы использовали +
:
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
dotExp2 = snd . dotExp1
in dotExp2 $ xs
Он анализировался точно так же, как когда мы имели +
, поэтому единственное различие заключается в том, что где +
означает "добавить мой левый аргумент в правый аргумент", $
означает "применить мой левый аргумент как функция для моего правого аргумента". Это то, что мы хотели! Ура!
TL;DR:Плохая новость заключается в том, что $
не работает, просто заключая в круглые скобки; это более сложно. Хорошей новостью является то, что если вы понимаете, как Haskell разрешает выражения, связанные с инфиксными операторами, то вы понимаете, как работает $
. Ничего особенного в этом вопросе нет. это обычный оператор, который вы могли бы определить сами, если его не было.
1 Parenthesising оператор типа (+)
также дает вам точно такую же функцию, обозначенную +
, но теперь он не имеет специального синтаксиса infix, поэтому это влияет на то, как вещи анализируются в этом случае. Не так с ( xs )
, где это просто имя внутри.
Ответ 4
"$
знак используется, чтобы просто заменить скобки" в значительной степени правильнее, однако он эффективно вставляет в скобки все обе стороны! Так
snd . head . filter (\(k,v) -> key == k) $ xs
эффективно
( snd . head . filter (\(k,v) -> key == k) ) ( xs )
Конечно, параны вокруг xs
здесь не нужны (что "атом" в любом случае), поэтому соответствующие в этом случае находятся вокруг левой стороны. Действительно, это часто происходит в Haskell, потому что общая философия состоит в том, чтобы как можно больше думать о функциях как абстрактных сущностях, а не о конкретных значениях, связанных с применением функции к некоторому аргументу. Ваше определение также может быть записано
let findKey key xs' = let filter (\(k,v) -> key == k) $ xs
x0 = head xs'
v0 = snd x0
in v0
Это было бы предельно явным, но все эти промежуточные значения не очень интересны. Поэтому мы предпочитаем просто объединять функции "без точек" с помощью .
. Это часто избавляет нас от множества шаблонов, ведь для вашего определения можно сделать следующее:
-
η-редукция xs
. Этот аргумент просто перешел к цепочке функций, поэтому мы могли бы также сказать: "findKey key
" - это цепочка, с любым аргументом, который вы поставляете "!
findKey key = snd . head . filter (\(k,v) -> key == k)
-
Далее мы можем избежать этой явной лямбда: \(k,v) -> k
- это просто функция fst
. Затем вам нужно выполнить посткомпозицию сравнения с key
.
findKey key = snd . head . filter ((key==) . fst)
-
Я бы остановился здесь, так как слишком много бессмысленных бессмысленных и нечитаемых. Но вы могли бы продолжить: там теперь новые аргументы вокруг аргумента key
, мы можем снова избавьтесь от тех, у кого $
. Но осторожно:
"findKey key = snd . head . filter $ (key==) . fst"
имеет значение не, так как $
снова вставляет в скобки обе стороны, но (snd . head . filter)
не является типичным. Фактически snd . head
должен появляться только после обоих аргументов до filter
. Возможный способ выполнения такой пост-пост-композиции - использовать функтор функции:
findKey key = fmap (snd . head) . filter $ (key==) . fst
... мы могли бы продолжить еще и избавиться от переменной key
, но это выглядело бы неплохо. Я думаю, у вас есть смысл...
Ответ 5
Другие ответы подробно прокомментировали, как ($)
может заменить скобки, поскольку ($)
был определен как оператор приложения с правильным приоритетом.
Я хотел бы добавить, что GHC, чтобы заменить скобки ($)
, использует еще манию под капотом, чем то, что видно из определения ($)
. Когда задействованы функции более высокого ранга, система типов может справляться с аргументами более высокого ранга при передаче через стандартное приложение (как в f x
), но не при передаче с помощью оператора приложения (как в f $ x
). Чтобы преодолеть эту проблему, GHC особым образом обрабатывает ($)
в системе типов. В самом деле, следующий код показывает, что если мы определяем и используем собственный оператор приложения ($$)
, система типов не применяет ту же магическую обработку.
{-# LANGUAGE RankNTypes #-}
-- A higher rank function
higherRank :: (forall a. a -> a -> a) -> (Int, Char)
higherRank f = (f 3 4, f 'a' 'b')
-- Standard application
test0 :: (Int, Char)
test0 = higherRank const -- evaluates to (3,'a')
-- Application via the ($) operator
test1 :: (Int, Char)
test1 = higherRank $ const -- again (3, 'a')
-- A redefinition of ($)
infixr 0 $$
($$) :: (a -> b) -> a -> b
($$) = ($)
test2 :: (Int, Char)
test2 = higherRank $$ const -- Type error (!)
-- Couldn't match expected type `forall a. a -> a -> a'
-- with actual type `a0 -> b0 -> a0'