Здравствуйте, NeoCode, Вы писали:
NC>В данном случае синтаксически явно (скобками) выделен аргумент, внутри которого может быть что угодно. В языках, в которых блоки кода могут возвращать значения, можно воткнуть в аргмент целый кусок кода с циклами, условыми переходами и вероятно даже объявлениями классов. Против этого я ничего не имею. NC>В случае void становятся возможными вставки void просто в основной список аргументов, т.е. со стороны это выглядит именно как передача полноценных аргументов NC>
NC>Более того, такое невозможно сделать ни с одним другим типом. ИМХО, это вносит путаницу и именно это мне не нравится.
Ну сейчас невозможно вызвать функцию с передачей void значение. Такое невозможно сделать с другим типом
В принципе такое нужно только в обобщенном коде.
Возможно в обычном действительно вносит лишнюю путаницу.
Здравствуйте, _NN_, Вы писали:
_NN>Ну сейчас невозможно вызвать функцию с передачей void значение. Такое невозможно сделать с другим типом _NN>В принципе такое нужно только в обобщенном коде. _NN>Возможно в обычном действительно вносит лишнюю путаницу.
Между обобщенным и обычным кодом не должно быть разницы, иначе это вообще полная фигня получится.
Здравствуйте, Sinclair, Вы писали:
NC>>По идее, в оригинальном виде тоже можно (полноценный тип же). Но из этого следует возможность расширить еще еще дальше, т.е. втыкать void в любые функции? Или не следует? S>Нет, не следует. Попытки скормить default(void) в неожиданные места должны приводить к ошибкам типизации.
То есть получается следующее:
1. если функция объявлена (тем или иным способом — явно или в результате раскрытия обобщенного кода) например как foo(void,int,void,void), то ее можно "сокращать" ко всем вариантам, получаемым путем выкидывания одного или нескольких void. Все эти варианты будут эквивалентны. Но только сокращать — вставлять void-значения в произвольные места в списке аргументов функции (туда где их не было в прототипе) нельзя.
Сразу вопрос — а должны ли быть эквивалентны варианты foo(void,int) и foo(int)? ведь void у нас теперь полноценный тип.
2. Функция, объявленная как foo(), эквивалентна функции foo(void), т.е. отсутствие аргументов вообще эквивалентно одному аргументу void. Это исключение вводится для того, чтобы можно было писать foo(bar()); для void.
А кому-то захочется передать foo(bar(), bar()); Ведь чем один void-аргумент лучше двух или трех?
Опять некрасивое противоречие, приводящее в конечном итоге все к тому же — к возможности пихать void-значнения в произвольные места списков аргументов функций при их вызове.
Здравствуйте, NeoCode, Вы писали:
NC>То есть получается следующее: NC>1. если функция объявлена (тем или иным способом — явно или в результате раскрытия обобщенного кода) например как foo(void,int,void,void), то ее можно "сокращать" ко всем вариантам, получаемым путем выкидывания одного или нескольких void. Все эти варианты будут эквивалентны. Но только сокращать — вставлять void-значения в произвольные места в списке аргументов функции (туда где их не было в прототипе) нельзя.
NC>Сразу вопрос — а должны ли быть эквивалентны варианты foo(void,int) и foo(int)? ведь void у нас теперь полноценный тип.
Похожая логика уже есть для параметров со значениями по умолчанию: выкидывать можно, вставлять лишние нет; foo(int a, bool b = false) не эквивалентен foo(int a); даны правила для разрешения потенциальных неоднозначностей.
Здравствуйте, Don Reba, Вы писали:
DR>Похожая логика уже есть для параметров со значениями по умолчанию: выкидывать можно, вставлять лишние нет; foo(int a, bool b = false) не эквивалентен foo(int a); даны правила для разрешения потенциальных неоднозначностей.
Здравствуйте, _NN_, Вы писали:
_NN>Насколько логично относиться к 'void' или 'Unit' как к обычному типу ? _NN>Скажем, логично ли требовать чтобы код с обычными типами работал и с void ?
Разумеется, логично.
_NN>А с void ? _NN>
_NN>function f(a : void) {..}
_NN>function g(b : void) {..}
_NN>function h(c : void, d : void) {..}
_NN>var x = f();
_NN>f(g()); // OK ?
_NN>f(x); // OK ?
_NN>h(f(), g()); // OK ?
_NN>
если void это нормальный тип, то нет никакой проблемы — параметр тоже может быть void
в обычном коде это не нужно, а вот в дженериках очень даже нужно
Здравствуйте, deniok, Вы писали:
D>В языках с алгебраическими типами данных Unit — обычный тип, в котором имеется ровно одно значение.
На си то же самое можно сделать
struct SVoid {};
Тут, скорее проблема "безэлементный кортеж — это что?" и "одноэлементный кортеж — это что?"
В питоне, например, любой кортеж является последовательностью, и чтобы отличить кортеж от элемента, изобретён специальный синтаксис конструирования
Здравствуйте, NeoCode, Вы писали:
NC>2. Функция, объявленная как foo(), эквивалентна функции foo(void), т.е. отсутствие аргументов вообще эквивалентно одному аргументу void.
Я думаю, это лишнее. NC> Это исключение вводится для того, чтобы можно было писать foo(bar()); для void.
Зачем? У вас есть функция foo(). Она ничего не принимает. Зачем вы хотите передать в неё результат функции, даже если она ничего не возвращает?
Если у нас есть foo<T>(T a), и T bar<T>(), то композиция функций — дело приемлемое, и она должна работать, в том числе, и для T=void. Но совершенно непонятно, зачем разрешать "передавать" в foo() результат bar<T>(), ведь такой код будет работать только для одного типа параметра. А для него вместо foo(bar()) можно написать bar();foo(). NC>А кому-то захочется передать foo(bar(), bar()); Ведь чем один void-аргумент лучше двух или трех?
Надо делать не то, что "кому-то захочется", а то, что упрощает типовые сценарии.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Надо делать не то, что "кому-то захочется", а то, что упрощает типовые сценарии.
Я бы смягчил.
В конце концов, в С++ долго воевали за унификацию return smth и return void, в первую очередь, в шаблонах, — и к 2011 стандарту таки согласились, что можно, нужно и полезно.
То есть, сценарии есть, равно как и есть обходные решения. А делать надо, исходя из здорового умеренного консерватизма.
Потому что затащить в С++ систему типов, похожую на Хиндли-Милнера, например, — это было бы круто, но удовольствие очень дорогое.
Сделать void первоклассным типом — это удовольствие дешёвое, но возникают подводные грабли при обработке списка аргументов.
Но, прежде чем вводить в язык, надо прикинуть, как оно будет работать.
Допустим, к примеру, мы определим тип
namespace std {
struct unit_t
{
unit_t() {}
template<class T> unit_t(T const&) {} // любой тип можно привести к void, а значит, и к unit
};
static unit_t const unit;
}
std::unit_t foo(std::unit_t x, std::unit_t y, std::unit_t z) { return std::unit; }
std::unit_t v() { return std::unit; }
int main()
{
foo( v(), v(), (v(),v(),v()) ); // третий операнд использует оператор запятую
}
Так, вроде бы понятно, как отличать последовательности от списка аргументов. Как в старом добром си, дополнительные скобки.
Недостаёт трёх вещей
— неявного приведения void к std::unit_t
— неявных return'ов в конце функций
— особой реализации оператора запятой, т.к. пользователь может определить свой оператор, который захавает unit_t, — но захавать чистый void не получится, — все operator,(void,T) и operator,(T,void) являются встроенными, — то же самое надо (?) и для unit_t
Это один путь.
Другой путь — не делать новый тип, а сразу принять void как первоклассный (т.е. внутренне реализовать его так же, как std::unit_t).
Тогда у нас встаёт проблема legacy — старый синтаксис
some foo(); // нуль-арная
some foo(void); // нуль-арная или, по-новому, унарная?
Волюнтаристский подход состоит в том, чтобы запретить старый стиль и трактовать всё сразу по-новому.
Резать-хвост-по-частям — в том, чтобы такие унарные функции домысливать до дефолтных параметров
some foo(); // имея в виду some foo(void _ = void());
foo();
foo( void() );
foo( bar ); // приводя значение bar к void
NC>(такое может получиться, например, из шаблонной функции при подстановке void в параметры шаблона) NC>Должен ли компилятор "сворачивать" ее к виду "foo(int)"?
Мой ответ: нет, ни в коем случае. От этого опять куча проблем и граблей.
И еще важный момент: нужно понимать, что f() — это не вызов функции без аргументов, а именно вызов с одним аргументом — (), он же void. Он потому так и называется (), не просто так. Поэтому f(void) не имеет никакого смысла, это же f (()), бессмысленная конструкция даже синтаксически.
А foo(void, int, void) принимает тупл из трех элементов, ни больше ни меньше.
Здравствуйте, Sinclair, Вы писали:
NC>>2. Функция, объявленная как foo(), эквивалентна функции foo(void), т.е. отсутствие аргументов вообще эквивалентно одному аргументу void. S>Я думаю, это лишнее.
А чем тут не подходит ваш же собственный аргумент в пользу возвращения void
Опять же, всякие ФВП приходится писать дважды — для функций, возвращающих значение, и для невозвращающих значение.
просто исправленный на "всякие ФВП приходится писать дважды — для функций, принимающих значение, и для непринимающих значение"?
Да тут дело даже не в том, что их приходится писать дважды, но и некий обобщенный код не будет рассчитан на такие случаи, и если вдруг понадобится в такой обобщенный код передать функцию, которая не принимает ничего — придется самому вводить тип Unit с одним значением и оборачивать "непринимающие" функции.
По крайней мере в подавляющим большинстве ФЯ именно так и сделано, () — это значение тина unit, единственное населяющее этот тип (не считая _|_ в ленивых языках) — это проверенное временем решение.
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
K>А чем тут не подходит ваш же собственный аргумент в пользу возвращения void K>
K>Опять же, всякие ФВП приходится писать дважды — для функций, возвращающих значение, и для невозвращающих значение.
K>просто исправленный на "всякие ФВП приходится писать дважды — для функций, принимающих значение, и для непринимающих значение"?
Тогда получается "...приходится писать бесконечное количество раз — для функций, принимающих 0 значений, 1 значение, 2 значения, и т.п."
А если ФВП способна работать с функцией, принимающей N аргументов, то она отлично справится и с N=0.
Если, конечно, логика ФВП этому не противоречит — например, каррировать функцию без аргументов уже не получится.
K>Да тут дело даже не в том, что их приходится писать дважды, но и некий обобщенный код не будет рассчитан на такие случаи, и если вдруг понадобится в такой обобщенный код передать функцию, которая не принимает ничего — придется самому вводить тип Unit с одним значением и оборачивать "непринимающие" функции. K>По крайней мере в подавляющим большинстве ФЯ именно так и сделано, () — это значение тина unit, единственное населяющее этот тип (не считая _|_ в ленивых языках) — это проверенное временем решение.
Тогда проще отказаться от функций, не принимающих значение.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Если, конечно, логика ФВП этому не противоречит — например, каррировать функцию без аргументов уже не получится. S>... S>Тогда проще отказаться от функций, не принимающих значение.
Именно так и надо поступить. Тогда и с каррированием проблем не будет: f () — вызов, f — каррированная ф-я.
Здравствуйте, Sinclair, Вы писали:
S>Тогда получается "...приходится писать бесконечное количество раз — для функций, принимающих 0 значений, 1 значение, 2 значения, и т.п." S>А если ФВП способна работать с функцией, принимающей N аргументов, то она отлично справится и с N=0.
ФВП, работающая с функцией одного аргумента a -> b отлично со всем этим справляется, несколько параметров можно определять так a -> (c -> d) или (c,d) -> b и так далее. Возращающую несколько значений — a -> (c,d) и так далее. Функцию с одним параметром, с областью определения в одно значение (а привычные функции foo() как раз такие) соответственно () -> b, с областью значений в одно значение a -> () (это всякие void foo функции). С пустой областью определений Void -> b (Void — это пустой тип, не нужно путать его с void из мейнстрим языков, который вовсе не пустой и вообще называется Unit), такие тоже находят применение см. http://hackage.haskell.org/package/void-0.6.1/docs/Data-Void.html#v:absurd
S>Тогда проще отказаться от функций, не принимающих значение.
Ну да, что такое "функция не принимающее значение" вообще не очень понятно.
'You may call it "nonsense" if you like, but I'VE heard nonsense, compared with which that would be as sensible as a dictionary!' (c) Lewis Carroll
Систематизирую организацию кортежей в различных языках. И вот какой вопрос с void возник совершенно неожиданно.
1. Если функция возвращает значение, а оно не используется — это нормально и менять это (заставлять использовать плейсхолдер для ненужного возвращаемого значения) крайне не хочется. Просто неудобно.
func foo() int { return 10;}
foo(); // теряем возвращаемое значение
_ = foo(); // так правильнее, но писать каждый раз лень!
2. По аналогии получается, если функция возвращает два и более значения (кортеж), то мы обязаны разрешить терять эти значения — иначе нарушится логика языка. Именно такое поведение реализовано например в Go.
func bar() int, float { return 10, 1.23; }
i, f = bar(); // ok
i = bar(); // ok, теряем второе значение
bar(); // ok, теряем все
3. Присваивание — обычная операция, ничем не отличающаяся от остальных. И экстраполируя поведение множественного присваивания на остальные операции, неизбежно получаем правило, что множественные операции выполняются над минимальным кортежем, участвующим в операции.
// '@' - некоторая бинарная операция
x @ 1,2,3; // выполняется x @ 1, остальное выкидываем
x,y,z @ 1; // выполняется x @ 1, остальное выкидываем
4. Поскольку void — это нуль-арный кортеж, то неизбежно получаем результат, что любые операции над void с точки зрения компилятора допустимы и просто игнорируются. Рассмотрим действия с переменной типа void:
_void_ = 1; // ok, ничего не происходит
x = _void_; // ok, ничего не происходит, значение x остается тем которое было до присваивания
5. Вообще говоря, из этого свойства следует очень интересное следствие: любые фрагменты выражений с участием переменной void следует игнорировать, а не ругаться на ошибки компиляции:
x = y * (z + _void_); // x = y*z
что не есть хорошо, так как появляется возможность например воткнуть вызов функции, возвращающей void, в выражение; если раньше компилятор ругался, то теперь он проглотит и сократит выражение так чтобы ближайщая к void операция не выполнялась.
Собственно вот. К чему приводит лень — попытка облегчить жизнь введением исключения из правила привела в конце концов к усложнению этой же жизни в самом неожиданном месте