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

118

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

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

Есть ли объяснение этому?

M4N
источник
40
потому что вы можете вернуть массив ...
Натан Хейфилд
6
Так почему бы не разрешить только один аргумент? Я подозреваю, что это связано с речью. Возьмите несколько идей, смушите их в одну вещь, которую вы возвращаете тому, кому повезло / не повезло достаточно, чтобы слушать. Возвращение почти как мнение. «это» - это то, что вы делаете с «этим».
Эрик Реппен
30
Я считаю , что подход питона довольно прост и элегантен , когда вы должны возвращать несколько значений просто возвращаю кортеж: def f(): return (1,2,3)а затем вы можете использовать кортеж-распаковку «раскол» кортеж: a,b,c = f() #a=1,b=2,c=3. Нет необходимости создавать массив и извлекать элементы вручную, не нужно определять какой-либо новый класс.
Бакуриу
7
Я могу сообщить вам, что Matlab имеет переменное число возвращаемых значений. Количество выходных аргументов определяется вызывающей подписи (например , [a, b] = f()против [a, b, c] = f()) , и полученный внутри fпутем nargout. Я не большой поклонник Matlab, но иногда это бывает очень удобно.
Gerrit
5
Я думаю , что если большинство языков программирования разработаны таким образом , является спорным. В истории языков программирования было несколько очень популярных языков, созданных таким образом (таких как Pascal, C, C ++, Java, классический VB), но сегодня также существует множество других языков, которые получают все больше поклонников, которые допускают многократный возврат ценности.
Док Браун

Ответы:

58

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

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

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

    (password, username) = GetUsernameAndPassword()  
    

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

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

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

BlueRaja - Дэнни Пфлугхофт
источник
50
Трудно сказать, что возвращение кортежа - это возвращение нескольких вещей. Это возвращение одного кортежа . Код, который вы написали, просто распаковывает его, используя синтаксический сахар.
10
@Lego: Я не вижу различий - кортеж по определению состоит из нескольких значений. Что бы вы назвали «множественными возвращаемыми значениями», если бы не это?
BlueRaja - Дэнни Пфлугхофт
19
Это действительно смутное различие, но рассмотрим пустой Tuple (). Это одна вещь или ноль вещей? Лично я бы сказал одно. Я могу назначить x = ()просто отлично, так же, как я могу назначить x = randomTuple(). В последнем случае, если возвращенный кортеж пуст или нет, я все еще могу назначить один возвращенный кортеж x.
19
... Я никогда не утверждал, что кортежи не могут быть использованы для других вещей. Но утверждать, что «Python не поддерживает множественные возвращаемые значения, он поддерживает кортежи» , просто крайне бессмысленно педантично. Это все еще правильный ответ.
BlueRaja - Дэнни Пфлюгофт
14
Ни кортежи, ни классы не являются «множественными значениями».
Андрес Ф.
54

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

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

Telastyn
источник
5
@FrustratedWithFormsDesigner - это возникло в недавнем вопросе . С одной стороны, я могу рассчитывать, сколько раз мне понадобилось несколько выходов за 20 лет.
Теластин
61
Функции в математике и функции в большинстве языков программирования - два совершенно разных зверя.
tdammers
17
@tdammers в первые дни, они были очень похожи в мыслях. Фортран, паскаль и тому подобное, где математика в большей степени под влиянием, чем вычислительная архитектура.
9
@tdammers - как так? Я имею в виду, что для большинства языков это сводится к лямбда-исчислению в конце - один вход, один выход, без побочных эффектов. Все остальное - симуляция / взлом на вершине этого. Функции программирования могут быть не совсем чистыми в том смысле, что множественные входы могут давать один и тот же вывод, но дух есть.
Теластин
16
@SteveEvers: прискорбно, что имя «функция» вступило во владение в императивном программировании вместо более подходящей «процедуры» или «рутины». В функциональном программировании функция больше напоминает математические функции.
tdammers
35

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

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

Для типов функций, о которых вы говорите (т. Е. Функций, которые возвращают несколько значений разных типов ), я вижу это немного по-другому, чем кажется: я вижу необходимость / использование out-параметров в качестве обходного пути для улучшения дизайна или более полезная структура данных. Например, я бы предпочел, чтобы *.TryParse(...)методы возвращали Maybe<T>монаду вместо использования параметра out. Подумайте об этом коде в F #:

