Есть ли причина, по которой функции в большинстве (?) Языков программирования поддерживают любое количество входных параметров, но только одно возвращаемое значение?
В большинстве языков можно обойти это ограничение, например, используя выходные параметры, возвращая указатели или определяя / возвращая структуры / классы. Но кажется странным, что языки программирования не были разработаны для поддержки множественных возвращаемых значений более «естественным» способом.
Есть ли объяснение этому?
def f(): return (1,2,3)
а затем вы можете использовать кортеж-распаковку «раскол» кортеж:a,b,c = f() #a=1,b=2,c=3
. Нет необходимости создавать массив и извлекать элементы вручную, не нужно определять какой-либо новый класс.[a, b] = f()
против[a, b, c] = f()
) , и полученный внутриf
путемnargout
. Я не большой поклонник Matlab, но иногда это бывает очень удобно.Ответы:
Некоторые языки, такие как Python , изначально поддерживают множественные возвращаемые значения, в то время как некоторые языки, такие как C #, поддерживают их через свои базовые библиотеки.
Но в целом, даже в языках, которые их поддерживают, множественные возвращаемые значения используются не часто, потому что они небрежные:
Легко ошибиться в порядке возврата значений
(По этой же причине многие люди избегают иметь слишком много параметров для функции; некоторые даже считают, что функция никогда не должна иметь два параметра одного типа!)
Они более строго типизированы, они возвращают возвращаемые значения сгруппированными как одна логическая единица, и они сохраняют имена (свойства) возвращаемых значений согласованными для всех применений.
Единственное место , они являются очень удобно в языках (как Python) , где несколько возвращаемых значений из одной функции могут быть использованы в качестве нескольких входных параметров к другому. Но случаи использования, когда это лучший дизайн, чем использование класса, довольно тонкие.
источник
()
. Это одна вещь или ноль вещей? Лично я бы сказал одно. Я могу назначитьx = ()
просто отлично, так же, как я могу назначитьx = randomTuple()
. В последнем случае, если возвращенный кортеж пуст или нет, я все еще могу назначить один возвращенный кортежx
.Потому что функции - это математические конструкции, которые выполняют вычисления и возвращают результат. Действительно, многое из того, что находится «под колпаком» не многих языков программирования, фокусируется исключительно на одном входе и одном выходе, при этом несколько входов являются просто тонкой оболочкой для ввода - и когда вывод одного значения не работает, используя один связная структура (или кортеж, или
Maybe
) будет выходом (хотя это «одиночное» возвращаемое значение состоит из множества значений).Это не изменилось, потому что программисты сочли
out
параметры неудобными конструкциями, которые полезны только в ограниченном наборе сценариев. Как и во многих других вещах, поддержки нет, потому что нет потребности / спроса.источник
В математике «четко определенная» функция - это функция, в которой для данного входа имеется только 1 выходной сигнал (в качестве примечания вы можете иметь только одну функцию ввода и при этом семантически получать несколько входов, используя каррирование ).
Для многозначных функций (например, квадрат корня положительного целого числа, например) достаточно вернуть коллекцию или последовательность значений.
Для типов функций, о которых вы говорите (т. Е. Функций, которые возвращают несколько значений разных типов ), я вижу это немного по-другому, чем кажется: я вижу необходимость / использование out-параметров в качестве обходного пути для улучшения дизайна или более полезная структура данных. Например, я бы предпочел, чтобы
*.TryParse(...)
методы возвращалиMaybe<T>
монаду вместо использования параметра out. Подумайте об этом коде в F #:Поддержка компилятора / IDE / анализа очень хороша для этих конструкций. Это решило бы большую часть «потребности» в наших параметрах. Честно говоря, я не могу думать о каких-либо других методах, где это не было бы решением.
Для других сценариев - тех, которые я не могу вспомнить - достаточно простого кортежа.
источник
var (value, success) = ParseInt("foo");
это будет тип проверки во время компиляции, потому что он(int, bool) ParseInt(string s) { }
был объявлен. Я знаю, что это можно сделать с помощью дженериков, но все же это будет хорошим дополнением к языку.В дополнение к тому, что уже было сказано, когда вы смотрите на парадигмы, используемые в сборке, когда функция возвращает функцию, она оставляет указатель на возвращаемый объект в определенном регистре. Если бы они использовали переменные / множественные регистры, вызывающая функция не знала бы, где получить возвращенные значения, если эта функция была в библиотеке. Так что это затруднит соединение с библиотеками, и вместо того, чтобы устанавливать произвольное количество возвращаемых указателей, они идут с одним. Языки более высокого уровня не имеют такого же оправдания.
источник
Во многих случаях использования, где вы бы использовали несколько возвращаемых значений в прошлом, просто больше нет необходимости с современными языковыми функциями. Хотите вернуть код ошибки? Бросить исключение или вернуть
Either<T, Throwable>
. Хотите вернуть необязательный результат? ВернутьOption<T>
. Хотите вернуть один из нескольких типов? ВозвратEither<T1, T2>
или помеченный союз.И даже в тех случаях, когда вам действительно необходимо вернуть несколько значений, современные языки обычно поддерживают кортежи или какую-либо структуру данных (список, массив, словарь) или объекты, а также некоторую форму деструктурирования привязки или сопоставления с шаблоном, что делает упаковку несколько значений в одно значение, а затем снова разложить его на три значения тривиально.
Вот несколько примеров языков, которые не поддерживают возврат нескольких значений. Я не очень понимаю, как добавление поддержки нескольких возвращаемых значений сделает их значительно более выразительными, чтобы компенсировать стоимость новой языковой функции.
Рубин
питон
Scala
Haskell
Perl6
источник
sort
функции Matlab :sorted = sort(array)
возвращает только отсортированный массив, а[sorted, indices] = sort(array)
возвращает оба. Единственный способ, которым я могу придумать в Python, - это передать флагsort
в соответствии сsort(array, nout=2)
илиsort(array, indices=True)
.[a, b, c] = func(some, thing)
), и действовать соответственно. Это полезно, например, если вычисление первого выходного аргумента дешево, а вычисление второго - дорого. Я не знаком ни с каким другим языком, где эквивалент Matlabsnargout
доступен во время выполнения.sorted, _ = sort(array)
.sort
функция может сказать, что ей не нужно вычислять индексы? Это круто, я этого не знал.Реальная причина того, что одно возвращаемое значение настолько популярно, - это выражения, которые используются во многих языках. На любом языке, где у вас может быть выражение, которое
x + 1
вы уже рассматриваете в терминах единичных возвращаемых значений, потому что вы оцениваете выражение в своей голове, разбивая его на части и определяя значение каждого элемента. Вы смотритеx
и решаете, что это значение равно 3 (например), и вы смотрите на 1, а затем вы смотрите наx + 1
и сложите все вместе, чтобы решить, что значение целого равно 4. Каждая синтаксическая часть выражения имеет одно значение, а не любое другое число значений; это естественная семантика выражений, которую все ожидают. Даже когда функция возвращает пару значений, она все равно действительно возвращает одно значение, которое выполняет работу с двумя значениями, потому что идея функции, которая возвращает два значения, которые каким-то образом не заключены в одну коллекцию, слишком странная.Люди не хотят иметь дело с альтернативной семантикой, которая требуется для того, чтобы функции возвращали более одного значения. Например, в основанном на стеке языке, таком как Forth, вы можете иметь любое количество возвращаемых значений, потому что каждая функция просто изменяет верхнюю часть стека, извлекая входные данные и выдвигая выходные данные по желанию. Вот почему у Форта нет таких выражений, как у нормальных языков.
Perl - это другой язык, который иногда может действовать так, как будто функции возвращают несколько значений, хотя обычно считается, что он возвращает список. Способ, которым списки «интерполируют» в Perl, дает нам списки, подобные
(1, foo(), 3)
которым могут иметь 3 элемента, как и ожидалось бы большинству людей, которые не знают Perl, но могут с легкостью иметь только 2 элемента, 4 элемента или любое большее количество элементов в зависимости отfoo()
, Списки в Perl сведены так, что синтаксический список не всегда имеет семантику списка; это может быть просто часть большого списка.Другим способом заставить функции возвращать несколько значений было бы иметь альтернативную семантику выражения, где любое выражение может иметь несколько значений, и каждое значение представляет возможность. Возьмем еще
x + 1
раз, но на этот раз представьте, что уx
него есть два значения {3, 4}, тогда значенияx + 1
будут {4, 5}, а значенияx + x
будут {6, 8} или, возможно, {6, 7, 8} в зависимости от того, разрешено ли в одной оценке использовать несколько значений дляx
. Подобный язык может быть реализован с использованием обратного отслеживания, аналогичного тому, который использует Prolog, чтобы дать несколько ответов на запрос.Короче говоря, вызов функции - это единственная синтаксическая единица, а единственная синтаксическая единица имеет одно значение в семантике выражения, которую мы все знаем и любим. Любая другая семантика вынудит вас к странным способам действий, таким как Perl, Prolog или Forth.
источник
Как предполагается в этом ответе , это вопрос аппаратной поддержки, хотя традиции в языковой разработке также играют роль.
Из трех первых языков, Fortran, Lisp и COBOL, первый использовал одно возвращаемое значение, которое было смоделировано по математике. Второе возвращало произвольное количество параметров так же, как оно получило их: в виде списка (можно также утверждать, что он только передал и возвратил единственный параметр: адрес списка). Третье возвращает ноль или одно значение.
Эти первые языки сильно повлияли на дизайн последующих языков, хотя единственный, который возвращал множество значений, Lisp, никогда не пользовался большой популярностью.
Когда появился C, находясь под влиянием предшествующих языков, он уделял большое внимание эффективному использованию аппаратного ресурса, сохраняя тесную связь между тем, что делал язык C, и машинным кодом, который его реализовывал. Некоторые из его самых старых функций, таких как переменные «auto» и «register», являются результатом этой философии проектирования.
Следует также отметить, что ассемблер был широко популярен до 80-х годов, когда он, наконец, начал постепенно вытесняться из основного развития. Люди, которые писали компиляторы и создавали языки, были знакомы с ассемблером и, по большей части, придерживались того, что там работало лучше всего.
Большинство языков, которые отклонялись от этой нормы, никогда не находили большой популярности и, следовательно, никогда не играли сильную роль, влияющую на решения языковых дизайнеров (которые, конечно, были вдохновлены тем, что они знали).
Итак, давайте рассмотрим ассемблер. Давайте сначала посмотрим на 6502 , микропроцессор 1975 года, который классно использовался в микрокомпьютерах Apple II и VIC-20. Он был очень слабым по сравнению с тем, что использовалось в мэйнфреймах и миникомпьютерах того времени, хотя и был мощным по сравнению с первыми компьютерами 20, 30 лет назад, на заре языков программирования.
Если вы посмотрите на техническое описание, у него есть 5 регистров плюс несколько однобитовых флагов. Единственным «полным» регистром был счетчик программ (ПК) - этот регистр указывает на следующую команду, которая должна быть выполнена. Другие регистры, где аккумулятор (A), два «индексных» регистра (X и Y) и указатель стека (SP).
Вызов подпрограммы помещает ПК в память, указанную SP, а затем уменьшает SP. Возврат из подпрограммы работает в обратном порядке. Можно помещать и извлекать другие значения из стека, но сложно обратиться к памяти относительно SP, поэтому написание подпрограмм с повторным входом было затруднено. То, что мы считаем само собой разумеющимся, вызывая подпрограмму в любое время, когда мы чувствуем, было не так распространено в этой архитектуре. Часто создается отдельный «стек», чтобы параметры и адрес возврата подпрограммы оставались раздельными.
Если вы посмотрите на процессор, который вдохновил 6502, 6800 , у него был дополнительный регистр, регистр индекса (IX), такой же широкий, как SP, который мог принимать значение от SP.
На компьютере вызов подпрограммы с повторным входом состоял из помещения параметров в стек, нажатия ПК, смены ПК на новый адрес, а затем подпрограмма помещала бы свои локальные переменные в стек . Поскольку число локальных переменных и параметров известно, их адресация может выполняться относительно стека. Например, функция, получающая два параметра и имеющая две локальные переменные, будет выглядеть так:
Его можно вызывать любое количество раз, потому что все временное пространство находится в стеке.
8080 , используемый на ТРС-80 и множество CP / M микрокомпьютеров на основе могли бы сделать что - то похожее на 6800, нажав SP на стек , а затем выскакивают его на косвенном регистре, HL.
Это очень распространенный способ реализации, и он получил еще большую поддержку на более современных процессорах, с базовым указателем, который упрощает вывод всех локальных переменных перед возвратом.
Проблема в том, как вы что-то возвращаете ? Регистры процессора были не очень многочисленными на раннем этапе, и часто приходилось использовать некоторые из них, даже чтобы выяснить, к какой части памяти обращаться. Возврат вещей в стеке будет сложным: вам нужно будет выгрузить все, сохранить ПК, передать возвращаемые параметры (которые будут храниться где-то еще?), Затем снова нажать ПК и вернуться.
Так что обычно делали резервирование одного регистра для возвращаемого значения. Вызывающий код знал, что возвращаемое значение будет в конкретном регистре, который должен быть сохранен до тех пор, пока не будет сохранен или использован.
Давайте посмотрим на язык, который допускает множественные возвращаемые значения: Forth. То, что делает Форт, - это хранение отдельного стека возврата (RP) и стека данных (SP), так что все, что нужно было сделать функции, - это вытолкнуть все ее параметры и оставить возвращаемые значения в стеке. Поскольку стек возвратов был отдельным, он не мешал.
Как человек, который выучил ассемблер и Форт за первые шесть месяцев работы с компьютерами, множественные возвращаемые значения выглядят для меня совершенно нормально. Такие операторы, как операторы Форта
/mod
, которые возвращают целочисленное деление, а остальные - кажутся очевидными. С другой стороны, я легко вижу, как кто-то, чей ранний опыт был С-умом, находит эту концепцию странной: она идет вразрез с их укоренившимися ожиданиями о том, что такое «функция».Что касается математики ... ну, я программировал компьютеры задолго до того, как начал заниматься функциями на уроках математики. Там есть целый раздел CS и языков программирования , который под влиянием математики, но, опять же , есть целый раздел , который не является.
Таким образом, у нас есть стечение факторов, где математика влияла на ранний языковой дизайн, где аппаратные ограничения диктовали то, что было легко реализовано, и где популярные языки влияли на то, как развивалось оборудование (машины Лиспа и процессоры Форта в этом процессе были путаницами).
источник
Функциональные языки, о которых я знаю, могут легко возвращать несколько значений с помощью кортежей (в динамически типизированных языках вы даже можете использовать списки). Кортежи также поддерживаются на других языках:
В приведенном выше примере
f
это функция, возвращающая 2 дюйма.Аналогично, ML, Haskell, F # и т. Д. Также могут возвращать структуры данных (указатели слишком низкоуровневы для большинства языков). Я не слышал о современном языке GP с таким ограничением:
Наконец,
out
параметры можно эмулировать даже в функциональных языкахIORef
. Существует несколько причин, по которым в большинстве языков отсутствует встроенная поддержка переменных:Непонятная семантика : печатает ли следующая функция 0 или 1? Я знаю языки, которые выводили бы 0, и те, которые выводили бы 1. У них обоих есть преимущества (как с точки зрения производительности, так и соответствия ментальной модели программиста):
Нелокализованные эффекты : Как и в примере выше, вы можете обнаружить , что вы можете иметь длинную цепь и сокровенные функции влияет на глобальное состояние. В целом, становится сложнее рассуждать о требованиях функции и о том, является ли изменение законным. Учитывая, что большинство современных парадигм пытаются либо локализовать эффекты (инкапсуляция в ООП), либо устранить побочные эффекты (функциональное программирование), это противоречит этим парадигмам.
Избыточность : если у вас есть кортежи, у вас есть 99% функциональности
out
параметров и 100% идиоматического использования. Если вы добавите указатели в микс, вы покроете оставшиеся 1%.У меня проблемы с именованием одного языка, который не может возвращать несколько значений с помощью кортежа, класса или
out
параметра (и в большинстве случаев допускается 2 или более таких методов).источник
Я думаю, что это из-за выражений , таких как
(a + b[i]) * c
.Выражения состоят из «единичных» значений. Таким образом, функция, возвращающая единственное значение, может непосредственно использоваться в выражении вместо любой из четырех переменных, показанных выше. Функция с несколькими выходами по крайней мере несколько неуклюжа в выражении.
Я лично считаю , что это вещь , что особенного особой возвращаемого значения. Вы можете обойти эту проблему, добавив синтаксис для указания того, какое из нескольких возвращаемых значений вы хотите использовать в выражении, но оно должно быть более неуклюжим, чем старые добрые математические обозначения, которые являются краткими и знакомыми для всех.
источник
Это немного усложняет синтаксис, но на уровне реализации нет веской причины не допускать этого. В отличие от некоторых других ответов, возвращение нескольких значений, если они доступны, приводит к более ясному и эффективному коду. Я не могу сосчитать, как часто мне хотелось бы возвращать X и Y или логическое значение «успеха» и полезное значение.
источник
[out]
параметр, но практически все возвращаютHRESULT
(код ошибки). Было бы довольно практично просто найти там пару. В языках с хорошей поддержкой кортежей, таких как Python, это используется во многих кодах, которые я видел.sort
функция обычно сортирует массив:sorted_array = sort(array)
. Иногда мне нужны соответствующие индексы:[sorted_array, indices] = sort(array)
. Иногда я хочу, чтобы только индексы:[~, indices]
= sort (array). The function
sort` фактически указывал, сколько выходных аргументов необходимо, поэтому, если требуется дополнительная работа для 2 выходов по сравнению с 1, он может вычислять эти выходные данные только при необходимости.В большинстве языков, где поддерживаются функции, вы можете использовать вызов функции везде, где может использоваться переменная этого типа:
Если функция возвращает более одного значения, это не будет работать. Динамически типизированные языки, такие как python, позволят вам сделать это, но в большинстве случаев они вызовут ошибку времени выполнения, если не удастся найти что-то разумное для кортежа в середине уравнения.
источник
foo()[0]
Я просто хочу основываться на ответе Харви. Первоначально я нашел этот вопрос на новостном техническом сайте (arstechnica) и нашел удивительное объяснение, которое, как мне кажется, действительно отвечает на суть этого вопроса и отсутствует во всех других ответах (кроме ответа Харви):
Источник одиночного возврата из функций лежит в машинном коде. На уровне машинного кода функция может возвращать значение в регистр A (аккумулятор). Любые другие возвращаемые значения будут в стеке.
Язык, который поддерживает два возвращаемых значения, скомпилирует его как машинный код, который возвращает одно, и помещает второе в стек. Другими словами, второе возвращаемое значение в конечном итоге будет выходным параметром.
Это все равно что спросить, почему присваивание - это одна переменная за раз. Вы можете иметь язык, который допускает, например, a, b = 1, 2. Но в конечном итоге на уровне машинного кода будет a = 1, за которым следует b = 2.
Есть смысл в том, чтобы конструкции языка программирования имели некоторое сходство с тем, что действительно произойдет, когда код скомпилирован и запущен.
источник
Это началось с математики. FORTRAN, названный по имени «Перевод формулы», был первым компилятором. FORTRAN был и ориентирован на физику / математику / инженерию.
COBOL, почти такой же старый, не имел явного возвращаемого значения; У него едва были подпрограммы. С тех пор это было в основном инерцией.
Go , например, имеет несколько возвращаемых значений, и результат получается чище и менее неоднозначен, чем при использовании параметров out. После небольшого использования, это очень естественно и эффективно. Я рекомендую учитывать несколько возвращаемых значений для всех новых языков. Может быть, для старых языков тоже.
источник
Вероятно, это больше связано с наследием того, как выполняются вызовы функций в машинных инструкциях процессора, и с тем фактом, что все языки программирования являются производными от машинного кода: например, C -> Assembly -> Machine.
Как процессоры выполняют вызовы функций
Первые программы были написаны в машинном коде, а затем в сборке. Процессоры поддерживали вызовы функций, помещая копию всех текущих регистров в стек. Возвращение из функции приведет к извлечению сохраненного набора регистров из стека. Обычно один регистр оставляют нетронутым, чтобы возвращающая функция возвращала значение.
Теперь о том, почему процессоры были спроектированы таким образом ... это, вероятно, вопрос ограниченности ресурсов.
источник