В языке программирования стратегия оценки — это набор правил для оценки выражений. [1] Этот термин часто используется для обозначения более конкретного понятия стратегии передачи параметров [2] , которая определяет вид значения, передаваемого функции для каждого параметра ( стратегия связывания ) [3] и следует ли оценивать параметры вызова функции, и если да, то в каком порядке ( порядок оценки ). [4] Понятие стратегии сокращения является отдельным, [5] хотя некоторые авторы объединяют эти два термина, и определение каждого термина не является общепринятым. [6]
Для иллюстрации, выполнение вызова функции f(a,b)
может сначала оценить аргументы a
и b
, сохранить результаты в ссылках или ячейках памяти ref_a
и ref_b
, а затем оценить тело функции с этими переданными ссылками. Это дает функции возможность искать исходные значения аргументов, переданные через разыменование параметров (некоторые языки используют специальные операторы для этого), изменять их через присваивание , как если бы они были локальными переменными, и возвращать значения через ссылки. Это стратегия оценки вызова по ссылке. [7]
Стратегия оценки является частью семантики определения языка программирования. Некоторые языки, такие как PureScript , имеют варианты с различными стратегиями оценки. Некоторые декларативные языки , такие как Datalog , поддерживают несколько стратегий оценки. Некоторые языки определяют соглашение о вызовах . [ необходимо разъяснение ]
Это таблица стратегий оценки и репрезентативных языков по году введения. Репрезентативные языки перечислены в хронологическом порядке, начиная с языка(ов), которые ввели стратегию, а затем следуют известные языки, которые используют стратегию. [8] : 434
В то время как порядок операций определяет абстрактное синтаксическое дерево выражения, порядок оценки определяет порядок, в котором выражения оцениваются. Например, программа Python
def f ( x ): print ( x , end = '' ) return xраспечатать ( f ( 1 ) + f ( 2 ), конец = '' )
выводит данные 123
из-за порядка вычисления слева направо в Python, но похожая программа на OCaml :
пусть f x = print_int x ; x ;; print_int ( f 1 + f 2 )
выходные данные 213
из-за порядка оценки OCaml справа налево.
Порядок оценки в основном виден в коде с побочными эффектами , но он также влияет на производительность кода, поскольку жесткий порядок препятствует планированию инструкций . По этой причине стандарты языка, такие как C++, традиционно оставляют порядок неопределенным, хотя такие языки, как Java и C#, определяют порядок оценки как слева направо [8] : 240–241 , а стандарт C++17 добавил ограничения на порядок оценки. [21]
Аппликативный порядок — это семейство порядков оценки, в которых аргументы функции полностью оцениваются до того, как функция будет применена. [22] Это приводит к тому, что функция становится строгой , т. е. результат функции не определен, если какой-либо из аргументов не определен, поэтому аппликативный порядок оценки чаще называют строгой оценкой . Кроме того, вызов функции выполняется, как только он встречается в процедуре, поэтому его также называют жадной оценкой или жадной оценкой . [23] [24] Некоторые авторы называют строгую оценку «вызовом по значению» из-за стратегии связывания вызова по значению, требующей строгой оценки. [4]
Common Lisp, Eiffel и Java оценивают аргументы функций слева направо. C оставляет порядок неопределенным. [25] Scheme требует, чтобы порядок выполнения был последовательным выполнением неопределенной перестановки аргументов. [26] OCaml аналогичным образом оставляет порядок неопределенным, но на практике оценивает аргументы справа налево из-за конструкции своей абстрактной машины . [27] Все они являются строгими вычислениями.
Нестрогий порядок оценки — это порядок оценки, который не является строгим, то есть функция может вернуть результат до того, как все ее аргументы будут полностью оценены. [28] : 46–47 Прототипическим примером является нормальный порядок оценки , который не оценивает ни один из аргументов, пока они не понадобятся в теле функции. [29] Нормальный порядок оценки имеет свойство завершаться без ошибок всякий раз, когда любой другой порядок оценки завершился бы без ошибок. [30] Название «нормальный порядок» происходит от лямбда-исчисления, где редукция нормального порядка найдет нормальную форму, если таковая имеется (это «нормализирующая» стратегия редукции ). [31] Ленивая оценка классифицируется в этой статье как метод связывания, а не порядок оценки. Но это различие не всегда соблюдается, и некоторые авторы определяют ленивую оценку как оценку нормального порядка или наоборот, [22] [32] или путают нестрогость с ленивой оценкой. [28] : 43–44
Булевы выражения во многих языках используют форму нестрогой оценки, называемую короткой оценкой , где оценка оценивает левое выражение, но может пропустить правое выражение, если результат может быть определен, например, в дизъюнктивном выражении (ИЛИ), где true
встречается, или в конъюнктивном выражении (И), где false
встречается, и т. д. [32] Условные выражения аналогичным образом используют нестрогую оценку — оценивается только одна из ветвей. [28]
При оценке нормального порядка выражения, содержащие дорогостоящие вычисления, ошибки или бесконечные циклы, будут игнорироваться, если они не нужны, [4] позволяя специфицировать определяемые пользователем конструкции потока управления, что недоступно при оценке аппликационного порядка. Оценка нормального порядка использует сложные структуры, такие как переходники для неоцененных выражений, по сравнению со стеком вызовов , используемым при оценке аппликационного порядка. [33] Оценка нормального порядка исторически имела недостаток пригодных для использования инструментов отладки из-за своей сложности. [34]
При вызове по значению (или передаче по значению) вычисленное значение выражения аргумента привязывается к соответствующей переменной в функции (часто путем копирования значения в новую область памяти). Если функция или процедура может присваивать значения своим параметрам, присваивается только ее локальная переменная — то есть все, что передается в вызов функции, остается неизменным в области действия вызывающей стороны , когда функция возвращается. Например, в Pascal передача массива по значению приведет к копированию всего массива, и любые изменения в этом массиве будут невидимы для вызывающей стороны: [35]
программа Main ; использует crt ; procedure PrintArray ( a : Массив целых чисел ) ; var i : Целое число ; begin for i := Low ( a ) to High ( a ) do Write ( a [ i ]) ; WriteLn () ; end ; Процедура Modify ( Строка : Массив целых чисел ) ; начало PrintArray ( Строка ) ; // 123 Строка [ 1 ] := 4 ; PrintArray ( Строка ) ; // 143 конец ; Var A : Массив целых чисел ; begin A := [ 1 , 2 , 3 ] ; PrintArray ( A ) ; // 123 Modify ( A ) ; PrintArray ( A ) ; // 123 end .
Строго говоря, при вызове по значению никакие операции, выполняемые вызываемой подпрограммой, не могут быть видны вызывающему, кроме как как часть возвращаемого значения. [16] Это подразумевает форму чисто функционального программирования в семантике реализации. Однако иносказание «вызов по значению, где значение является ссылкой» стало обычным в некоторых языках, например, в сообществе Java. [36] По сравнению с традиционной передачей по значению, передаваемое значение не является значением, как оно понимается в обычном значении значения, например, целым числом, которое может быть записано как литерал, а внутренним дескриптором ссылки реализации . Мутации этого дескриптора ссылки видны в вызывающем. Из-за видимой мутации эта форма «вызова по значению» более правильно называется вызовом путем разделения. [16]
В чисто функциональных языках значения и структуры данных неизменяемы, поэтому функция не может изменять какие-либо свои аргументы. Таким образом, обычно нет семантической разницы между передачей по значению и передачей по ссылке или указателю на структуру данных, и реализации часто используют вызов по ссылке внутри для повышения эффективности. Тем не менее, эти языки обычно описываются как языки вызова по значению.
Вызов по ссылке (или передача по ссылке) — это стратегия оценки, при которой параметр привязан к неявной ссылке на переменную, используемую в качестве аргумента, а не к копии ее значения. Обычно это означает, что функция может изменять (т. е. присваивать ) переменную, используемую в качестве аргумента, — то, что будет видно ее вызывающему. Поэтому вызов по ссылке может использоваться для предоставления дополнительного канала связи между вызываемой функцией и вызывающей функцией. Передача по ссылке может значительно повысить производительность: вызов функции со структурой размером в несколько мегабайт в качестве аргумента не должен копировать большую структуру, только ссылку на структуру (которая обычно является машинным словом и всего несколькими байтами). Однако язык вызова по ссылке затрудняет для программиста отслеживание эффектов вызова функции и может привести к появлению тонких ошибок.
Из-за различий в синтаксисе разница между вызовом по ссылке (где тип ссылки неявный) и вызовом по разделению (где тип ссылки явный) часто неясна на первый взгляд. Простая лакмусовая бумажка — возможно ли написать традиционную swap(a, b)
функцию на этом языке. [36] Например, в Fortran:
программа Main неявный none целое :: a = 1 целое :: b = 2 вызов Swap ( a , b ) печать * , a , b !2 1 содержит подпрограмму Swap ( a , b ) целое , намерение ( inout ) :: a , b целое :: temp temp = a a = b b = temp конец подпрограммы Swap конец программы Main
Таким образом, намерение Fortran inout
реализует вызов по ссылке; любая переменная может быть неявно преобразована в ссылочный дескриптор. В отличие от этого, самое близкое, что можно получить в Java, это:
class Main { static class Box { int value ; public Box ( int value ) { this.value = value ; } } static void swap ( Box a , Box b ) { int temp = a .value ; a .value = b .value ; b .value = temp ; } public static void main ( String [] args ) { Box a = new Box ( 1 ) ; Box b = new Box ( 2 ) ; swap ( a , b ) ; System .out .println ( String .format ( " % d %d " , a .value , b .value ) ) ; } } // вывод : 2 1
где для введения дескриптора должен использоваться явный Box
тип. Java — это вызов по разделению, но не вызов по ссылке. [36]
Вызов копированием-восстановлением — также известный как «копирование-ввод-вывод», «вызов по значению результата», «вызов по значению возврата» (как это называется в сообществе Fortran ) — это разновидность вызова по ссылке. При вызове копированием-восстановлением содержимое аргумента копируется в новую переменную, локальную для вызова вызова. Затем функция может изменить эту переменную, аналогично вызову по ссылке, но поскольку переменная является локальной, изменения не видны за пределами вызова вызова во время вызова. Когда вызов функции возвращается, обновленное содержимое этой переменной копируется обратно, чтобы перезаписать исходный аргумент («восстановлено»). [37]
Семантика вызова по копированию-восстановлению во многих случаях похожа на вызов по ссылке, но отличается, когда два или более аргументов функции являются псевдонимами друг друга (т. е. указывают на одну и ту же переменную в среде вызывающей стороны). При вызове по ссылке запись в один аргумент повлияет на другой во время выполнения функции. При вызове по копированию-восстановлению запись в один аргумент не повлияет на другой во время выполнения функции, но в конце вызова значения двух аргументов могут различаться, и неясно, какой аргумент копируется обратно первым и, следовательно, какое значение получает переменная вызывающей стороны. [38] Например, Ada указывает, что назначение копирования для каждого параметра in out
или out
происходит в произвольном порядке. [39] Из следующей программы (незаконной в Ada 2012) [40] можно увидеть, что поведение GNAT заключается в копировании слева направо при возврате:
с Ada.Text_IO ; использовать Ada.Text_IO ;procedure Test_Copy_Restore is procedure Modify ( A , B : in out Integer ) is begin A := A + 1 ; B := B + 2 ; end Modify ; X : Integer := 0 ; begin Modify ( X , X ); Put_Line ( "X = " & Integer ' Image ( X )); end Test_Copy_Restore ; -- $ gnatmake -gnatd.E test_copy_restore.adb; ./test_copy_restore -- test_copy_restore.adb:12:10: предупреждение: доступный для записи фактический для "A" перекрывается с фактическим для "B" [-gnatw.i] -- X = 2
Если бы программа вернула 1, это было бы копирование справа налево, а при вызове по семантике ссылки программа вернула бы 3.
Когда ссылка передается вызывающей стороне неинициализированной (например, out
параметр в Ada, а не in out
параметр), такую стратегию оценки можно назвать «вызовом по результату».
Эта стратегия привлекла внимание в многопроцессорной обработке и удаленных вызовах процедур [41], поскольку в отличие от вызова по ссылке она не требует частого взаимодействия между потоками выполнения для доступа к переменным.
Вызов по разделению (также известный как «передача по разделению», «вызов по объекту» или «вызов по разделению объекта») — это стратегия оценки, которая является промежуточной между вызовом по значению и вызовом по ссылке. Вместо того, чтобы каждая переменная была представлена как ссылка, только определенный класс значений, называемых «ссылками», « упакованными типами » или «объектами», имеет семантику ссылок, и именно адреса этих указателей передаются в функцию. Как и в случае вызова по значению, значение переданного адреса является копией, а прямое назначение параметру функции перезаписывает копию и не видно вызывающей функции. Как и в случае вызова по ссылке, изменение цели указателя видно вызывающей функции. Мутации изменяемого объекта внутри функции видны вызывающей функции, потому что объект не копируется и не клонируется — он является общим , отсюда и название «вызов по разделению». [16]
Впервые этот метод был описан Барбарой Лисков в 1974 году для языка CLU . [16] Он используется во многих современных языках, таких как Python (общие значения называются «объектами»), [42] Java (объекты), Ruby (объекты), JavaScript (объекты), Scheme (структуры данных, такие как векторы), [43] AppleScript (списки, записи, даты и объекты скриптов), OCaml и ML (ссылки, записи, массивы, объекты и другие составные типы данных), Maple (rtables и таблицы) и Tcl (объекты). [44] Термин «вызов путем совместного использования», используемый в этой статье, не является общепринятым; терминология противоречива в разных источниках. Например, в сообществе Java говорят, что Java — это вызов по значению. [36]
Для неизменяемых объектов нет никакой реальной разницы между вызовом по разделению и вызовом по значению, за исключением случаев, когда идентичность объекта видна в языке. Использование вызова по разделению с изменяемыми объектами является альтернативой параметрам ввода/вывода : параметр не назначается (аргумент не перезаписывается и идентичность объекта не изменяется), но объект (аргумент) мутирует. [45]
Например, в Python списки изменяемы и передаются с помощью вызова путем совместного использования, поэтому:
def f ( a_list ) : a_list .append ( 1 )м = [] ф ( м ) печать ( м )
выводит, [1]
потому что append
метод изменяет объект, для которого он вызван.
Напротив, назначения внутри функции не заметны для вызывающей стороны. Например, этот код привязывает формальный аргумент к новому объекту, но он не виден для вызывающей стороны, поскольку не мутирует a_list
:
def f ( a_list ): a_list = a_list + [ 1 ] print ( a_list ) # [1]м = [] ф ( м ) печать ( м ) # []
Вызов по адресу , передача по адресу или вызов/передача по указателю — это метод передачи параметров, в котором адрес аргумента передается как формальный параметр. Внутри функции адрес (указатель) может использоваться для доступа к значению аргумента или его изменения. Например, операция обмена может быть реализована следующим образом в C: [46]
#include <stdio.h> void swap ( int * a , int * b ) { int temp = * a ; * a = * b ; * b = temp ; } int main () { int a = 1 ; int b = 2 ; swap ( &a a , & b ); printf ( "%d %d" , a , b ); // 2 1 return 0 ; }
Некоторые авторы рассматривают &
как часть синтаксиса вызова swap
. Согласно этой точке зрения, C поддерживает стратегию передачи параметров по ссылке. [47] Другие авторы придерживаются иной точки зрения, что представленная реализация swap
в C является лишь имитацией вызова по ссылке с использованием указателей. [48] Согласно этой «симуляционной» точке зрения, изменяемые переменные в C не являются первоклассными (то есть l-значения не являются выражениями), а скорее являются типами указателей. Согласно этой точке зрения, представленная программа подкачки является синтаксическим сахаром для программы, которая использует указатели повсюду, [49] например, эта программа ( read
и assign
были добавлены, чтобы подчеркнуть сходство с Box
программой вызова по разделению Java выше):
#include <stdio.h> int read ( int * p ) { return * p ; } void присваивать ( int * p , int v ) { * p = v ; } void swap ( int * a , int * b ) { int temp_storage ; int * temp = & temp_storage ; назначить ( temp , read ( a )); назначить ( a , read ( b )); назначить ( b , read ( temp )); } int main () { int a_storage ; int * a = & a_storage ; int b_storage ; int * b = & b_storage ; назначить ( a , 1 ); назначить ( b , 2 ); поменять местами ( a , b ); printf ( "%d %d" , прочитать ( a ), прочитать ( b )); // 2 1 вернуть 0 ; }
Поскольку в этой программе swap
используются указатели, и она не может изменять сами указатели, а только значения, на которые указывают указатели, эта точка зрения предполагает, что основная стратегия оценки языка C больше похожа на вызов с разделением.
C++ еще больше запутывает проблему, позволяя swap
объявлять и использовать очень легкий «ссылочный» синтаксис: [50]
void swap ( int & a , int & b ) { int temp = a ; a = b ; b = temp ; } int main () { int a = 1 ; int b = 2 ; swap ( a , b ); std :: cout << a << b << std :: endl ; // 2 1 return 0 ; }
Семантически это эквивалентно примерам на языке C. Поэтому многие авторы считают вызов по адресу уникальной стратегией передачи параметров, отличной от вызова по значению, вызова по ссылке и вызова по совместному использованию.
В логическом программировании оценка выражения может просто соответствовать объединению задействованных терминов в сочетании с применением некоторой формы разрешения . Объединение должно быть классифицировано как стратегия строгого связывания, поскольку оно выполняется полностью. Однако объединение может также выполняться на неограниченных переменных, поэтому вызовы не обязательно могут фиксировать конечные значения для всех его переменных.
Вызов по имени — это стратегия оценки, при которой аргументы функции не оцениваются до вызова функции, а подставляются непосредственно в тело функции (используя подстановку, избегающую захвата ), а затем остаются для оценки всякий раз, когда они появляются в функции. Если аргумент не используется в теле функции, он никогда не оценивается; если он используется несколько раз, он переоценивается каждый раз, когда появляется. (См. устройство Дженсена для техники программирования, которая использует это.)
Оценка вызова по имени иногда предпочтительнее оценки вызова по значению. Если аргумент функции не используется в функции, вызов по имени сэкономит время, не оценивая аргумент, тогда как вызов по значению оценит его независимо. Если аргумент является не завершающимся вычислением, преимущество огромно. Однако, когда используется аргумент функции, вызов по имени часто медленнее, требуя механизма, такого как thunk .
Языки .NET могут имитировать вызов по имени с использованием делегатов или Expression<T>
параметров. Последнее приводит к абстрактному синтаксическому дереву , заданному для функции. Eiffel предоставляет агенты, которые представляют операцию, которая должна быть оценена при необходимости. Seed7 предоставляет вызов по имени с параметрами функции. Программы Java могут выполнять аналогичную ленивую оценку с использованием лямбда-выражений и java.util.function.Supplier<T>
интерфейса.
Вызов по потребности — это мемоизированный вариант вызова по имени, где, если аргумент функции вычисляется, это значение сохраняется для последующего использования. Если аргумент чистый (т.е. не имеет побочных эффектов), это дает те же результаты, что и вызов по имени, экономя затраты на пересчет аргумента.
Haskell — известный язык, использующий оценку по вызову. Поскольку оценка выражений может происходить произвольно далеко в вычислении, Haskell поддерживает только побочные эффекты (такие как мутация ) с помощью монад . Это исключает любое неожиданное поведение переменных, значения которых изменяются до их отложенной оценки.
В реализации вызова по необходимости в R передаются все аргументы, что означает, что R допускает произвольные побочные эффекты.
Ленивая оценка — наиболее распространенная реализация семантики вызова по потребности, но существуют и такие вариации, как оптимистическая оценка. Языки .NET реализуют вызов по потребности с помощью типа Lazy<T>
.
Редукция графа — это эффективная реализация ленивых вычислений.
Вызов по расширению макроса похож на вызов по имени, но использует текстовую подстановку вместо подстановки, избегающей захвата . Таким образом, макроподстановка может привести к захвату переменной, что приводит к ошибкам и нежелательному поведению. Гигиенические макросы избегают этой проблемы, проверяя и заменяя затененные переменные , которые не являются параметрами.
"Call by future", также известный как "parallel call by name" или "lenient evaluation", [51] - это стратегия параллельной оценки, сочетающая нестрогую семантику с нетерпеливой оценкой. Метод требует мелкозернистого динамического планирования и синхронизации, но подходит для машин с массовым параллелизмом.
Стратегия создает будущее (обещание) для тела функции и каждого из ее аргументов. Эти будущие события вычисляются одновременно с потоком остальной части программы. Когда будущее A требует значения другого будущего B, которое еще не было вычислено, будущее A блокируется до тех пор, пока будущее B не завершит вычисления и не получит значение. Если будущее B уже завершило вычисления, значение возвращается немедленно. Условные выражения блокируются до тех пор, пока их условие не будет оценено, а лямбды не создают будущие события, пока они не будут полностью применены. [52]
Если реализовано с процессами или потоками, создание будущего создаст один или несколько новых процессов или потоков (для обещаний), доступ к значению синхронизирует их с основным потоком, а завершение вычисления будущего соответствует остановке обещаний, вычисляющих его значение. Если реализовано с помощью сопрограммы , как в .NET async/await , создание будущего вызывает сопрограмму (асинхронную функцию), которая может уступить вызывающему и, в свою очередь, быть уступлена обратно, когда значение используется, кооперативно выполняя многозадачность.
Стратегия недетерминирована, так как оценка может произойти в любой момент между созданием будущего (т. е. когда выражение задано) и использованием значения будущего. Стратегия нестрогая, так как тело функции может возвращать значение до оценки аргументов. Однако в большинстве реализаций выполнение все равно может застрять при оценке ненужного аргумента. Например, программа
f x = 1 / x g y = 1 main = print ( g ( f 0 ))
может либо g
закончиться до f
, и вывести 1, либо может привести к ошибке из-за оценки 1/0
. [28]
Call-by-future похож на call-by-need в том, что значения вычисляются только один раз. При тщательной обработке ошибок и незавершенности, в частности, при завершении futures на полпути, если определено, что они не понадобятся, call-by-future также имеет те же свойства завершения, что и оценка call-by-need. [52] Однако call-by-future может выполнять ненужную спекулятивную работу по сравнению с call-by-need, например, глубокую оценку ленивой структуры данных. [28] Этого можно избежать, используя ленивые futures, которые не начинают вычисление, пока не будет уверенности, что значение необходимо.
Оптимистическая оценка — это вариант вызова по необходимости, где аргумент функции частично оценивается в стиле вызова по значению в течение некоторого времени (которое может быть скорректировано во время выполнения ). По истечении этого времени оценка прерывается, и функция применяется с использованием вызова по необходимости. [53] Такой подход позволяет избежать некоторых затрат времени выполнения вызова по необходимости, сохраняя при этом желаемые характеристики завершения.
Вероятно, это был первый язык, который систематически использовал возможности ленивых вычислений.
{{cite book}}
: |work=
проигнорировано ( помощь )