let s = "1"
match tryParse s with
| Some(i) -> // do whatever with i
| None -> // failed to parse

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

Для других сценариев - тех, которые я не могу вспомнить - достаточно простого кортежа.

Стивен Эверс
источник
1
Кроме того, я действительно хотел бы иметь возможность писать на C #: var (value, success) = ParseInt("foo");это будет тип проверки во время компиляции, потому что он (int, bool) ParseInt(string s) { }был объявлен. Я знаю, что это можно сделать с помощью дженериков, но все же это будет хорошим дополнением к языку.
Гримаса Отчаяния
10
@GrimaceofDespair, что вам действительно нужно, так это деструктурировать синтаксис, а не множественные возвращаемые значения.
Доменик
2
@ Warren: Да. Смотрите здесь и обратите внимание, что такое решение не будет непрерывным: en.wikipedia.org/wiki/Well-definition
Стивен Эверс
4
Математическое понятие четкости не имеет ничего общего с количеством выходов, которые возвращает функция. Это означает, что выходы всегда будут одинаковыми, если вход одинаковый. Строго говоря, математические функции возвращают одно значение, но это значение часто является кортежем. Для математика, по сути, нет никакой разницы между этим и возвращением нескольких значений. Аргументы в пользу того, что функции программирования должны возвращать только одно значение, потому что математические функции не очень убедительны.
Майкл Силер
1
@MichaelSiler (я согласен с вашим комментарием) Но обратите внимание, что аргумент является обратимым: «аргументы, которые программные функции могут возвращать несколько значений, потому что они могут возвращать одно значение кортежа, тоже не слишком убедительны» :)
Андрес Ф.
23

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

Дэвид
источник
Ах! Очень интересная деталь. +1!
Стивен Эверс
Это должен быть принятый ответ. Обычно люди думают о целевых машинах, когда строят компиляторы. Другая аналогия - почему у нас есть int, float, char / string и т. Д., Потому что это то, что поддерживается целевой машиной. Даже если цель не голая металлическая (например, jvm), вы все равно хотите получить достойную производительность, не слишком эмулируя.
imel96
26
... Вы можете легко определить соглашение о вызове для возврата нескольких значений из функции, почти так же, как вы можете определить соглашение о вызове для передачи нескольких значений в функцию. Это не ответ. -1
BlueRaja - Дэнни Пфлугхофт
2
Было бы интересно узнать, рассматривали ли когда-нибудь механизмы выполнения на основе стека (JVM, CLR) / разрешали множественные возвращаемые значения ... это должно быть довольно легко; вызывающий просто должен получить правильное количество значений, точно так же, как он выдвигает правильное количество аргументов!
Лоренцо Дематте
1
@ Давид нет, cdecl допускает (теоретически) неограниченное количество параметров (поэтому возможны функции varargs ) . Хотя некоторые C-компиляторы могут ограничивать вас несколькими десятками или сотнями аргументов для каждой функции, что, я думаю, все еще более чем разумно -_-
BlueRaja - Danny Pflughoeft
18

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

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

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

Рубин

def foo; return 1, 2, 3 end

one, two, three = foo

one
# => 1

three
# => 3

питон

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
Йорг Миттаг
источник
1
Я думаю, что один аспект заключается в том, что в некоторых языках (таких как Matlab) функция может быть гибкой в ​​отношении того, сколько значений она возвращает; см. мой комментарий выше . В Matlab есть много аспектов, которые мне не нравятся, но это одна из немногих (возможно, единственных) функций, которые мне не хватает при портировании с Matlab, например, на Python.
Gerrit
1
Но как насчет динамических языков, таких как Python или Ruby? Предположим, я пишу что-то вроде sortфункции Matlab : sorted = sort(array)возвращает только отсортированный массив, а [sorted, indices] = sort(array)возвращает оба. Единственный способ, которым я могу придумать в Python, - это передать флаг sortв соответствии с sort(array, nout=2)или sort(array, indices=True).
Gerrit
2
@MikeCellini Я так не думаю. Функция может определить количество выходных аргументов, вызываемых функцией ( [a, b, c] = func(some, thing)), и действовать соответственно. Это полезно, например, если вычисление первого выходного аргумента дешево, а вычисление второго - дорого. Я не знаком ни с каким другим языком, где эквивалент Matlabs nargoutдоступен во время выполнения.
Gerrit
1
@gerrit правильное решение в Python, чтобы написать это: sorted, _ = sort(array).
Майлз Рут
1
@MilesRout: И sortфункция может сказать, что ей не нужно вычислять индексы? Это круто, я этого не знал.
Йорг Миттаг
12

