Правила языка: строковые литералы (ссылка на статью)


Комментарии:

2023-05-22 kt

Я бы ещё добавил коэффициент повторения для текстовых констант, аналогично PL/1, например:
dcl x bit(64) static init('1'(64)b); // все единицы 
put skip list('='(50),'Начало','='(50)); // заголовок из равенств

2023-05-23 Автор сайта

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

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

2023-05-29 alextretyak

А чем не устраивает решение как в Python, а именно оператор умножения (*)?
s = 5 * '-' # равнозначно s = '-----'

2023-05-29 Автор сайта

Философия Питона предполагает принцип наименьшего удивления. Однако ожидания прикладных программистов на Питоне не совпадает с ожиданиями системных программистов на Си или Rust. Системные программисты помнят, что литера '-' соответствует числу 45. Поэтому 5 * '-' эквивалентно 5 * 45 и даёт 225. Если Вы попробуете, то убедитесь в этом.

Допустим, мы поменяем '-' на "-", чтобы иметь дело не с отдельной литерой, а со строкой, длина которой 1. Но что такое 5 умножить на число n? Это сложить 5 с самой собой n раз. Но как сложить 5 с самой собой "-" раз? Ведь "-" не является числом.

Было бы логичнее записать "-" * 5, тогда бы это значило "-" + "-" + "-" + "-" + "-". Это уже ближе к истине, если рассматривать операцию «+» как склейку строк. Но в любом случае надо 7 раз отмерить. Пока что острой необходимости в этом в первой версии нет, торопиться не будем.

