Исторически сложилось, что функция возвращает одно значение. В return.
Да, разумеется я знаю по ссылочные аргументы, про in/out аргументы и прочее изменение входных параметров. Но! Return все равно один.
То есть если рассматривать функцию как черный ящик, то выход у этого ящика всегда один.
Почему не придумали типа такого?
int/string/int* MyFunc()
{
int res1 = 5;
string res2 = “ssssss”;
int* res3 = (int*)malloc…;
return1 res1;
return2 res2;
return3 res3;
}
Print(MyFunc()2);
int b = MyFunc()1 + 6;
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Наверное потому что синтаксически громоздко, и тривиально решается кортежами, а ещё лучше полноценными структурами с именованными полями.
Нет. Я например сейчас разбираюсь в одной визуально программируемой штуке на нодах графа. Так вот там бывает, что у ноды три выхода, а у следующей ноды три входа. И они прям один к другому коннектятся. Хотя можно каждый выход и по отдельности использовать. То есть в коде это выглядело бы просто вызов одной функции как аргумент другой. Одна строчка. А вот с обычным подходом надо кучу кода городить
Здравствуйте, Буравчик, Вы писали:
Б>Здравствуйте, Hоmunculus, Вы писали:
H>>Почему не придумали типа такого?
Б>Придумали. Называется корутины. См. ключевое слово yield в разных языках программирования
Я знаю про них. Нет. Не то. Прочитай мой пример выше.
Здравствуйте, Буравчик, Вы писали:
Б>Здравствуйте, Hоmunculus, Вы писали:
H>>Я знаю про них. Нет. Не то. Прочитай мой пример выше.
Б>Прочитал, не вижу разницы. Поясни
У нас есть тяжелая функция. Вызывать ее несколько раз — накладно. Она должна за один проход вычислить кучу всего и отдать сразу же при вызове.
Да, и про классы я знаю. Городить класс вокруг каждой тяжелой функции — тоже не то
Здравствуйте, Hоmunculus, Вы писали:
H>У нас есть тяжелая функция. Вызывать ее несколько раз — накладно. Она должна за один проход вычислить кучу всего и отдать сразу же при вызове. H>Да, и про классы я знаю. Городить класс вокруг каждой тяжелой функции — тоже не то
Здравствуйте, Hоmunculus, Вы писали:
H>Здравствуйте, T4r4sB, Вы писали:
H>А, ну да. В Питоне вроде видел такое. Это питоновский кортеж?
Так-то это код на Русте.
На С++ это не сильно сложнее кстати
std::tuple<int,string,int*> MyFunc()
{
int res1 = 5;
string res2 = “ssssss”;
int* res3 = (int*)malloc…;
return {res1,res2,res3};
}
...
auto tuple = MyFunc(); // так можноauto [some_it, some_string, some_pointer] = MyFunc(); // так тоже можно начиная с ++17
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте
Здравствуйте, Hоmunculus, Вы писали:
H>Почему не придумали типа такого?
int/string/int* MyFunc()
{
int res1 = 5;
string res2 = “ssssss”;
int* res3 = (int*)malloc…;
return1 res1;
return2 res2;
return3 res3;
}
Print(MyFunc()2);
int b = MyFunc()1 + 6;
Я тоже над этим думал, но вероятно нужно читать историю создания языка, причём языка Си откуда и пошёл C++. А в Си ещё так же нет функционального блока в отличие от Structured Text.
Я сейчас не говорю про лишнее копирование, то есть возврат по значению, а не по ссылке или ещё что-то такое. Но даже пользователи Си могут имитировать возврат множества аргументов, функциональный блок и прочее, причём разными способами.
По крайне мере именованные возвращаемые значения выглядят более логично, к тому же происходит возврат всех аргументов в одном месте.
Но с другой стороны можно вернуть значения в параметрах функции, можно использовать структуру. И я не думаю, что пользователи Си над этим заморачиваются.
Если уж философствовать, то почему бы всегда не передавать аргументы в функцию в структуре. Тогда получится единый подход к передаче данных.
Здравствуйте, Hоmunculus, Вы писали:
H>Почему не придумали типа такого?
Значит не нужно было.
H>То есть если рассматривать функцию как черный ящик, то выход у этого ящика всегда один.
Выходов может быть много. Вы не функции смотрите, а классы например там не только выходов, но и входов может быть сколько угодно.
Здравствуйте, T4r4sB, Вы писали:
V>>Я сейчас не говорю про лишнее копирование, то есть возврат по значению, а не по ссылке или ещё что-то такое. TB>А разве это в скомпилированном коде будет отличаться от возврата по ссылке?
Меня Страуструп в его книге учил, что нельзя полагаться на особое предопределённое поведение компиляторов. Какая это реализация компилятора, какой версии, какие флаги оптимизации и тому подобное.
Потому про компилятор и что у него "под капотом" ничего не скажу. Я веду речь только про то, что программист явно указывает в коде. А указать он может передача это по значению, по ссылке или по указателю.
Здравствуйте, Hоmunculus, Вы писали:
H> Да знаю я и про структуры и про классы. Получится лютый треш, если вокруг каждой из возможных комбинаций типов городить такое.
Не получится треша. Получится хорошо читаемый и легко поддерживаемый код.
Здравствуйте, Hоmunculus, Вы писали:
EP>>Наверное потому что синтаксически громоздко, и тривиально решается кортежами, а ещё лучше полноценными структурами с именованными полями. H>Нет. Я например сейчас разбираюсь в одной визуально программируемой штуке на нодах графа. Так вот там бывает, что у ноды три выхода, а у следующей ноды три входа. И они прям один к другому коннектятся H>Хотя можно каждый выход и по отдельности использовать. То есть в коде это выглядело бы просто вызов одной функции как аргумент другой. Одна строчка. А вот с обычным подходом надо кучу кода городить
В других языках более громоздкие std::apply и подобное.
В принципе можно было бы завести специальный тип кортежа, который бы автоматически разбрасывался языком по аргументам. Но
H>И они прям один к другому коннектятся
В таких случаях намного лучше, читаемей и безопасней структуры, нежели безымянные кортежи (явные или внедрённые в язык для множественных значений возврата — не суть).
Ибо структура:
1. сама по себе является отдельным типом, что улучшает читаемость, type safety и type richness, что например позволяет делать перегрузку по разным типа структур.
2. поля структуры именованы, что улучшает читаемость и опять-таки safety. Вот например std::map::insert возвращает std::pair — это всего два значения, но всё же я искал ответ множество раз, ибо нет мнемоники за что зацепиться.
Здравствуйте, T4r4sB, Вы писали:
V>>Меня Страуструп в его книге учил, что нельзя полагаться на особое предопределённое поведение компиляторов. TB>Как в сишке не знаю, но в С++ rvo часть стандарта
Начиная с С++17, оно ещё и гарантированно (что например позволяет возвращать значения не копируемых и не перемещаемых типов).
Здравствуйте, velkin, Вы писали:
V>Если уж философствовать, то почему бы всегда не передавать аргументы в функцию в структуре. Тогда получится единый подход к передаче данных.
Ну в некоторых языках у всех функций только один параметр
Здравствуйте, Hоmunculus, Вы писали:
H>Городить класс вокруг любой комбинации типов — такое себе.
Это собственно говоря почему? У вас черный ящик и если вам нужна некая абстракция — класс самое то.
Если нужны комбинации есть шаблоны.
А именно так — а где все же выход из функции ? Ведь return тут вовсе не выход, а присваивание некоей скрытой переменной result1, result2...
А настоящий return будет лишь закрывающей фигурной скобкой. А если я хочу именно выйти из функции внутри какого-то if — else — for... ?
В интерпретаторе такое можно позволить. Пусть он отслеживает, каким result присваивались значения и при выходе проверяет, всем ли присвоено.
В компиляторе — нет. Совершенно ненужные накладные расходы.
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>А настоящий return будет лишь закрывающей фигурной скобкой. А если я хочу именно выйти из функции внутри какого-то if — else — for... ?
ну если исходить из логики ТС то у него должно быть return res2/res2/res3; что автоматически нас приводит к кортежам, которые поддерживаются всеми языками, а где не поддерживаются — есть структуры.
Здравствуйте, mike_rs, Вы писали:
_>ну если исходить из логики ТС то у него должно быть return res2/res2/res3; что автоматически нас приводит к кортежам, которые поддерживаются всеми языками, а где не поддерживаются — есть структуры.
Если да — это скрытый кортеж, верно. Превратить его в явный и дело с концом.
Но тогда просто return1.res1 превращается в tuple.field1 = res1, а return tuple где надо.
Здравствуйте, Hоmunculus, Вы писали:
H>Городить класс вокруг любой комбинации типов — такое себе.
А смешивать нити исполнения в одну функцию это не так себе?
int/string/int* MyFunc()
{
int res1 = 5;
string res2 = “ssssss”;
int* res3 = (int*)malloc…;
return1 res1;
return2 res2;
return3 res3;
}
Print(MyFunc()2);
int b = MyFunc()1 + 6;
H>То есть в коде это выглядело бы просто вызов одной функции как аргумент другой. Одна строчка. А вот с обычным подходом надо кучу кода городить
Ну эта возможность должна быть базовая для функциональных языков. Так что много где она есть.
Проверил C# — туплы поддерживает уже несколько лет, но именно фичи "разобрать тупл в параметры" почему-то не сделали. На практике, так чтобы прямо точно совпадали и типы тупла и порядок аргументов функции, и смысл, наверное достаточно редко случается (думаю в MS прямо по кодовой базе гитхаба проверили), и поэтому решили что им невыгодно это писать. Может ждут pull-request от комьюнити.
Если проблема частая, то самопальным SourceGenerator-ом можно в C# для функций генерировать обёртки, которые примут тупл, разложат, и вызовут. Т.е. технически уже сейчас даже в C# для себя сделать можно.
Проверил Nemerle — он эту фичу умел с самого начала.
using System;
public class Test
{
static fn() : int*string*double
{ (1, "one", 1.0) }
static getName(key : int, name : string, len : double) : string
{ name }
static Main () : void
{
Console.WriteLine (getName(fn()));
}
}
Здравствуйте, Hоmunculus, Вы писали:
H>Исторически сложилось, что функция возвращает одно значение. В return. H>Да, разумеется я знаю по ссылочные аргументы, про in/out аргументы и прочее изменение входных параметров. Но! Return все равно один. H>То есть если рассматривать функцию как черный ящик, то выход у этого ящика всегда один.
Здравствуйте, Hоmunculus, Вы писали:
H>Исторически сложилось, что функция возвращает одно значение. В return. H>Да, разумеется я знаю по ссылочные аргументы, про in/out аргументы и прочее изменение входных параметров. Но! Return все равно один. H>То есть если рассматривать функцию как черный ящик, то выход у этого ящика всегда один.
H>Почему не придумали типа такого?
H>
в шарпах ref/out, а вот в java видимо придется делать обертку, но JIT должен это по идее заоптимизировать, т.к. непонятно как вернуть несколько параметров по значению, т.к. что ссылки, что примитивы возвращаются через копирование.
Здравствуйте, Hоmunculus, Вы писали: H>Исторически сложилось, что функция возвращает одно значение. В return. H>.. H>То есть если рассматривать функцию как черный ящик, то выход у этого ящика всегда один. H>Почему не придумали типа такого?
Непонятно: тебе все три возврата одновременно нужны или каждый раз свой возврат при разных вызовах?
Если одновременно, то кортеж напрашивается (уже подсказали выше).
Если не одновременно, то нужна диспетчеризация:
Javascript
(()=>{
let MyFunc = (i) => {
if (typeof i === 'number') {
let res1 = 5;
return res1;
} else if (typeof i === 'string') {
let res2 = 'ssssss';
return res2;
} else if (Array.isArray(i)) {
let res3 = [1, 2, 3, 4, 5];
return res3;
} else {
throw new Error(`Неправильный тип параметра запроса: ${typeof i}`);
}
}
console.log(MyFunc(''));
console.log(MyFunc(1) + 6);
console.log(MyFunc([]));
})();
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>А именно так — а где все же выход из функции ? Ведь return тут вовсе не выход, а присваивание некоей скрытой переменной result1, result2... PD>А настоящий return будет лишь закрывающей фигурной скобкой. А если я хочу именно выйти из функции внутри какого-то if — else — for... ?
Совершенно согласен. Если пытаться напилить язык, который работает именно с запрошенным синтаксисом, то возможен взрыв мозга.
int/string/int* MyFunc()
{
int res1 = 5;
int* res3 = (int*)malloc…;
return1 res1;
if (res1 == 0)
return2 "foo"else
return2 "bar";
for (int i =0; i< 42; i++)
return3 res3++;
}
PD>В интерпретаторе такое можно позволить. Пусть он отслеживает, каким result присваивались значения и при выходе проверяет, всем ли присвоено.
То-то и оно. Внезапно оказывается, что вот такой код делает совершенно не то, чего ожидается:
int/int find(int[] array, int length, int value)
{
for(int i=0; i<length; i++)
if (array[i] == value)
{
return1 i;
return2 value;
}
return1 -1;
return2 0;
}
PD>В компиляторе — нет. Совершенно ненужные накладные расходы.
Тут даже в не в расходах компилятора дело. Современный компилятор типа C# при возврате туплов проверяет definite assignment. Дело в расходе умственной энергии программиста.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Тут даже в не в расходах компилятора дело. Современный компилятор типа C# при возврате туплов проверяет definite assignment. Дело в расходе умственной энергии программиста.
Согласен.
В годы моей молодости в одной весьма в целом неплохой книге была дана рекомендация по написанию функций — один вход, один выход. То есть в терминах C-like языков — вход только через заголовок, выход только через последнюю }
Идея одного выхода оказалась не лучшей. Действительно, при, например, линейном поиске проще сделать return i внутри цикла, когда нашли, а конце return -1, так как не нашли.
А вот идея одного входа практически везде сейчас. В фортране можно было иметь несколько входов , чтобы не писать несколько функций с одним и тем же почти кодом. Но сейчас это решается выделением общего кода в отдельную приватную функцию и вызовом ее везде. С несколькими входами та же проблема — черт его знает, как мы сюда попали, через какой вход.
Здравствуйте, Hоmunculus, Вы писали:
H>Исторически сложилось, что функция возвращает одно значение. В return.
В одном значении может быть что-то, что хранит в себе юнион с типом, или коллекцию значений.
Даже в голом C можно вернуть структуру. И нет смысла избегать этого, более того: возврат структур
по-значению для компилятора легче в плане генерации оптимального кода, чем работа с указателями.
Просто на некоторых старинных рахитектурах возврат структуры по-значению требовал выкрутасов
с копированием на стеке и этого избегали. Сейчас везде как правило возврат структуры компилятором
преобразуется в ту же передачу по-ссылке (на структуру которая будет возвращена, и память под
которую аллоцирована на стеке вызывающей функции), но при этом отлично оптимизируется в регистры
при случае.
H>Почему не придумали типа такого?
H>
Здравствуйте, Hоmunculus, Вы писали:
H>Здравствуйте, velkin, Вы писали:
H>Городить отдельную структуру для каждой комбинации выходных типов — слишком накладно.
Проблема в том, что в убогих недоязыках отсутствует auto и шаблоны.
Здравствуйте, Hоmunculus, Вы писали:
H>Да, и про классы я знаю. Городить класс вокруг каждой тяжелой функции — тоже не то
Зато именами полей они дают семантику, да и порядок знать не надо. С порядком вообще легко в лужу сесть, особенно если типы одинаковые.
В тех же плюсах в STL в новых классах перестали возвращать std::pair, и хорошо, что не начали возвращать туплы.
Здравствуйте, Hоmunculus, Вы писали:
H>Исторически сложилось, что функция возвращает одно значение. В return. H>Да, разумеется я знаю по ссылочные аргументы, про in/out аргументы и прочее изменение входных параметров. Но! Return все равно один. H>То есть если рассматривать функцию как черный ящик, то выход у этого ящика всегда один.
H>Почему не придумали типа такого?
Все ЯП так или иначе соответствуют возможностям ассемблера и инструкциям процессора.
У процессора есть возможность вызова кода (call с вариантами получения адреса точки вызова), при этом адрес следующей за вызовом инструкции call помещается в стек и появляется возможность вернуться после вызова обратно и продолжить. Для возврата есть своя инструкция (ret с вариантами), берущая из стека адрес, ранее сохранённый там call.
И вот эта инструкция ret — она одна, и return в функциях прямо ей соответствует.
Повторение ret после ret бессмысленно с точки зрения управления последовательностью выполнения инструкций.
А дальше навешивались правила, какие регистры и как можно использовать и в качестве чего, как ещё использовать стек и указатели на данные. Как передавать параметры и получать результат.
Другими словами, управление последовательностью выполнения инструкций процессора отдельно, а параметры отдельно. Но лукавые авторы ЯП посчитали, что они смогут их гармонично смешать. Круги на воде от результата их гармонии мы до сих пор наблюдаем.
Здравствуйте, Hоmunculus, Вы писали:
H>Почему не придумали типа такого?
Придумали.
func minmax(nums []int) (min, max int) {
if len(nums) > 0 {
min = nums[0]
max = nums[0]
for i := 1; i < len(nums); i++ {
n := nums[i]
if n < min {
min = n
}
if n > max {
max = n
}
}
}
return min, max
}
P.S. А что твой вариант должен делать, если return1, 2, 3 в разных сочетаниях встречаются в разных путях программы?
Здравствуйте, velkin, Вы писали:
V>Я тоже над этим думал, но вероятно нужно читать историю создания языка, причём языка Си откуда и пошёл C++. А в Си ещё так же нет функционального блока в отличие от Structured Text.
Авторы Си потом добавили множественные возвращаемые значения в свой следующий язык под названием Алеф. Потом это попало в Go, руками тех же идей. Очень подозреваю, что в Rust оттуда же попало.