Реальная причина того, что одно возвращаемое значение настолько популярно, - это выражения, которые используются во многих языках. На любом языке, где у вас может быть выражение, которое 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.

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

Если вы посмотрите на процессор, который вдохновил 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 , используемый на ТРС-80 и множество CP / M микрокомпьютеров на основе могли бы сделать что - то похожее на 6800, нажав SP на стек , а затем выскакивают его на косвенном регистре, HL.

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

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

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

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

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

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

Таким образом, у нас есть стечение факторов, где математика влияла на ранний языковой дизайн, где аппаратные ограничения диктовали то, что было легко реализовано, и где популярные языки влияли на то, как развивалось оборудование (машины Лиспа и процессоры Форта в этом процессе были путаницами).

Даниэль С. Собрал
источник
@gnat Использование «для информирования», как и «для обеспечения необходимого качества», было преднамеренным.
Даниэль С. Собрал
не стесняйтесь отката, если вы сильно настроены по этому поводу; согласно моему влиянию чтения, здесь немного лучше: «влияет ... важным образом»
комнат
1
+1 Как указывается, то, что редкое число регистров ранних процессоров по сравнению с обильным набором регистров современных процессоров (также используемых во многих ABI, например, x64 abi) может быть изменением игры, и прежде веские причины возвращать только 1 значение может быть в наше время просто историческая причина.
BitTickler
Я не уверен, что ранние 8-битные микропроцессоры имеют большое влияние на языковой дизайн и то, что предполагается в соглашениях о вызовах C или Fortran в любой архитектуре. Fortran предполагает, что вы можете передавать аргументы массива (по существу, указатели). Итак, у вас уже есть серьезные проблемы с реализацией нормального Фортрана на машинах, подобных 6502, из-за отсутствия режимов адресации для указателя + индекса, как описано в вашем ответе и в разделе Почему компиляторы с C на Z80 производят плохой код? на ретрокомпьютинг.
Питер Кордес
Фортран, как и С, также предполагает, что вы можете передавать произвольное количество аргументов и иметь произвольный доступ к ним и произвольное количество местных жителей, верно? Как вы только что объяснили, вы не можете сделать это легко на 6502, потому что адресация относительно стека не важна, если вы не откажетесь от повторного входа. Вы можете вставить их в статическое хранилище. Если вы можете передать произвольный список аргументов, вы можете добавить дополнительные скрытые параметры для возвращаемых значений (например, после первого), которые не помещаются в регистры.
Питер Кордес
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 дюйма.

Аналогично, 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параметра (и в большинстве случаев допускается 2 или более таких методов).

Мацей Печотка
источник
+1 За упоминание о том, как функциональные языки справляются с этим элегантно и безболезненно.
Андрес Ф.
1
Технически вы все еще возвращаете одно значение: D (Просто это одно значение тривиально разложить на несколько значений).
Томас Эдинг
1
Я бы сказал, что параметр с реальной семантикой "out" должен вести себя как временный компилятор, который копируется в место назначения при нормальном выходе из метода; переменная с семантикой «inout» должна вести себя как временная компилятор, которая загружается из переданной переменной при входе и записывается обратно при выходе; один с семантикой "ref" должен вести себя как псевдоним. Так называемые «out» параметры C # действительно являются «ref» параметрами и ведут себя как таковые.
суперкат
1
Кортеж «обходной путь» также не приходит бесплатно. Блокирует возможности оптимизации. Если бы существовал ABI, который позволял возвращать N возвращаемых значений в регистрах ЦП, компилятор мог бы фактически оптимизировать вместо создания экземпляра кортежа и его конструирования.
BitTickler
1
@BitTickler - ничто не мешает возвратить первые n полей структуры, которые будут переданы регистрами, если вы управляете ABI.
Мацей Пехотка
6

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

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

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

Роман Старков
источник
4

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

