Почему большинство языков программирования поддерживают только возврат одного значения из функции? [закрыто]

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

В большинстве языков можно «обойти» это ограничение, например. используя out-parameters, возвращающие указатели или определяя /возвращая structs /classes. Но странно, что языки программирования не были предназначены для поддержки нескольких возвращаемых значений более «естественным» способом.

Есть ли объяснение для этого?

116 голосов | спросил M4N 3 J000000Wednesday13 2013, 00:54:08

14 ответов


57

Некоторые языки, как Python , поддерживают множественный возврат значения, а некоторые языки как C # поддерживают их через их базовые библиотеки.

Но в целом даже в языках, которые их поддерживают, несколько возвращаемых значений часто не используются, потому что они неаккуратные:

  • Функции, возвращающие несколько значений, трудно четко обозначить .
  • Легко принять порядок возвращаемых значений

    (password, username) = GetUsernameAndPassword()  
    

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

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

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

ответил BlueRaja - Danny Pflughoeft 3 J000000Wednesday13 2013, 02:50:31
54

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

Это не изменилось, потому что программисты обнаружили, что параметры out являются неудобными конструкциями, которые полезны только в ограниченном наборе сценариев. Как и во многих других вещах, поддержки там нет, потому что потребности /спроса там нет.

ответил Telastyn 3 J000000Wednesday13 2013, 01:01:06
24

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

ответил David 3 J000000Wednesday13 2013, 01:53:14
18

Множество вариантов использования, в которых вы использовали бы несколько возвращаемых значений в прошлом, просто больше не нужны с современными языковыми функциями. Хотите вернуть код ошибки? Выбросьте исключение или верните Either<T, Throwable>. Хотите вернуть дополнительный результат? Верните Option<T>. Хотите вернуть один из нескольких типов? Верните Either<T1, T2>, либо помеченный союз.

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

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

рубин

 def foo; return 1, 2, 3 end

one, two, three = foo

one
# => 1

three
# => 3

Python

 def foo(): return 1, 2, 3

one, two, three = foo()

one
# >>> 1

three
# >>> 3

Scala

 def foo = (1, 2, 3)

val (one, two, three) = foo
// => one:   Int = 1
// => two:   Int = 2
// => three: Int = 3

Haskell

let foo = (1, 2, 3)

let (one, two, three) = foo

one
-- > 1

three
-- > 3

Perl6

sub foo { 1, 2, 3 }

my ($one, $two, $three) = foo

$one
# > 1

$three
# > 3
ответил Jörg W Mittag 3 J000000Wednesday13 2013, 14:04:15
12

Настоящая причина того, что одно возвращаемое значение настолько популярна, - это выражения, которые используются на столь многих языках. На любом языке, где вы можете иметь выражение типа x + 1, вы уже думаете о единичных возвращаемых значениях, потому что вы оцениваете выражение в своей голове, разбивая его на части и определяя значение каждого кусок. Вы смотрите на x и решаете, что это значение равно 3 (например), и вы смотрите на 1, а затем просматриваете x + 1 и собираете все вместе, чтобы решить что значение целого равно 4. Каждая синтаксическая часть выражения имеет одно значение, а не любое другое число значений; это естественная семантика выражений, которые все ожидают. Даже когда функция возвращает пару значений, она по-прежнему действительно возвращает одно значение, которое выполняет задание двух значений, потому что идея функции, которая возвращает два значения, которые не каким-то образом завернуты в одну коллекцию, слишком странно.

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

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

Другой способ иметь функции возвращать несколько значений - это иметь альтернативную семантику выражения, где любое выражение может иметь несколько значений, и каждое значение представляет собой возможность. Возьмите x + 1 снова, но на этот раз представьте, что x имеет два значения {3, 4}, тогда значения x + 1 будут be {4, 5}, а значения x + x будут {6, 8} или, возможно, {6, 7, 8}, в зависимости от того, разрешено ли одной оценке использовать несколько значений для x. Такой язык может быть реализован с использованием backtracking, так как Prolog использует, чтобы дать несколько ответов на запрос.

Короче говоря, вызов функции представляет собой единую синтаксическую единицу, и одна синтаксическая единица имеет одно значение в семантике выражения, которую все мы знаем и любим. Любая другая семантика заставит вас в странные способы делать вещи, такие как Perl, Prolog или Forth.

ответил Geo 20 32013vEurope/Moscow11bEurope/MoscowWed, 20 Nov 2013 19:43:43 +0400 2013, 19:43:43
9

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

  

, когда функция возвращает его, появляется указатель на возвращаемый объект в определенном регистре

Из трех первых языков, 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, поэтому запись re-entrant были сложными. Эта вещь, которую мы считаем само собой разумеющимся, называя подпрограмму в любое время, когда мы чувствуем, была не столь распространена в этой архитектуре. Часто создается отдельный «стек», так что параметры и адрес возврата подпрограммы будут сохраняться отдельно.

Если вы посмотрите на процессор, который вдохновил 6502, 6800 , он имел дополнительный регистр, индексный регистр (IX), столь же широкий, как и SP, который мог бы получить значение из SP.

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

SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1

Его можно вызывать любое количество раз, потому что все временное пространство находится в стеке.

8080 , используемый на TRS-80 и хосте CP /M-based микрокомпьютеры могли бы сделать что-то похожее на 6800, нажав SP на стек, а затем поместив его в свой косвенный регистр, HL.

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

Проблема, the, как вы возвращаете что-либо ? Регистры процессоров были не очень многочисленными на раннем этапе, и часто приходилось использовать некоторые из них, даже чтобы выяснить, какую часть памяти адресовать. Возвращение вещей в стеке было бы сложным: вам нужно было бы вскрыть все, сохранить ПК, нажать возвращаемые параметры (которые будут храниться там пока?), А затем снова нажать ПК и вернуться.

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

