Первоклассная функция - First-class function

В Информатика, а язык программирования говорят, что имеет первоклассные функции если это лечит функции в качестве первоклассные граждане. Это означает, что язык поддерживает передачу функций в качестве аргументов другим функциям, возвращая их как значения из других функций и присваивая их переменным или сохраняя их в структурах данных.[1] Некоторым теоретикам языков программирования требуется поддержка анонимные функции (функциональные литералы).[2] В языках с первоклассными функциями имена функций не имеют особого статуса; с ними обращаются как с обычными переменные с тип функции.[3] Термин был придуман Кристофер Стрейчи в контексте «функций граждан первого класса» в середине 1960-х гг.[4]

Первоклассные функции необходимы для функциональное программирование стиль, в котором использование функции высшего порядка это стандартная практика. Простым примером функции высшего порядка является карта функция, которая принимает в качестве аргументов функцию и список и возвращает список, сформированный путем применения функции к каждому члену списка. Для языка для поддержки карта, он должен поддерживать передачу функции в качестве аргумента.

Существуют определенные сложности реализации при передаче функций в качестве аргументов или их возвращении в качестве результатов, особенно при наличии нелокальные переменные введено в вложенный и анонимные функции. Исторически их называли Funarg проблемы, имя происходит от «аргумент функции».[5] В ранних императивных языках этих проблем можно было избежать за счет отказа от функций в качестве типов результатов (например, АЛГОЛ 60, Паскаль ) или исключение вложенных функций и, следовательно, нелокальных переменных (например, C ). Ранний функциональный язык Лисп взял подход динамическое определение, где нелокальные переменные относятся к ближайшему определению этой переменной в точке, где функция выполняется, а не там, где она была определена. Надлежащая поддержка лексически ограниченный первоклассные функции были введены в Схема и требует обработки ссылок на функции как закрытие вместо голого указатели на функции,[4] что, в свою очередь, делает вывоз мусора необходимость.

Концепции

В этом разделе мы сравниваем, как конкретные идиомы программирования обрабатываются на функциональном языке с функциями первого класса (Haskell ) по сравнению с императивным языком, где функции являются гражданами второго сорта (C ).

Функции высшего порядка: передача функций в качестве аргументов

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

карта :: (а -> б) -> [а] -> [б]карта ж []     = []карта ж (Икс:хз) = ж Икс : карта ж хз

Языки, на которых функции не являются первоклассными, часто по-прежнему позволяют писать функции более высокого порядка с использованием таких функций, как указатели на функции или же делегаты. На языке C:

пустота карта(int (*ж)(int), int Икс[], size_t п) {    за (int я = 0; я < п; я++)        Икс[я] = ж(Икс[я]);}

Есть ряд различий между двумя подходами, которые нет напрямую связано с поддержкой первоклассных функций. Образец Haskell работает на списки, а образец C оперирует массивы. Обе являются наиболее естественными составными структурами данных на соответствующих языках, и использование примера C для работы со связанными списками сделало бы его излишне сложным. Это также учитывает тот факт, что функции C требуется дополнительный параметр (задающий размер массива). Функция C обновляет массив. на месте, не возвращая значения, тогда как в Haskell структуры данных настойчивый (возвращается новый список, а старый остается нетронутым). В примере Haskell используется рекурсия для обхода списка, в то время как в примере C используется итерация. Опять же, это наиболее естественный способ выразить эту функцию на обоих языках, но образец Haskell можно легко выразить в терминах складывать и образец C с точки зрения рекурсии. Наконец, функция Haskell имеет полиморфный type, поскольку это не поддерживается C, мы зафиксировали все переменные типа в константе типа int.

Анонимные и вложенные функции

В языках, поддерживающих анонимные функции, мы можем передать такую ​​функцию в качестве аргумента функции высшего порядка:

главный = карта (\Икс -> 3 * Икс + 1) [1, 2, 3, 4, 5]

На языке, который не поддерживает анонимные функции, мы должны вместо этого привязать его к имени:

int ж(int Икс) {    возвращаться 3 * Икс + 1;}int главный() {    int список[] = {1, 2, 3, 4, 5};    карта(ж, список, 5);}

Нелокальные переменные и замыкания

Когда у нас есть анонимные или вложенные функции, для них становится естественным ссылаться на переменные вне их тела (называемые нелокальные переменные):

главный = позволять а = 3           б = 1        в карта (\Икс -> а * Икс + б) [1, 2, 3, 4, 5]

Если функции представлены голыми указателями на функции, мы больше не можем знать, как значение, находящееся за пределами тела функции, должно быть передано в нее, и из-за этого закрытие необходимо создавать вручную. Поэтому о «первоклассных» функциях здесь говорить не приходится.

