Почему встроенная функция применяется к слишком малому количеству аргументов, которые считаются нормальной формой слабой головы?

В определении Haskell говорится:

Выражение находится в слабой нормальной форме головы (WHNF), если оно:

  • конструктор (в конечном итоге применяемый к аргументам), например True, Just (квадрат 42) или (:) 1
  • встроенная функция, применяемая к слишком малому количеству аргументов (возможно, ни к одному), например (+) 2 или sqrt.
  • или лямбда-абстракция \ x -> выражение.

Почему встроенным функциям уделяется особое внимание? Согласно лямбда-исчислению, нет никакой разницы между частично применяемой функцией и любой другой функцией, потому что в конце у нас есть только один аргумент функции.


person Robert Zaremba    schedule 27.06.2014    source источник
comment
Я взял на себя смелость отредактировать эту вики-страницу Haskell, объяснив, почему это различие между встроенными функциями и лямбдами не имеет значения для семантики. (А также замечание о тонкости со строгими полями конструктора.)   -  person Ørjan Johansen    schedule 28.06.2014
comment
Кстати, эта вики-страница на самом деле не является частью официального определения Haskell. Однако в официальном отчете этот вопрос даже не упоминается, по-видимому, предпочитая предполагать, что читатели знакомы с денотационной семантикой ...   -  person Ørjan Johansen    schedule 28.06.2014


Ответы (2)


Обычная функция, применяемая к аргументу, например следующая:

(\x y -> x + 1 : y) 1

Может быть уменьшено, чтобы дать:

\y -> 1 + 1 : y

В первом выражении «самым внешним» было приложение, поэтому его не было в WHNF. Во втором случае самое внешнее - это лямбда-абстракция, поэтому она находится в WHNF (хотя мы могли бы сделать больше сокращений внутри тела функции).

Теперь рассмотрим применение встроенной (примитивной) функции:

(+) 1

Поскольку это встроенная функция, нет тела функции, в которой мы могли бы заменить первый параметр 1. Оценщик «просто знает», как оценивать полностью «насыщенные» приложения (+), например (+) 1 2. Но ничего нельзя сделать с помощью частично встроенного модуля; все, что мы можем сделать, это создать структуру данных, описывающую «применить (+) к 1 и дождаться еще одного аргумента», но это именно то, что мы пытаемся уменьшить . Итак, мы ничего не делаем.

Встроенные модули особенные, потому что они не определены выражениями лямбда-исчисления, поэтому процесс сокращения не может «заглянуть внутрь» их определения. Таким образом, в отличие от приложений с обычными функциями, приложения со встроенными функциями должны быть «сокращены» путем простого накопления аргументов до тех пор, пока они не станут полностью «насыщенными» (в этом случае сокращение до WHNF осуществляется за счет запуска какой-либо волшебной реализации встроенной функции) . Ненасыщенные встроенные приложения не могут быть сокращены дальше, и поэтому они уже находятся в WHNF.

