Подтвердить что ты не робот

Разница между $и()

Я начал изучать 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). Я читал, что знак $используется, чтобы просто заменить скобки, и я не могу понять, почему это простое изменение кода плохое. Может кто-нибудь объяснить это мне?

4b9b3361

Ответ 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'