typedef структура {    int (*ж)(int, int, int);    int *а;    int *б;} closure_t;пустота карта(closure_t *закрытие, int Икс[], size_t п) {    за (int я = 0; я < п; ++я)        Икс[я] = (*закрытие->ж)(*закрытие->а, *закрытие->б, Икс[я]);}int ж(int а, int б, int Икс) {    возвращаться а * Икс + б;}пустота главный() {    int л[] = {1, 2, 3, 4, 5};    int а = 3;    int б = 1;    closure_t закрытие = {ж, &а, &б};    карта(&закрытие, л, 5);}

Также обратите внимание, что карта теперь специализируется на функциях, относящихся к двум ints вне своего окружения. Это можно настроить в более общем плане, но требуется больше шаблонный код. Если ж был бы вложенная функция мы все равно столкнемся с той же проблемой, и именно по этой причине они не поддерживаются в C.[6]

Функции высшего порядка: возвращение функций как результатов

Возвращая функцию, мы фактически возвращаем ее закрытие. В примере C любые локальные переменные, захваченные замыканием, выйдут за пределы области видимости, как только мы вернемся из функции, которая создает замыкание. Принудительное закрытие в более поздний момент приведет к неопределенному поведению, возможно, к повреждению стека. Это известно как восходящая задача Funarg.

Назначение функций переменным

Назначение функции для переменные и их хранение внутри (глобальных) структур данных потенциально сталкивается с теми же трудностями, что и возвращаемые функции.

ж :: [[Целое число] -> [Целое число]]ж = позволять а = 3        б = 1     в [карта (\Икс -> а * Икс + б), карта (\Икс -> б * Икс + а)]

Равенство функций

Поскольку можно проверить на равенство большинство литералов и значений, естественно спросить, может ли язык программирования поддерживать функции проверки на равенство. При дальнейшем рассмотрении этот вопрос кажется более сложным, и необходимо различать несколько типов равенства функций:[7]

Экстенсивное равенство
Две функции ж и грамм считаются экстенсивно равными, если они согласовывают свои выходы для всех входов (∀Икс. ж(Икс) = грамм(Икс)). В соответствии с этим определением равенства, например, любые две реализации стабильный алгоритм сортировки, Такие как вставка сортировки и Сортировка слиянием, будет считаться равным. Решение об экстенсиональном равенстве неразрешимый вообще и даже для функций с конечной областью часто трудноразрешимой. По этой причине ни один язык программирования не реализует равенство функций как экстенсиональное равенство.
Внутреннее равенство
При содержательном равенстве две функции ж и грамм считаются равными, если они имеют одинаковую «внутреннюю структуру». Такое равенство может быть реализовано в интерпретируемые языки сравнивая исходный код тел функций (например, в Interpreted Lisp 1.5) или объектный код в компилированные языки. Интенсиональное равенство подразумевает экстенсиональное равенство (при условии, что функции детерминированы и не имеют скрытых входных данных, таких как счетчик команд или изменчивый глобальная переменная.)
Ссылочное равенство
Учитывая непрактичность реализации экстенсионального и интенсионального равенства, большинство языков, поддерживающих функции тестирования на равенство, используют ссылочное равенство. Всем функциям или замыканиям присваивается уникальный идентификатор (обычно это адрес тела функции или замыкания), и равенство определяется на основе равенства идентификаторов. Два отдельно определенных, но в остальном идентичных определения функций будут считаться неравными. Ссылочное равенство подразумевает интенсиональное и экстенсиональное равенство. Нарушение ссылочного равенства ссылочная прозрачность и поэтому не поддерживается в чистый языки, такие как Haskell.

Теория типов

В теория типов, тип функций, принимающих значения типа А и возвращающие значения типа B можно записать как АB или же BА. в Переписка Карри – Ховарда, типы функций связаны с логическое следствие; лямбда-абстракция соответствует выполнению гипотетических предположений, а применение функции соответствует modus ponens правило вывода. Помимо обычного случая программирования функций, теория типов также использует функции первого класса для моделирования ассоциативные массивы и подобные структуры данных.

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

Языковая поддержка

Функциональные языки программирования, такие как Схема, ML, Haskell, F #, и Scala, все имеют первоклассные функции. Когда Лисп, один из первых функциональных языков, не все аспекты первоклассных функций были тогда правильно поняты, в результате чего функции были динамически ограничены. Позже Схема и Common Lisp диалекты имеют лексически ограниченные функции первого класса.