Посмотрел раздел «Строковые литералы» на Вашем сайте, там эта тема не затронута :(

2023-05-31 alextretyak

литера '-' соответствует числу 45. Поэтому 5 * '-' эквивалентно 5 * 45 и даёт 225.
Да, верно.
А я уже и забыл про такую особенность языка Си.
Но дело в том, что на практике такая "особенность" никогда не требуется {}, и связана скорее всего с тем, что в Си было невозможно ввести [отдельный/]полноценный тип для символа (который было бы запрещено умножать на число), поэтому тип char по факту совпадает с типом int8_t. Такую "фичу" я бы смело добавил в «Признаки устаревшего языка».

не совпадает с ожиданиями системных программистов на Си или Rust. ... Если Вы попробуете, то убедитесь в этом.
Ну вот в Rust, кстати, такое не прокатывает: умножение символьного литерала на число приводит к ошибке компиляции.
А вот в Java и C#, на удивление, оставлено поведение Си (могли бы хоть warning\предупреждение выдавать в таких случаях).

Посмотрел раздел «Строковые литералы» на Вашем сайте, там эта тема не затронута :(
Не затронута, т.к. конструкции вида 5 * "-" не являются строковыми литералами [также как 5 * 45 не является целочисленным литералом, хотя и является целочисленным константным выражением (integral constant expression) и может использоваться везде, где могут использоваться только целочисленные константы/литералы].
В 11l, также как в Python, можно умножать число на строку (в т.ч. на строковый литерал).

И хочу добавить, что разделение символьных и строковых литералов мне категорически не нравится {}. Поэтому в 11l такого разделения нет, а есть "сырые" (например
‘\’
) и "не сырые" (например
"\\"
[что равнозначно
‘\’
]
) строковые литералы, фактический тип которых (Строка либо Символ) определяется при компиляции в зависимости от контекста использования этого литерала:
пер м1 = [‘а’, ‘б’, ‘в’] // это массив символов
пер м2 = [‘а’, ‘бб’, ‘в’] // а это массив строк

пер с = ‘-’ // это строка; если требуется символ, то следует писать пер с = Символ(‘-’)
вывод(‘-’.код) // выведет 45 (в таком контексте ‘-’ считается символом, а не строкой)

пер м3 = [Строка(‘а’), ‘б’, ‘в’] // это массив строк

Вообще, я подумываю над тем, чтобы строковые литералы, состоящие всего из одного символа [т.е. длина которых равна 1], всегда преобразовывались в тип Символ [в таком случае вместо пер с = ‘-’ в примере выше придётся писать пер с = Строка(‘-’) или Строка с = ‘-’], но для принятия [однозначного/]окончательного решения мне пока ещё не хватает данных. [{}]

Пока что острой необходимости в этом в первой версии нет
Да, умножение числа на строку требуется достаточно редко, но вот аналогичное умножение булевой переменной/выражения на строку нужно намного чаще (например, в коде моей реализации pqmarkup оно встречается 15 раз).

2023-05-31 Автор сайта

тип char по факту совпадает с типом int8_t. Такую "фичу" я бы смело добавил в «Признаки устаревшего языка».
Было бы интересно услышать от Вас аргументы в пользу этого. Если такие типы эквивалентны, то в чём становится хуже?

А вот в Java и C#, на удивление, оставлено поведение Си
За автоматическое приведение целых типов с меньшей разрядностью к целым типам с большей разрядностью, которое не искажает значения, есть серьёзный аргумент. Они делают код короче, освобождают от лишних несущественных деталей. Другое дело, если автоматические приведения искажают значения. Тогда да, с этим надо бороться. Если чётко провести границу между искажающими и неискажающими преобразованиями, то польза в виде краткости кода вполне очевидна. А какие аргументы против Вы видите?

Посмотрел раздел «Строковые литералы» на Вашем сайте, там эта тема не затронута :(
Не затронута, т.к. конструкции вида 5 * "-" не являются строковыми литералами
Запутался в терминологии, имел в виду символьные литералы, которые искал в разделе «Строковые литералы». Выше на этой странице описаны оба вида литералов, того же ожидал от вашего сайта. Но не нашёл у Вас описания именно символьных литералов.

разделение символьных и строковых литералов мне категорически не нравится
Ну так объединяйте их. Есть же языки, где строка единичной длины эквивалентна строковому литералу. Да, небольшая потеря эффективности процессора, зато небольшое увеличение эффективности программиста. Множество языков придерживаются такой философии. Если Вы позиционируете свой язык в этой же нише, то почему бы и нет?

пер м1 = [‘а’, ‘б’, ‘в’] // это массив символов
. . .
пер с = ‘-’ // это строка; если требуется символ, то следует писать пер с = Символ(‘-’)
По какой причине в первой строке делается вывод, что это массив символов? Там же не написано
Символ(‘<литера>’)
.

чтобы строковые литералы, состоящие всего из одного символа [т.е. длина которых равна 1], всегда преобразовывались в тип Символ
Тут ещё надо определиться: эта длина, равная 1, должна быть известна во время компиляции, чтобы принимать решение именно в это время? Или же во время исполнения тоже.

умножение числа на строку требуется достаточно редко
Ещё раз хочу расставить точки над «ё». Если 5 умножаем на строку, то множимое тут — 5, а строка — множитель. Умножается (то есть размножается!) пятёрка, а не строка. Логичнее, если строка бывает множимым, то есть размножаемым. Если умножаем множимое на множитель, то размножается первое, а не второе. Тот случай, когда перемена мест имеет значение.

умножение булевой переменной/выражения на строку нужно намного чаще
Мне кажется, что оператор Элвиса вполне подходящ в таких случаях. Кстати, как Вы относитесь к автоматическим преобразованиям значений в булев тип и обратно? Типа 0 — «ложь», всё остальное — «истина». И наоборот, «ложь» автоматически превращается в 0, вот только во что превращается «истина» и почему? И следует ли считать такие автоматические преобразования свидетельством устаревания языка?

сложение символьного литерала с числом имеет практическое применение (например: '0' + i для получения символа-цифры i [при этом i должно быть от 0 до 9] или 'a' + n для получения n-ной буквы латинского алфавита).
Вам теперь будет над чем подумать, как совместить и то, и это: отсутствие символьных литералов с операциями над ними :)

2023-06-01 MihalNik

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

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

2023-06-02 alextretyak

тип char по факту совпадает с типом int8_t. Такую "фичу" я бы смело добавил в «Признаки устаревшего языка».
Было бы интересно услышать от Вас аргументы в пользу этого. Если такие типы эквивалентны, то в чём становится хуже?
Во-первых, в том, что компилятор позволяет писать всякие глупости вроде сложения, умножения или даже деления символьных литералов или символьных переменных (аналогично, ни один язык программирования не позволяет производить такие операции над указателями, ведь они также не имеют смысла).
Во-вторых, специальный тип для символа позволяет снабдить его специфическими методами, такими как
is_digit()
или
lowercase()
, или даже операторами (например, операция вычитания для символов вполне допустима: если символ
c
является строчной латинской буквой, то
c - 'a'
даст номер этой буквы в алфавите [кстати, для указателей операция вычитания также допустима/применима]; или операция сложения — только не двух символов, а сложение символа и числа [при этом тип результата разумно сделать символом, а не числом, как собственно уже и сделано в Kotlin]).
И в-третьих, иногда требуется массив 8-разрядных целых чисел {}, и хочется, чтобы это было отражено средствами языка явно. Для чего? Ну, например, чтобы имея массив 8-разрядных целых при выводе его в консоль [либо при просмотре его в отладчике] он выводился/отображался именно как массив чисел, а не массив символов с непонятно какими кодами. Вот тут человек жалуется, что ему нужно однобайтовое целое число, но проблема в том, что
std::cout
выводит его как символ, и приходится кастить 8-разрядное число к short или int при его выводе.

А вот в Java и C#, на удивление, оставлено поведение Си
За автоматическое приведение целых типов с меньшей разрядностью к целым типам с большей разрядностью, которое не искажает значения, есть серьёзный аргумент. ... А какие аргументы против Вы видите?
Нет, против такого автоматического приведения я ничего не имею. Я против трактовки символов/‘символьных литералов’ как целых чисел малой разрядности. И если даже ради совместимости с Си в Java и C# оставили такой архаизм, то могли бы отдельно проверить/обработать случай умножения символьного литерала на число.
А в Rust, между прочим, char является отдельным типом [причём 4-х байтовым], который автоматически не приводится ни к какому целочисленному типу. В этом отношении, я согласен с разработчиками Rust, а вот с тем, что в Rust отсутствует автоматическое приведение целых типов с меньшей разрядностью к целым типам с большей разрядностью, я не согласен [также как и Вы, насколько я понимаю].

Но не нашёл у Вас описания именно символьных литералов.
Таких литералов в 11l нет, и описания, соответственно, тоже. :)(:
Но описание правил автоматического преобразования строковых литералов к типу Символ, конечно, стоит добавить в документацию языка.

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

Ну так объединяйте их. Есть же языки, где строка единичной длины эквивалентна строковому литералу.
Не совсем понятно, что вы имели в виду. Может «строка единичной длины эквивалентна символьному литералу»?
И о каких языках речь? [Я что-то навскидку не могу припомнить такого поведения {}, хотя языков повидал немало.]

Да, небольшая потеря эффективности процессора, зато небольшое увеличение эффективности программиста.
Я считаю, что в данном случае можно добиться увеличения эффективности программиста без потери производительности. В наиболее общем виде можно сформулировать такое правило: если строковый литерал единичной длины можно рассматривать как символьный литерал [ну т.е. если в этом случае не будет ошибок компиляции], тогда его нужно рассматривать именно как символьный литерал (и при необходимости приводить к типу Символ), иначе он приводится к типу Строка [но опять же при необходимости: например при передаче строкового литерала в качестве аргумента функции совсем не обязательно создавать при вызове этой функции [в run-time] объект типа Строка, можно создать некий аналог объекта
std::string_view
, который состоит лишь из указателя и длины строки, и передавать в функцию именно его]
. Вот пример применения такого правила: {}

По какой причине в первой строке делается вывод, что это массив символов? Там же не написано
Символ(‘<литера>’)
.
По причине того, что так захардкодено в компиляторе. :)(:
[Ну т.е. буквально так и делается: при компиляции проверяются все элементы массива, и если все они являются строковыми литералами единичной длины, значит это массив символов.]
После фразы «фактический тип (Строка либо Символ) определяется при компиляции в зависимости от контекста использования этого литерала» собственно и приводятся примеры кода, в каких случаях строковые литералы единичной длины считаются символьными, а в каких нет.

Тут ещё надо определиться: эта длина, равная 1, должна быть известна во время компиляции, чтобы принимать решение именно в это время? Или же во время исполнения тоже.
Я говорю в основном про этап компиляции, т.к. основная цель — обойтись без разделения на строковые и символьные литералы.
А проверку на единичность длины строк во время исполнения лучше вынести в реализацию конкретных функций, где такая оптимизация имеет смысл (к примеру, та же
s.find(some_string)
в случае когда длина
some_string
равна 1, получит некоторое повышение производительности за счёт избавления от вложенного цикла по длине строки
some_string
— в этом случае вызов
s.find(some_string)
будет равнозначен
s.find(some_string[0])
).

Ещё раз хочу расставить точки над «ё». Если 5 умножаем на строку, то множимое тут — 5, а строка — множитель. Умножается (то есть размножается!) пятёрка, а не строка. ... Если умножаем множимое на множитель, то размножается первое, а не второе. Тот случай, когда перемена мест имеет значение.
Юрий, что вы мутите воду, ну не имеет перемена мест тут значения. :)(:
А то иначе, следуя такой логике, получится, что 5x всегда означает сложить 5 с самой собой x раз. Но если x = 1.5 [или вообще, если x — это комплексное число!], то как сложить 5 с самой собой 1.5 раз? А вот сложить 1.5 с самой собой 5 раз прекрасно получается.

Мне кажется, что оператор Элвиса вполне подходящ в таких случаях.
Нет, Элвис же несколько про другое [ведь Элвис — это урезанный тернарный оператор
?:
, а здесь нужен полноценный]
.
Т.е. речь о таком коде:
outfile.write("<table" + ' style="display: inline"'*is_inline + ">\n")
Без оператора умножения его пришлось бы писать так [на Python]:
outfile.write("<table" + (' style="display: inline"' if is_inline else '') + ">\n")
Или так на C++:
outfile.write("<table"s + (is_inline ? " style=\"display: inline\"" : "") + ">\n")
Вроде разница и небольшая, но читаемость кода с оператором умножения всё-таки немного лучше.

Кстати, как Вы относитесь к автоматическим преобразованиям значений в булев тип и обратно?
Отрицательно. За редкими исключениями (например, тип
re:Match
в 11l имеет такое автоматическое преобразование, впрочем, не исключено, что в будущих версиях языка я от него откажусь: функция
RegEx.match()
может возвращать не
re:Match
, а
re:Match?
, и тогда вместо
if ...match(...) {...}
нужно будет писать
if ...match(...) != null {...}
).

Типа 0 — «ложь», всё остальное — «истина».
Вот это вот «всё остальное» — это что? Это то, что не равно 0. Вот так, я считаю, и следует всегда писать:
if int_var != 0 {...}
, а просто
if int_var {...}
должно считаться ошибкой. {}

И наоборот, «ложь» автоматически превращается в 0, вот только во что превращается «истина» и почему?
Истину логично превращать в 1, т.к. именно такое значение на аппаратном уровне имеет истина\true в булевом типе {} во всех реализациях всех языков программирования, насколько мне известно. [Что-то слышал про все единицы (речь про двоичное представление, разумеется), но в каком конкретно языке или в какой реализации такое, сказать не могу.]

И следует ли считать такие автоматические преобразования свидетельством устаревания языка?
По моему мнению, да.
Даже в древнем Java, несмотря на его совместимость с Си во многих моментах [включая целочисленный тип char], числа не преобразуются автоматически в булев тип и обратно (и написать
if (0) ...
или
if (1) ...
в Java нельзя).

Вам теперь будет над чем подумать, как совместить и то, и это: отсутствие символьных литералов с операциями над ними :)
Ну такое отсутствие весьма условно. Я бы назвал это «автоматическое определение вида литерала при компиляции», или «строкосимвольные литералы».
Но даже при отсутствии символьных литералов ничто не мешает писать
"a"[0]
.
А насчёт операций над символьными литералами: в 11l уже есть всё что нужно. Вот [слегка модифицированная] строчка из кода реализации шифра Цезаря (приводится в примерах на главной странице 11l-lang.org/ru):
r[L.index] = Char(code' ‘a’.code + (c.code - ‘a’.code + key) % 26)
Хотя, признаю, что на Си это записывается короче:
r[L_index] = 'a' + (c - 'a' + key) % 26;

2023-06-01 Автор сайта

Отсутствие удобного размножения строк на уровне языка в консольных приложениях выливается в полотна литералов псевдографики и отступов.
Но ситуация не столь катастрофична. В языке могут отсутствовать строковые операции времени компиляции, но скорее всего есть строковые функции, которые при константных аргументах вычисляемы при компиляции. Например, функции
str_repeat
(PHP) и
replicate
(Clipper) тоже размножают строки.

Кстати, в PHP
5 * '-'
,
'-' * 5
,
5 * "-"
и
"-" * 5
выдают одинаковый результат:
0
. Везде разброд и шатания. Се ля ви.

2023-06-02 alextretyak

Кстати, в PHP
5 * '-'
,
'-' * 5
,
5 * "-"
и
"-" * 5
выдают одинаковый результат:
0
.
Это смотря в какой версии PHP. В PHP 8 такое умножение является ошибкой. А в версии 7.1.0 и выше [до 8.0] выдаётся предупреждение.
Вот хороший сайт, благодаря которому я это выяснил: https://onlinephp.io (там можно раскрыть группу ‘PHP Versions’ и поставить галочки на множестве версий PHP, чтобы проверить поведение одного и того же кода на разных версиях).

Везде разброд и шатания.
Пока да. Но я вижу довольно устойчивую тенденцию движения/эволюции языков программирования в сторону некоторого консенсуса. Наиболее удачные решения из одних языков постепенно проникают в другие {}. Ошибки проектирования постепенно устраняют в новых версиях (хорошо заметно на том же PHP). Правда, при этом, в новые версии языков добавляют много ненужного хлама... но это уже другая история.

2023-06-04 Автор сайта

Истину логично превращать в 1
Машинная операция NOT 0 даст -1. То есть «не ноль» равно -1.

По моему мнению, да... числа не преобразуются автоматически в булев тип и обратно (и написать
if (0) ...
или
if (1) ...
в Java нельзя).
Мне тоже не нравится автоматическое приведение числа к булеву типу. Но хочется отключить эмоции и руководствоваться только здравым смыслом. Нельзя не признать, что с автоматическим приведением код становится короче. Краткость кода — один из признаков, который характеризует уровень языка: высокий он или низкий. Ведь чем больше деталей приходится описывать, тем меньший уровень абстракций. Не всегда опущение деталей полезно (например, в Фортране когда-то тип переменной определялся по первой букве её имени), но чаще всё-таки так.

Кстати, такая философская дилемма. Мы тут обсуждаем автоматические приведения типов, а ведь существует ещё странная (с точки зрения математики) практика применять побитовые операции к объектам целого типа. В математике нельзя применить, к примеру, операцию «исключающее или» к числу. А в Си — пожалуйста. По идее надо разделить числа и битовые поля: к первым надо применять арифметические операции, а ко вторым — побитовые. Правда, это тоже сделает код менее кратким.

2023-06-06 alextretyak

Истину логично превращать в 1
Машинная операция NOT 0 даст -1. То есть «не ноль» равно -1.
Машинной операции NOT соответствует сишный оператор ~, который для булевых переменных/выражений не используется — для них есть оператор !, и int(!0) даёт таки единичку.

а ведь существует ещё странная (с точки зрения математики) практика применять побитовые операции к объектам целого типа. В математике нельзя применить, к примеру, операцию «исключающее или» к числу. А в Си — пожалуйста.
Думаю, тут математика не причём, Си просто отображает машинные инструкции на операторы языка. Ну т.е. если целочисленные переменные соответствуют целочисленным регистрам процессора, то инструкции add соответствует оператор +, инструкции xor — оператор ^ и т.д. Не зря же Си иногда называют высокоуровневым ассемблером.

2023-06-08 Автор сайта

Хочу спросить: а как в Вашем языке определяется длина строк? Эта величина хранится или строка заканчивается нулём?

2023-06-10 alextretyak

В текущей реализации класс строк основан на std::u16string, который хранит длину строки, и одновременно хранимая строка завершается нулём [такое поведение присуще если не всем реализациям STL, то большинству, т.к. это повышает производительность метода c_str(), который по сути становится эквивалентен методу data()]. Длину хранить нужно обязательно — это залог эффективной работы со строками (включая быструю проверку выхода индекса символа за границу строки в операторе [], конкатенацию строк {}, получение последнего символа строки или функции вроде ends_with). А завершающий ноль полезен для вызова функций WinAPI, которым требуется передать строку в качестве аргумента.