Комментарии 56
Для C++ еще есть известный пазл: константный указатель/указатель на константу
int *const p1
int const* p2
const int* p3
Существует простое правило:
const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке. В этом случае, очевидно, модифицирует то, что прямо после.
И сразу легко понять что
это константный указатель на указатель на константный int.
const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке. В этом случае, очевидно, модифицирует то, что прямо после.
И сразу легко понять что
const int ** const p4
это константный указатель на указатель на константный int.
Существует простое правило:
const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке.
Хаха, это простое правило в стиле C++, а в стиле C правило звучит так «надо прочитать объявление в обратную сторону».
const int *const p; // p - это константный указатель на int костантный
const int *const *p; // p - это указатель на константный указатель на int костантный
const int **const **p; // p - это указатель на указатель на константный указатель на указатель на int константный
ЕМНИП синтаксис объявлений в C был сделан так, как удобнее разбирать компилятору. В Паскале же наоборот, как удобно читать человеку, за счет усложнения компилятора. В Go, видимо, нашли некий компромисс. :)
Как раз наоборот: когда компилятор C++ строит AST и встречает int name..., он не знаете что будет дальше — объявление переменной или функции, поэтому эту информацию надо или запоминать где-то или возвращаться назад по исходнику. К тому же для языков с подобными грамматиками сложнее программировать восстановление после сбоев во время синтаксического анализа. Для паскаля как раз проще, для него отлично подходит обычный леворекурсивный парсер без наворотов.
Сорри за оффтоп, но плиз, добавьте мягкий знак в слово «находитЬся» в заголовке.
Синтаксис объявления переменных в Си меня вполне устраивает, и я не вижу в нем ничего непонятного; то, что объявление совмещено с выражениями тоже очень удачно.
А вот для функций я бы предпочел ключевое слово func вначале, а возвращаемый тип после агрументов
принцип очень простой: сначала пишем существующее, затем новое. То есть сначала пишем имя сущности, которая нам известна (ключевое слово или имя типа), а затем вводим новый идентификатор. Лично мне так гораздо понятнее.
Ключевые слова var и let, повсеместно используемые в новых языках (и не только в Go) для объявления переменных, очень удобны для компилятора. Они снимают любые неоднозачности, связанные с разбором: после них может быть только объявление переменных.
Удобны ли они для человека? Думаю, кому как, мне не очень. Но это дело привычки.
А вот объявление функций с ключевого слова было бы действительно удобно — по общему принципу с объявлением структур, классов, перечислений и т.д. Решалась бы путаница с указателями на функции. Упростилась бы работа компиляторов и IDE. Искать объявления функций в коде стало бы легче. Упростилась бы реализация объявления вложенных функций (напомню, еще в Паскале они были, а в современном С++ есть только частный случай в виде лямбд). Появились бы интересные дополнительные возможности: введение имен возвращаемых значений, введение специальных ключевых слов для специальных функций, удобный синтаксис для возврата сразу нескольких возвращаемых значений и т.д.
А вот для функций я бы предпочел ключевое слово func вначале, а возвращаемый тип после агрументов
func foo(int x) int
принцип очень простой: сначала пишем существующее, затем новое. То есть сначала пишем имя сущности, которая нам известна (ключевое слово или имя типа), а затем вводим новый идентификатор. Лично мне так гораздо понятнее.
Ключевые слова var и let, повсеместно используемые в новых языках (и не только в Go) для объявления переменных, очень удобны для компилятора. Они снимают любые неоднозачности, связанные с разбором: после них может быть только объявление переменных.
Удобны ли они для человека? Думаю, кому как, мне не очень. Но это дело привычки.
А вот объявление функций с ключевого слова было бы действительно удобно — по общему принципу с объявлением структур, классов, перечислений и т.д. Решалась бы путаница с указателями на функции. Упростилась бы работа компиляторов и IDE. Искать объявления функций в коде стало бы легче. Упростилась бы реализация объявления вложенных функций (напомню, еще в Паскале они были, а в современном С++ есть только частный случай в виде лямбд). Появились бы интересные дополнительные возможности: введение имен возвращаемых значений, введение специальных ключевых слов для специальных функций, удобный синтаксис для возврата сразу нескольких возвращаемых значений и т.д.
собственно в языке Rust приблизительно так и сделано:
doc.rust-lang.org/book/functions.html
doc.rust-lang.org/book/functions.html
Видать комитету тоже так удобнее:
;-)
Для особых ценителей можно:
auto (*cb1)(int) -> int;
auto proc(int x) -> int
{
return 31337;
}
;-)
Для особых ценителей можно:
#define func auto
func proc(int x) -> int;
О нет. Это сделано нифига не для удобства. Просто в шаблонных функциях так бывает, что тип результата зависит от типа параметров — и тогда его описать до имени функции никак не получится!
А так да — можно использовать вполне и без шаблонов.
А так да — можно использовать вполне и без шаблонов.
Да как бы да. Я прочитал свой пост, пока думал как лучше переписать — время вышло. Махнул рукой — кому нужно, тот поймёт :)
Удобство использования стало как бы бонусом, описывать указатели на функции возвращающие функции стало удобнее
auto (*func_ptr)(int) ->
auto (*)(float, int) ->
int (*)()
Это проблема исключительно парсера С++. В C# прекрасно работает так:
То есть T используется еще до указания, что тип-параметр.
IEnumerable<T> Where<T>(Funct<T,bool> predicate)
То есть T используется еще до указания, что тип-параметр.
Не тот случай, в С++ выражение по типу
Тоже будут работать без всякого нового объявления. Новый тип объявления нужен в случае когда шаблонный тип один а возвращается совершенной другой. Например.
Можно конечно извернутся и слепить нечто такое
но это не совсем красиво, да и не уверен что будет работать везде и всегда.
Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.
template<class T>
IEnumerable<T> Where(Funct<T,bool> predicate)
Тоже будут работать без всякого нового объявления. Новый тип объявления нужен в случае когда шаблонный тип один а возвращается совершенной другой. Например.
struct A{};
struct B
{
A func();
};
template<class T>
auto Func(T& _val) -> decltype(_val.func());
Можно конечно извернутся и слепить нечто такое
template<class T>
decltype(((T*)(0))->func()) Func(T& _val)
но это не совсем красиво, да и не уверен что будет работать везде и всегда.
Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.
Ничего что template указывается до любого объявления? Причем это сделано специально чтобы помочь парсеру. Не забывайте, что С++ использует LL парсер. А LL парсер хорошо работает когда смысл написанного правее зависит от того что написано левее. Поэтому и типы слева, и template писать надо. Можно было бы отказаться, но это бы усложнио парсинг и, скорее всего, увеличило бы время компиляции.
Это к вопросу парсинга не имеет никакого отношения от слова вообще.
Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.
Это к вопросу парсинга не имеет никакого отношения от слова вообще.
Это к вопросу парсинга не имеет никакого отношения от слова вообще.имеет причём довольно-таки прямое. Так как у нас тип в дженериках предназначен для всяко-разных проверок и, в общем, не вляет на генерируемый код, то кроме типов в угловых скобках ничего указать нельзя. В C++ — можно, откуда и все беды.
Парсинг строит синтаксическое дерево, ему по большому счету без разницы как потом это дерево обрабатывается. Вы вообще не о том говорите.
Насколько я понял, речь о том, что парсить вот такое в качестве возвращаемого типа в C++ — норма:
decltype(decltype(_val.func())::n + 10)::result_type
, а в C# — нетНужно просто уметь парсить нечто зависящее от типа — а для этого нужно уметь понимать где у нас типы, а где — нетипы.
Я считал что этот пример всем, кто берётся рассуждать о тонкостях C++ известен.
Он просто очень выпукло показывают проблему во всей красе: в зависимости от опций компилятора у вас может быть по-разному построено синтаксическое дерево! Не выбраться другая функция и по другому посчитаться константа, нет — по разному будет именно всё разобрано. Без всяких ifdef'ов или define'ов (они-то вообще до компилятора отрабатывают и как бы «не в счёт»).
Я считал что этот пример всем, кто берётся рассуждать о тонкостях C++ известен.
Он просто очень выпукло показывают проблему во всей красе: в зависимости от опций компилятора у вас может быть по-разному построено синтаксическое дерево! Не выбраться другая функция и по другому посчитаться константа, нет — по разному будет именно всё разобрано. Без всяких ifdef'ов или define'ов (они-то вообще до компилятора отрабатывают и как бы «не в счёт»).
Это вы не о том говорите. Возьмите всем известный пример:
В C# подобное невозможно потому что дженерики параметризуются только типами.
int x = confusing<sizeof(x)>::q < 3 > (2);
Так вот в зависимости от того явзяется у вас q
типом или переменной у вас будет построено разное синтаксическое дерево. Хабрапарсер выбирает один вариант (тот, который ему больше нравится), но там есть ещё и второй, где вначале считается confusing<sizeof(x)>::q < 3
и вот уже это сравнивается с двойкой. В C# подобное невозможно потому что дженерики параметризуются только типами.
Та же самое может быть в C#. Вместо имени типа может оказаться переменная и выражение
IEnumerable может быть воспрнято как (IEnumerable < T ) > что-то там, где IEnumerable и T — переменные. Но у C# грамматика более стройна и не допускает таких ошибок, после имени типа выражение не напишешь, нужно тип в скобки брать, что делает парсинг однозначным,
Например если для C++ запретить приведения типов в операторной форме, то подобной проблемы не возникнет. Да и многих других проблем можно избежать если поправить синтаксис, но из-за совместимости этого не делают.
IEnumerable может быть воспрнято как (IEnumerable < T ) > что-то там, где IEnumerable и T — переменные. Но у C# грамматика более стройна и не допускает таких ошибок, после имени типа выражение не напишешь, нужно тип в скобки брать, что делает парсинг однозначным,
Например если для C++ запретить приведения типов в операторной форме, то подобной проблемы не возникнет. Да и многих других проблем можно избежать если поправить синтаксис, но из-за совместимости этого не делают.
Кому трудно привыкнуть писать в GO var, может использовать оператор :=
еще раз… в си тип размазан по определению, за исключением простых типов: в массиве — тип и размерность, в функции — возвращаемого и аргументов, указатели — привязанно к идентификатору, а не типу… а модификаторы… кто во что горазд.
В случае с Go некоторые примеры кода выглядят так, будто их скопировали из описания Компонентного Паскаля.
Да и зачем, спрашивается, тащить в новый язык полувековые дефекты и костыли, если можно взять что-то более продуманное, удобное и эффективное?
Да и зачем, спрашивается, тащить в новый язык полувековые дефекты и костыли, если можно взять что-то более продуманное, удобное и эффективное?
Ну я бы не сказал что это полувековые костыли:) Да и кто сказал что в компонентном паскале костыли?
Костыли проявляются после более глубокого изучения языка, а подход типа «раз похоже на компонентный паскаль — значит костыли» совершенно неправильный.
Вот например кто нибудь знает, что в С/С++ (и также в C#/Java) есть дефект с приоритетом операций? Сможете назвать и обосновать?
Костыли проявляются после более глубокого изучения языка, а подход типа «раз похоже на компонентный паскаль — значит костыли» совершенно неправильный.
Вот например кто нибудь знает, что в С/С++ (и также в C#/Java) есть дефект с приоритетом операций? Сможете назвать и обосновать?
Костыли — это про сишный синтасис, причём судя по некоторым статьям — число дефектов в том же С++ год от года только растёт.
& и |?
<<?
<<?
Операторы сравнения < <= > >= == != имеет приоритет выше чем битовые операции & | ^
В результате например вот такая вполне логичная конструкция
без скобок вокруг «x & 0x07» некорректна.
В результате например вот такая вполне логичная конструкция
if(x & 0x07 > 4)
без скобок вокруг «x & 0x07» некорректна.
f func(func(int,int) int, int) int
Вот зачем было в go изобретать велосипед, если уже давно в ML-образных языках используется синтаксис вроде
f : ((int, int) -> int, int) -> int
ИМХО, если хотелось сделать как привычнее, надо было брать синтаксис C. А тут хотели как лучше, явно посмотрели в сторону функциональных языков (да и не только их, любая статья по теории типов пестрит подобной нотацией), но почему-то не захотели вводить двоеточие и стрелочку.
А зачем двоеточия и стрелочки, кроме как для красоты?
Принято так, вроде
Да и, имхо, нагляднее, чем func. Хотя, может, и дело привычки.
Да и, имхо, нагляднее, чем func. Хотя, может, и дело привычки.
А, ну в ML-языках двоеточие потому, что запись через пробел (
f x
) — это применение функции (f(x)
)Перепишем вашу сложную функцию на Go
Даже если взять более сложную функцию из той же статьи.
Так что дело не в бобине.
f func(func(int,int) int, int) int
с указанием типа слеваint f func(int func(int,int), int)
и увидим, что и так нет сложностей с прочтением.Даже если взять более сложную функцию из той же статьи.
f func(func(int,int) int, int) func(int, int) int
(int func(int, int)) f func(int func(int,int), int)
Так что дело не в бобине.
int f func(int func(int,int), int)Есть проблема с прочтением. В выделенном мной месте непонятно, что следует за int. Анализатору надо заглянуть вперёд, понять, что там func и только потом понять, что это аргумент-функция, а не аргумент-число. Так-то.
Вот такие имеет отличия определение переменных в языках семейства C и Go. Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа.
Очень «тонкий» намек на преимущества Go, по сути бред.
Сложность типов в C вызвана тремя компонентами:
1) Указателями и повсеместным их использованием
2) Отсутствием нормального описания функционального типа
3) const
В C# и Java всего этого нет, поэтому и проблем с описанием типов нет. В С++ только const иногда мешает, да и то не часто. Так что никакого разительного преимущества Go перед современными языками нету.
Кстати писать тип после придумали в ML, лет за 40 до изобретения Go. Там даже еще дальше пошли — аннотации типов применяются не только к объявлениям, но и в выражениями. Это в сочетании с автоматическим выводом типов добавляет удобства в разы.
Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа. И они построены на парадигмах ООП и не используют (или практически не используют) передачу указателей на функции, все это нам заменили классы. Недостатки, которые выявляются у определения типа переменной слева, улетучиваются при использовании ООП.
Вы, вырвали слова из контекста и все сказано в следующих 2-х предложения.
К слову, в GO указатели используются повсеместно. И проблем с типами нет :)
Написание типа после имени — это необходимость, которую осознали слишком поздно. Такой подход, к примеру, позволяет компилятору выводить тип возвращаемого значения из типа аргументов функции. В C++11 даже (в очередной раз) ввели специальный синтаксис для этого:
Написать
Кроме того, как уже упоминалось, такой подход существенно упрощает как компилятор, так и разработку инструментов. Вспомним ключевое слово
Если бы было ключевое слово для объявления переменных, такой проблемы бы не возникло:
Языку 30 лет, а мой текстовый редактор, к примеру, зачастую не может распознать объявления переменных. Если бы было слово var, риск ошибки был бы практически нулевой.
Ну и мне лично кажется, что подход с объявлением типа после имени делает опциональный вывод типов более логичным.
template <class U, class V>
auto add(U const& u, V const& v) -> decltype(u + v) {
return u + v;
}
Написать
decltype(u+v)
вместо auto
нет возможности — там компилятору ещё не видны имена (и соответствующие типы) u и v.Кроме того, как уже упоминалось, такой подход существенно упрощает как компилятор, так и разработку инструментов. Вспомним ключевое слово
typename
из C++:template <class Iterator>
void doSomething(Iterator it) {
// Тут необходимо слово typename, чтобы компилятор мог понять, что вы хотите:
// 1) объявить переменную v с типом указателя на Iterator::value_type;
// 2) вызвать Itarator::value_type.operator*(v), где v нужно взять из окружающего контекста.
typename Iterator::value_type * v;
}
Если бы было ключевое слово для объявления переменных, такой проблемы бы не возникло:
var v: *Iterator::value_type; // объявление переменной
Iterator::value_type * v; // умножение
Языку 30 лет, а мой текстовый редактор, к примеру, зачастую не может распознать объявления переменных. Если бы было слово var, риск ошибки был бы практически нулевой.
Ну и мне лично кажется, что подход с объявлением типа после имени делает опциональный вывод типов более логичным.
// Если тип опустить, то компилятор его выводит, логично.
// К тому же, имена всегда выровнены по левому краю.
var x = 5;
var y: int = 5;
var z: MyType = init();
// Хм... Ок...
auto x = 5;
int y = 5;
MyType z = init();
Точно, тот самый знаменитый пример:)
X * Y; // что это - умножение или объявление указателя?
НЛО прилетело и опубликовало эту надпись здесь
int (*(*fp)(int (*)(int, int), int))(int, int)
пара typedef'ов обычно решает проблему нечитаемости
Пара typedef'ов обычно решает проблему нечитаемостиАга, конечно. Особенно если выражение встречается не в коде, а в документации. Пример с сигналом — он же не из воздуха взялся, а из официальной документации.
Хорошо хоть названия параметров сохранились! По синтаксису они там не нужны, но выкидывание
fp
превратит выражение в паззл:void (*signal(int, void (*)(int)))(int);
Вообще же — писать можно на чём угодно, хоть на брайнфаке, но то, что у вас выражение в полстроки невозможно понять и требуется сложный анализ производить — это же ненормально…Но вообще ворос: справа или слева не очень приниципиален. Можно слева (Java), можно справа (Go), главное — не со всех сторон сразу (как в C/C++). Описания переменных — это одно из мест в C/C++, которые сделаны очевидно плохо.
Спасибо за статью. Ещё, когда тип пишется справа, для меня это удачно укладывается в математическое представление типа как множества принадлежащих ему объектов.
var x int // x принадлежит множеству целых чисел
var p *int // p принадлежит множеству указателей на объекты, принадлежащие множеству целых чисел
var a [3]int // a принадлежит множеству трёхэлементных массивов объектов, принадлежащих множеству целых чисел
Где находиться типу: справа или слева?Нигде. Типы должны выводиться автоматически. Если тип всё же нужно указать явно, то он должен быть указан для всего выражения.
Это очень субъективно. Мне например намного привычнее Сишный способ, первое время я вообще не понимал что там за мешанина в Go-коде.
Понятие удобства в данном случае очень сильно зависит от того, какой у человека бэкграунд. Единственный объективный способ сравнения — это посадить 100 программистов, которые никогда не писали на языках со строгой типизацией, и замерить сколько времени их мозг тратит на разбор си-образного и го-образного способов объявления типов. Все остальное — субъективщина и холиворы.
Понятие удобства в данном случае очень сильно зависит от того, какой у человека бэкграунд. Единственный объективный способ сравнения — это посадить 100 программистов, которые никогда не писали на языках со строгой типизацией, и замерить сколько времени их мозг тратит на разбор си-образного и го-образного способов объявления типов. Все остальное — субъективщина и холиворы.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Где находиться типу: справа или слева?