Многие языки сценариев, включая Perl, Python, PHP, Lua, Tcl / Tk, JavaScript и Ио, имеют первоклассные функции.

Для императивных языков необходимо проводить различие между Algol и его потомками, такими как Pascal, традиционным семейством C и современными вариантами со сборкой мусора. В семействе Algol разрешены вложенные функции и функции, принимающие функции более высокого порядка в качестве аргументов, но не функции более высокого порядка, которые возвращают функции в качестве результатов (за исключением Algol 68, который позволяет это). Причина этого заключалась в том, что не было известно, как поступать с нелокальными переменными, если в результате была возвращена вложенная функция (и в таких случаях Algol 68 выдает ошибки времени выполнения).

Семейство C позволяло передавать функции как аргументы и возвращать их как результаты, но избегало каких-либо проблем, не поддерживая вложенные функции. (Компилятор gcc допускает их в качестве расширения.) Поскольку полезность возвращаемых функций в первую очередь заключается в возможности возвращать вложенные функции, которые захватили нелокальные переменные, а не функции верхнего уровня, эти языки обычно не считаются имеющими первыми -классовые функции.

Современные императивные языки часто поддерживают сборку мусора, что делает возможной реализацию первоклассных функций. Первоклассные функции часто поддерживаются только в более поздних версиях языка, включая C # 2.0 и расширение Apple Blocks для C, C ++ и Objective-C. В C ++ 11 добавлена ​​поддержка анонимных функций и замыканий в язык, но из-за того, что язык не является сборщиком мусора, необходимо соблюдать особую осторожность, чтобы нелокальные переменные в функциях возвращались в качестве результатов (см. Ниже ).

ЯзыкФункции высшего порядкаВложенные функцииНелокальные переменныеПримечания
АргументыПолученные результатыНазванныйАнонимныйЗакрытиеЧастичное применение
Семья АлголАЛГОЛ 60даНетдаНетВнизНетИметь типы функций.
АЛГОЛ 68дада[8]дадаВниз[9]Нет
ПаскальдаНетдаНетВнизНет
АдадаНетдаНетВнизНет
ОберондаТолько не вложенныедаНетВнизНет
Delphiдадада20092009Нет
Семья CCдадаНетНетНетНетИмеет указатели на функции.
C ++дадаC ++ 11[10]C ++ 11[11]C ++ 11[11]C ++ 11Имеет указатели на функции, функциональные объекты. (Также см. Ниже.)

Явное частичное нанесение возможно с std :: bind.

C #дада72.0 / 3.02.03.0Имеет делегаты (2.0) и лямбда-выражения (3.0).
Цель-CдадаИспользование анонимного2.0 + блоки[12]2.0 + блокиНетИмеет указатели на функции.
ЯваЧастичноеЧастичноеИспользование анонимногоJava 8Java 8НетИмеет анонимные внутренние классы.
ИдтидадаИспользование анонимногодадада[13]
ЛимбодададададаНет
NewsqueakдададададаНет
Ржавчинадададададада[14]
Функциональные языкиЛиспСинтаксисСинтаксисдадаCommon LispНет(Смотри ниже)
СхемадададададаSRFI 26[15]
Юлядададададада
Clojureдададададада
MLдададададада
Haskellдададададада
Scalaдададададада
F #дададададада
OCamlдададададада
Языки сценариевИодададададаНет
JavaScriptдададададаECMAScript 5Возможно частичное приложение с кодом пользователя на ES3 [16]
Luaдададададада[17]
PHPдадаИспользование анонимного5.35.3НетВозможно частичное применение с кодом земли пользователя.
Perlдада6дада6[18]
PythonдададаТолько выраженияда2.5[19](Смотри ниже)
РубинСинтаксисСинтаксисБез области действиядада1.9(Смотри ниже)
Другие языкиФортрандададаНетНетНет
КлендададададаНет
MathematicaдададададаНет
MATLABдададада[20]дадаВозможно частичное применение за счет автоматического создания новых функций.[21]
БолтовнядададададаЧастичноеВозможно частичное применение через библиотеку.
Быстрыйдададададада
C ++
C ++ 11 замыкания могут захватывать нелокальные переменные по ссылке (без увеличения срока их службы), путем копирования или переместить строительство (переменная живет, пока живет замыкание). Первый потенциально позволяет избежать дорогостоящей копии и позволяет изменять исходную переменную, но небезопасен в случае возврата закрытия (см. висячие ссылки ). Второй вариант безопасен, если замыкание возвращается, но требует копии и не может использоваться для изменения исходной переменной (которая может больше не существовать во время вызова замыкания). Последнее безопасно, если возвращается замыкание и не требует копии, но также не может использоваться для изменения исходной переменной.
Ява
Java 8 замыкания могут захватывать только final или «фактически final» нелокальные переменные. Java типы функций представлены как классы. Анонимные функции принимают тип, выведенный из контекста. Ссылки на методы ограничены. Подробнее см. Анонимная функция § Ограничения Java.
Лисп
Лексически ограниченный Варианты Lisp поддерживают замыкания. С динамической областью видимости варианты не поддерживают замыкания или нуждаются в специальной конструкции для создания замыканий.[22]
В Common Lisp, идентификатор функции в пространстве имен функции не может использоваться в качестве ссылки на значение первого класса. Специальный оператор функция должен использоваться для получения функции как значения: (функция foo) оценивается как объект функции. # 'фу существует как сокращенное обозначение. Чтобы применить такой функциональный объект, необходимо использовать веселье функция: (funcall # 'foo bar baz).
Python
Явное частичное нанесение с functools.partial начиная с версии 2.5, и оператор.methodcaller начиная с версии 2.6.
Рубин
Идентификатор обычной «функции» в Ruby (который на самом деле является методом) нельзя использовать в качестве значения или передавать. Сначала его нужно извлечь в Метод или же Proc объект, который будет использоваться как первоклассные данные. Синтаксис вызова такого функционального объекта отличается от вызова обычных методов.
Определения вложенных методов на самом деле не вкладывают в область видимости.
Явное каррирование с [1].