Давайте посмотрим на язык, который допускает несколько возвращаемых значений: Forth. То, что делает Forth, поддерживает отдельный стек возврата (RP) и стек данных (SP), так что все функции должны были поместить все его параметры и оставить возвращаемые значения в стеке. Поскольку стек возврата был отдельным, он не мешал.

Как человек, изучивший язык ассемблера и Forth в первые шесть месяцев опыта работы с компьютерами, множественные возвращаемые значения выглядят совершенно нормальными для меня. Операторы, такие как /mod Forth, которые возвращают целочисленное деление и остальные, кажутся очевидными. С другой стороны, я могу легко понять, как кто-то, чей ранний опыт был C mind, нашел эту концепцию странной: она идет вразрез с их укоренившимися ожиданиями о том, что такое «функция».

Что касается математики ... ну, я программировал компьютеры до того, как я получил функции в классах математики. Там есть целый раздел CS и языков программирования, на которые влияет математика, но опять же есть целая секция, которая не является.

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

ответил Daniel C. Sobral 25 WedEurope/Moscow2013-12-25T00:20:46+04:00Europe/Moscow12bEurope/MoscowWed, 25 Dec 2013 00:20:46 +0400 2013, 00:20:46
7

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

f :: Int -> (Int, Int)
f x = (x - 1, x + 1)

// Even C++ have tuples - see Boost.Graph for use
std::pair<int, int> f(int x) {
  return std::make_pair(x - 1, x + 1);
}

В приведенном выше примере f - это функция, возвращающая 2 ints.

Аналогично, ML, Haskell, F # и т. д. также могут возвращать структуры данных (указатели слишком низки для большинства языков). Я не слышал о современном языке GP с таким ограничением:

data MyValue = MyValue Int Int

g :: Int -> MyValue
g x = MyValue (x - 1, x + 1)

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

  • Нечеткая семантика : выполняет ли следующая функция: 0 или 1? Я знаю языки, которые будут печатать 0, и те, которые будут печатать 1. Преимущества для них обоих (как с точки зрения производительности, так и с учетом умственной модели программиста):

    int x;
    
    int f(out int y) {
      x = 0;
      y = 1;
      printf("%d\n", x);
    }
    f(out x);
    
  • Нелокализованные эффекты . Как и в примере выше, вы можете обнаружить, что у вас может быть длинная цепочка, а самая внутренняя функция влияет на глобальное состояние. В целом, это затрудняет рассуждение о том, каковы требования функции, и если это изменение является законным. Учитывая, что большинство современных парадигм пытаются либо локализовать эффекты (инкапсуляцию в ООП), либо исключить побочные эффекты (функциональное программирование), они противоречат этим парадигмам.

  • Быть избыточным . Если у вас есть кортежи, у вас есть 99% функциональности параметров out и 100% идиоматического использования. Если вы добавляете указатели в микс, вы покрываете оставшиеся 1%.

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

ответил Maciej Piechotka 3 J000000Wednesday13 2013, 13:49:53
6

Я думаю, что это из-за выражений , таких как (a + b[i]) * c.

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

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

ответил Roman Starkov 22 SunEurope/Moscow2013-12-22T16:30:56+04:00Europe/Moscow12bEurope/MoscowSun, 22 Dec 2013 16:30:56 +0400 2013, 16:30:56
3

Это немного усложняет синтаксис, но нет никакой веской причины при реализации чтобы не допустить этого. Вопреки некоторым другим ответам, возврат нескольких значений, когда это возможно, приводит к более четкому и эффективному коду. Я не могу сосчитать, как часто мне хотелось, чтобы я мог вернуть X и Y, или "success" boolean и полезное значение.

ответил ddyer 3 J000000Wednesday13 2013, 01:51:18
2

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

x = n + sqrt(y);

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

ответил James Anderson 3 J000000Wednesday13 2013, 05:19:49
2

Я просто хочу основываться на ответе Харви. Я изначально нашел этот вопрос на новостном техническом сайте (arstechnica) и нашел удивительное объяснение, которое, как я чувствую, действительно отвечает на суть этого вопроса и лишено всех других ответов (кроме Харви):

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

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

Это похоже на вопрос, почему назначение - это одна переменная за раз. У вас может быть язык, позволяющий a, b = 1, 2 например. Но это закончится тем, что уровень машинного кода равен a = 1, а затем b = 2.

Существует некоторое обоснование того, что конструкции языка программирования несут какое-то подобие того, что на самом деле произойдет, когда код будет скомпилирован и запущен.

ответил James Anderson 3 J000000Wednesday13 2013, 05:19:49
-1

Это началось с математики. FORTRAN, названный в честь «Formula Translation», был первым компилятором. FORTRAN был и ориентирован на физику /математику /инженерию.

COBOL, почти такой же старый, не имел явного возвращаемого значения; У него почти не было подпрограмм. С тех пор это была главным образом инерция.

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

ответил RickyS 22 SunEurope/Moscow2013-12-22T03:03:28+04:00Europe/Moscow12bEurope/MoscowSun, 22 Dec 2013 03:03:28 +0400 2013, 03:03:28
-1

Вероятно, это больше связано с наследием того, как вызовы функций выполняются в инструкциях машинного компьютера и в том, что все языки программирования выходят из машинного кода: например, C -> Сборка -> Машина.

Как процессоры выполняют вызовы функций

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

Теперь, почему процессоры были спроектированы таким образом ... вероятно, это вопрос ограничений ресурсов.

ответил Harvey 22 SunEurope/Moscow2013-12-22T10:06:45+04:00Europe/Moscow12bEurope/MoscowSun, 22 Dec 2013 10:06:45 +0400 2013, 10:06:45

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132