person Ben    schedule 27.06.2014
comment
Фактически сокращает ли исполняющая система частично применяемые пользовательские функции? Конечно, мы ожидаем сокращения (\x->if x==1 then function1 else function2) k, но мне кажется, что для фактического уменьшения (\x y -> x + x*y + x^y) k потребуется особенное представление. - person dfeuer; 27.06.2014
comment
@dfeuer Поскольку GHC во время выполнения не представляет функции как лямбда-термины, это было бы действительно очень сложно. GHC обычно применяет функцию только после того, как у нее есть все ее аргументы. Он имеет специальное представление частично примененных функции для этого. - person Ørjan Johansen; 28.06.2014
comment
@dfeuer Хотя вы, конечно, можете написать систему времени выполнения, которая уменьшила бы частично применяемые функции, подобные этой. Я реализовал игрушечный ленивый функциональный язык путем простой редукции графа, который сделал это; это проще, потому что тогда вам не нужно знать или заботиться о том, когда функция имеет все свои аргументы; частичное приложение такое же, как окончательное (за исключением встроенных). Хитрость в том, что вы используете не текстовые термины, а графики. Поэтому, хотя мы бы записали сокращение вашего примера как \y -> k + k*y + k^y, в реальной системе каждый k на самом деле является ссылкой на один узел. - person Ben; 28.06.2014
comment
Бен, на самом деле я хочу сказать, что любое сокращение частично применяемой функции (т. Е. Ничего не делая, кроме подстановки значения аргумента) может, в лучшем случае, выполнить небольшой и постоянный объем работы. , и никогда не может произвести дно. В результате единственная причина, по которой я лично могу не рассматривать такое приложение как находящееся в WHNF, - это упростить описание того, что означает WHNF. - person dfeuer; 28.06.2014
comment
Что касается постоянных факторов, мне интересно, есть ли что-нибудь интересное, связанное с частичным применением встроенных модулей. Например, можно ли уменьшить (+ 5), создав машинный код, который использует команду немедленного выполнения, вместо того, чтобы загружать 5? - person dfeuer; 28.06.2014
comment
@dfeuer Ваша точка зрения близка к тому, что я пытался подразумевать, редактируя эту вики-страницу Haskell. Кроме того, идея немедленного выполнения инструкций кажется вполне возможной и, насколько мне известно, уже может быть реализована GHC. (Может быть, с помощью этого нового анализа арности и оптимизации LLVM?) - person Ørjan Johansen; 28.06.2014
comment
@ ØrjanJohansen, я полагаю, что такие вещи делаются, когда аргумент известен статически. Когда это не так, все становится намного сложнее. Я предполагаю, что могут возникнуть некоторые проблемы, которые заставят некоторые операционные системы даже разрешить такую ​​вещь, и тогда возникает вопрос, когда стоит беспокоиться. - person dfeuer; 28.06.2014
comment
@dfeuer Да ладно, если бы это не было JIT-компиляцией. Хотя все еще возможно. Я думаю, что GHC на самом деле имеет небольшой объем генерации кода времени выполнения всякий раз, когда вы используете FFI для экспорта замыканий (потому что указатели функций C для них не могут быть созданы на лету без него), но ничего подобного. - person Ørjan Johansen; 28.06.2014
comment
@dfeur Когда у вас есть только функции с одним аргументом, нет такой вещи, как частичное применение; там просто приложение. И функции первоклассные; когда вы видите приложение, которое должно вызывать функцию, вы по сути не знаете, является ли это частичным применением функции или полным применением функции, которая имеет тип результата функции; последнее может легко привести к низу или выполнить более интересный объем работы (которую можно разделить). Если все, что у вас есть, это структуры лямбда-исчисления, вы должны оценить их до WHNF, чтобы узнать. - person Ben; 29.06.2014

Учитывать

Прелюдия> пусть f n = [(+ x) | x ‹- [1 ..]] !! n
Prelude> let g = f 20000000 :: Int -> Int

g на данный момент не в WHNF! Вы можете увидеть это, оценив, скажем, g 3, что требует заметного запаздывания, потому что вам нужен WHNF, прежде чем вы сможете применить аргумент. Это когда список просматривается в поисках нужной встроенной функции. Но впоследствии этот выбор фиксируется, g - это WHNF (и действительно NF: то же самое для лямбда-выражений, возможно, то, что вы имели в виду в своем вопросе), и, таким образом, любые последующие вызовы немедленно дадут результат.

person leftaroundabout    schedule 27.06.2014
comment
почему после первого запуска g 3 a f 20000000 не запоминается? Я имею в виду, почему два последующих вызова g 3 занимают одинаковое заметное время? - person Robert Zaremba; 27.06.2014
comment
Что ж, они этого не делают. Он запомнен, по крайней мере, во всех установленных мной ghci (7.4, 7.6, 7.8). Поскольку MonomorphismRestriction по умолчанию выключен, для этого требуется явная мономорфная подпись, может, вы забыли об этом? (Полиморфная функция имеет один скрытый дополнительный аргумент.) - person leftaroundabout; 27.06.2014
comment
Начиная с GHC 7.8.1, ограничение мономорфизма отключено по умолчанию в GHCi. Есть ли способ включить его? Что вы имеете в виду под дополнительным аргументом для полиморфной функции? - person Robert Zaremba; 30.06.2014
comment
@RobertZaremba: вы можете включить его, установив флаг -XMonomorphismRestriction ... но не надо, это зло; вы всегда можете достичь той же цели с помощью явных подписей (например, Int -> Int в моем примере); это намного прозрачнее. - Полиморфизм Haskell - это параметрический полиморфизм, что означает, что если в сигнатуре функции есть какая-то переменная типа (здесь у нас есть f :: (Num a, Enum a) => Int -> a->a), тогда требуются свойства этого типа (то есть, как его можно использовать как число) для передачи функции в качестве аргумента словаря. Вот почему воспоминания не всегда работают должным образом. - person leftaroundabout; 30.06.2014