Смотрите также

Примечания

  1. ^ Абельсон, Гарольд; Сассман, Джеральд Джей (1984). Структура и интерпретация компьютерных программ. MIT Press. Формулирование абстракций с помощью процедур более высокого порядка. ISBN  0-262-01077-1.
  2. ^ Прагматика языка программирования, Майкл Ли Скотт, раздел 11.2 «Функциональное программирование».
  3. ^ Роберто Иерусалимши; Луис Энрике де Фигейредо; Вальдемар Селес (2005). «Реализация Lua 5.0». Журнал универсальных компьютерных наук. 11 (7): 1159–1176. Дои:10.3217 / jucs-011-07-1159.
  4. ^ а б Берстолл, Род; Стрейчи, Кристофер (2000). «Понимание языков программирования» (PDF). Вычисление высшего порядка и символическое вычисление. 13 (52): 11–49. Дои:10.1023 / А: 1010052305354. Архивировано 16 февраля 2010 года.CS1 maint: BOT: статус исходного URL-адреса неизвестен (связь) (также 16 февраля 2010 г.
  5. ^ Джоэл Моисей. «Функция FUNCTION в LISP, или почему проблема FUNARG должна называться проблемой среды». MIT AI Memo 199, 1970.
  6. ^ «Если вы попытаетесь вызвать вложенную функцию через ее адрес после того, как содержащая функция завершится, все вырвется наружу». (Коллекция компиляторов GNU: вложенные функции )
  7. ^ Эндрю В. Аппель (1995). "Внутреннее равенство; =) для продолжений".
  8. ^ Таненбаум, А. (1977). «Сравнение PASCAL и Algol 68» (PDF). Компьютерный журнал. 21 (4): 319. Дои:10.1093 / comjnl / 21.4.316.
  9. ^ http://python-history.blogspot.nl/2009/04/origins-of-pythons-functional-features.html?showComment=1243166621952#c702829329923892023
  10. ^ Вложенные функции с использованием лямбда-выражений / замыканий
  11. ^ а б Док № 1968: В Самко; Дж. Уиллкок, Дж. Ярви, Д. Грегор, А. Ламсдайн (26 февраля 2006 г.) Лямбда-выражения и замыкания для C ++
  12. ^ https://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html
  13. ^ «2 примера на Go, которые можно частично применить».
  14. ^ "частичное_приложение". Docs.rs. Получено 2020-11-03.
  15. ^ http://srfi.schemers.org/srfi-26/srfi-26.html
  16. ^ http://ejohn.org/blog/partial-functions-in-javascript/
  17. ^ Кац, Ян (23 июля 2010 г.). «Код Lua для карри (функции каррирования)». Архивировано из оригинал на 2018-11-06.
  18. ^ http://perlgeek.de/blog-en/perl-5-to-6/28-currying.html
  19. ^ https://docs.python.org/whatsnew/2.5.html#pep-309-partial-function-application
  20. ^ http://www.mathworks.co.uk/help/matlab/matlab_prog/anonymous-functions.html
  21. ^ Оценка частичной функции в MATLAB
  22. ^ Закрытие в ZetaLisp В архиве 2012-03-19 в Wayback Machine

Рекомендации

внешняя ссылка