stringtranslate.com

Первоклассная функция

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

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

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

Концепции

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

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

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

карта :: ( a -> b ) -> [ a ] ​​-> [ b ] карта f [] = [] карта f ( x : xs ) = f x : карта f xs                     

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

void map ( int ( * f ) ( int ), int x [], size_t n ) { for ( int i = 0 ; i < n ; i ++ ) x [ i ] = f ( x [ i ]); }                   

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

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

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

основная = карта ( \ x -> 3 * x + 1 ) [ 1 , 2 , 3 , 4 , 5 ]              

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

int f ( int x ) { return 3 * x + 1 ; }         int main () { int list [] = { 1 , 2 , 3 , 4 , 5 }; map ( f , list , 5 ); }             

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

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

main = пусть a = 3 b = 1 в карте ( \ x -> a * x + b ) [ 1 , 2 , 3 , 4 , 5 ]                      

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

typedef struct { int ( * f )( int , int , int ); int a ; int b ; } closure_t ;           void map ( closure_t * closure , int x [], size_t n ) { for ( int i = 0 ; i < n ; ++ i ) x [ i ] = ( closure -> f )( closure -> a , closure -> b , x [ i ]); }                     int f ( int a , int b , int x ) { return a * x + b ; }             void main () { int l [] = { 1 , 2 , 3 , 4 , 5 }; int a = 3 ; int b = 1 ; closure_t closure = { f , a , b }; map ( & closure , l , 5 ); }                           

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

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

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

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

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

f :: [[ Целое число ] -> [ Целое число ]] f = пусть a = 3 b = 1 в [ карта ( \ x -> a * x + b ), карта ( \ x -> b * x + a )]                             

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

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

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

Теория типов

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

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

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

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

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

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

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

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

С++
Замыкания C++11 могут захватывать нелокальные переменные с помощью конструкции копирования, ссылки (без продления их срока службы) или конструкции перемещения (переменная существует столько же, сколько и замыкание). Первый вариант безопасен, если замыкание возвращается, но требует копирования и не может использоваться для изменения исходной переменной (которая может уже не существовать на момент вызова замыкания). Второй вариант потенциально позволяет избежать дорогостоящего копирования и позволяет изменять исходную переменную, но небезопасен в случае возврата замыкания (см. висячие ссылки ). Третий вариант безопасен, если замыкание возвращается и не требует копирования, но также не может использоваться для изменения исходной переменной.
Ява
Замыкания Java 8 могут захватывать только final или "эффективно final" нелокальные переменные. Типы функций Java представлены как классы. Анонимные функции принимают тип, выведенный из контекста. Ссылки на методы ограничены. Для получения более подробной информации см. Анонимная функция § Ограничения Java .
Лисп
Варианты Lisp с лексической областью видимости поддерживают замыкания. Варианты с динамической областью видимости не поддерживают замыкания или нуждаются в специальной конструкции для создания замыканий. [22]
В Common Lisp идентификатор функции в пространстве имен функций не может использоваться как ссылка на значение первого класса. functionДля извлечения функции как значения необходимо использовать специальный оператор: (function foo)вычисляется как объект функции. #'fooсуществует как сокращенная запись. Чтобы применить такой объект функции, необходимо использовать функцию funcall: (funcall #'foo bar baz).
Питон
Явное частичное применение с functools.partialверсии 2.5 и operator.methodcallerс версии 2.6.
Рубин
Идентификатор обычной "функции" в Ruby (которая на самом деле является методом) не может использоваться как значение или передаваться. Сначала его нужно извлечь в объект Methodили Procдля использования в качестве данных первого класса. Синтаксис вызова такого объекта функции отличается от вызова обычных методов.
Определения вложенных методов на самом деле не вкладывают область действия.
Явное каррирование с помощью [1].

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

Примечания

  1. ^ Абельсон, Гарольд ; Сассман, Джеральд Джей (1984). Структура и интерпретация компьютерных программ. MIT Press. Формулирование абстракций с помощью процедур высшего порядка. ISBN 0-262-01077-1. Архивировано из оригинала 2021-09-21 . Получено 2021-09-27 .
  2. ^ Прагматика языка программирования, Майкл Ли Скотт, раздел 11.2 «Функциональное программирование».
  3. ^ Роберто Иерусалимский ; Луис Энрике де Фигейредо; Вальдемар Селес (2005). «Реализация Lua 5.0». Журнал универсальной информатики . 11 (7): 1159–1176. дои : 10.3217/jucs-011-07-1159 .
  4. ^ ab Burstall, Rod; Strachey, Christopher (2000). "Understanding Programming Languages" (PDF) . Higher-Order and Symbolic Computation . 13 (52): 11–49. doi :10.1023/A:1010052305354. S2CID  1989590. Архивировано из оригинала 16 февраля 2010 г.{{cite journal}}: CS1 maint: бот: исходный статус URL неизвестен ( ссылка )(также 2010-02-16
  5. ^ Джоэл Мозес . «Функция FUNCTION в LISP, или почему проблема FUNARG должна называться проблемой окружения». MIT AI Memo 199, 1970.
  6. ^ «Если вы попытаетесь вызвать вложенную функцию через ее адрес после того, как содержащая ее функция завершилась, начнется настоящий ад». (GNU Compiler Collection: Nested Functions)
  7. ^ Эндрю В. Аппель (1995). «Интенсиональное равенство ;=) для продолжений».
  8. ^ Таненбаум, А.С. (1977). "Сравнение PASCAL и Algol 68". The Computer Journal . 21 (4): 319. doi : 10.1093/comjnl/21.4.316 .
  9. ^ «История Python: Истоки «функциональных» возможностей Python». 21 апреля 2009 г.
  10. ^ Вложенные функции с использованием лямбда-выражений/замыканий
  11. ^ ab Doc No. 1968: V Samko; J Willcock, J Järvi, D Gregor, A Lumsdaine (26 февраля 2006 г.) Лямбда-выражения и замыкания для C++
  12. ^ "Mac Dev Center: Blocks Programming Topics: Introduction". Архивировано из оригинала 2009-08-31.
  13. ^ «2 примера на Go, которые можно применить частично».
  14. ^ "partial_application". Docs.rs . Получено 2020-11-03 .
  15. ^ «SRFI 26: Нотация для специализации параметров без каррирования».
  16. ^ «Джон Резиг — Частичное применение в JavaScript».
  17. ^ Кац, Ян (23 июля 2010 г.). "Lua Code for Curry (Currying Functions)". Архивировано из оригинала 2018-11-06.
  18. ^ "Блог | Perlgeek.de :: Каррирование".
  19. ^ «Что нового в Python 2.5 — Документация Python 3.10.0».
  20. ^ «Анонимные функции — MATLAB и Simulink — MathWorks United Kingdom».
  21. ^ Оценка частичной функции в MATLAB
  22. ^ Замыкания в ZetaLisp Архивировано 19.03.2012 на Wayback Machine

Ссылки

Внешние ссылки