ddyer
источник
3
Не могли бы вы привести пример, когда несколько возвратов дают более четкий и / или более эффективный код?
Стивен Эверс
3
Например, в программировании COM на C ++ многие функции имеют один [out]параметр, но практически все возвращают HRESULT(код ошибки). Было бы довольно практично просто найти там пару. В языках с хорошей поддержкой кортежей, таких как Python, это используется во многих кодах, которые я видел.
Феликс Домбек
В некоторых языках вы бы возвращали вектор с координатами X и Y, а возвращение любого полезного значения считалось бы «успехом» с исключениями, возможно, несущими это полезное значение, используемое для сбоя.
doppelgreener
3
Много времени вы заканчиваете тем, что кодируете информацию в возвращаемое значение неочевидными способами - т.е. отрицательные значения - это коды ошибок, положительные значения - это результаты. ЮК. Получая доступ к хеш-таблице, всегда сложно указать, был ли найден элемент, а также вернуть его.
ddyer
@SteveEvers Matlab sortфункция обычно сортирует массив: sorted_array = sort(array). Иногда мне нужны соответствующие индексы: [sorted_array, indices] = sort(array). Иногда я хочу, чтобы только индексы: [~, indices]= sort (array) . The function sort` фактически указывал, сколько выходных аргументов необходимо, поэтому, если требуется дополнительная работа для 2 выходов по сравнению с 1, он может вычислять эти выходные данные только при необходимости.
Gerrit
2

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

x = n + sqrt(y);

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

Джеймс Андерсон
источник
5
Только не используйте неуместные функции. Это ничем не отличается от «проблемы», создаваемой функциями, которые не возвращают значений или возвращают нечисловые значения.
ddyer
3
В языках, которые я использовал, которые предлагают несколько возвращаемых значений (например, SciLab), первое возвращаемое значение является привилегированным и будет использоваться в тех случаях, когда требуется только одно значение. Так что никаких реальных проблем нет.
Фотон
И даже если это не так, как при распаковке кортежей в Python, вы можете выбрать, какой из них вы хотите:foo()[0]
Izkata
Точно, если функция возвращает 2 значения, то тип возвращаемого значения - 2 значения, а не одно значение. Язык программирования не должен читать ваши мысли.
Марк Э. Хааз
1

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

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

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

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

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

user2202911
источник
Если бы низкоуровневые языки, такие как C, поддерживали несколько возвращаемых значений в качестве функции первого класса, соглашения о вызовах C включали бы несколько регистров возвращаемых значений, так же, как до 6 регистров используются для передачи аргументов функции целого числа / указателя в x86- 64 Система V ABI. (На самом деле x86-64 SysV действительно возвращает структуры до 16 байтов, упакованных в пару регистров RDX: RAX. Это хорошо, если структура была только что загружена и будет сохранена, но стоит дополнительных распаковок по сравнению с наличием отдельных членов в отдельных регистрах даже если они уже, чем 64 бита.)
Питер Кордес
Очевидным соглашением будет RAX, а затем - arg-pass regs. (RDI, RSI, RDX, RCX, R8, R9). Или в соглашении Windows x64, RCX, RDX, R8, R9. Но поскольку в C изначально нет нескольких возвращаемых значений, в соглашениях C ABI / о вызовах указываются только несколько регистров возврата для широких целых чисел и некоторых структур. См. Стандарт вызова процедуры для архитектуры ARM®: 2 отдельных, но связанных возвращаемых значения для примера использования широкого int для получения компилятором эффективного asm для получения 2 возвращаемых значений на ARM.
Питер Кордес
-1

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

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

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

Rickys
источник
4
это не отвечает на заданный вопрос
Гнат
@gnat, как по мне, это отвечает. OTOH уже нужно иметь некоторый опыт, чтобы понять это, и этот человек, вероятно, не задаст вопрос ...
Netch
@ Нет необходимости в большом количестве знаний, чтобы понять, что такие утверждения, как «FORTRAN ... был первым компилятором», являются полным беспорядком. Это даже не неправильно. Так же, как и остальная часть этого «ответа»
комнат
Линк говорит, что были более ранние попытки компиляторов, но «команда FORTRAN во главе с Джоном Бэкусом в IBM, как правило, считается за то, что представила первый полный компилятор в 1957 году». Заданный вопрос был, почему только один? Как я сказал. В основном это были математика и инерция. Математическое определение термина «Функция» требует ровно одного значения результата. Так что это была знакомая форма.
RickyS
-2

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

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

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

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

Харви
источник