Комментарии 35
Эх, а мне больше unsafe enum были по душе...
Мне тоже — меньше ключевых слов, да и такое применение unsafe
вполне логично.
В Rust они остаются и в релизе, впрочем, аналогичное поведение можно получить при помощи макроса debug_assert!.
Тут стоит заметить, что в D есть конструкция debug(name) вида:
debug( anyDebugName ) {
// any code
}
Она вырезает код, если при компиляции не указан соответствующий флаг: --debug=anyDebugName
Аналогично, есть конструкция version(name), позволяющая варьировать даже релизный код в зависимости от платформы, архитектуры, компилятора или опять же вручную заданного условия.
В Rust, с одной стороны, эти проблемы невозможны из-за необходимости явного приведения типов. С другой стороны, оператор += для строк всё-таки не реализован.
Всё же сложение и соединение — разные операции. Хорошо это видно на примере векторов (массивов чисел), которые можно соединять, а можно поэлементно складывать. Но для векторов необходимо и 3 вида умножения (скалярное, поэлементное, векторное), которых зачастую нет.
Более того, Rust не позволит случайно обратиться не к тому полю объединения, которые было инициализировано. Поэтому и обращаться к ним придётся иначе
Тут вы путаете объединение и тип-сумму. Суть объединения в том, что одни и те же данные могут представлять из себя разные типы одновременно. Например: масссив байт и число. А вот тип-сумма в D выражается через Algebraic:
Algebraic!(int, string) variant;
variant = 10;
variant.visit!(
(string s) { writeln( "Hello " , s ); },
(int i) { writeln( "Hello " , i ); }
);
В Rust существует только один тип строк, которые представляют последовательность UTF-8 байт
Это же крайне не эффективно. Даже длину строки не узнать, не пробежавшись по ней. Та же ситуация со срезами.
let cstring: BTreeMap<_, _> = collect![
Не думаю, что бинарное дерево тут будет эффективней простого массива. Есть ли возможность отобразить перечисление именно на массив?
В Rust это делается через использование структуры-кортежа (tuple struct, перевод)
На самом деле и в D это всего-лишь параметризированная структура:
struct Typedef(T, T init = T.init, string cookie=null)
В Rust используется несколько другой подход: сортировка, как и некоторые другие алгоритмы, реализована для «срезов» (slice), а те контейнеры, для которых это имеет смысл, умеют к ним приводиться.
Всё же, диапазоны в D — более мощная абстракция, чем срезы, которые в D тоже есть.
Тут стоит заметить, что в D есть конструкция debug(name) вида:
В Rust тоже:
if cfg!(debug) {
...
}
Всё же сложение и соединение — разные операции.
Пожалуй, соглашусь, но "сложение" строк — штука уже привычная и существует во многих языках. А пример с векторами, как по мне, как раз показывает, что операции с произвольными типами мало смысла укладывать в общий интерфейс.
Тут вы путаете объединение и тип-сумму.
Ну не то чтобы путаю. Просто в С есть только "старый добрый" union, да и в С++ тоже, разве что добавляются более удобные обёртки поверх него. И используются они и для того и для другого.
А вот про Algebraic! в D действительно не знал, спасибо.
Это же крайне не эффективно. Даже длину строки не узнать, не пробежавшись по ней.
Длину (в байтах) узнать легко. Нужно ли легко/постоянно узнавать длину в символах — вопрос спорный.
Ну и тут дело в том, что строки в Rust — это немного больше, чем "тупой контейнер", как в том же С++. Тем не менее, если есть необходимость, никто не запрещает нам работать с ними как с контейнером u8/u16/u32, просто это будут уже не строки в терминах языка.
Есть ли возможность отобразить перечисление именно на массив?
Наверное, я не совсем понял, что делается в D в том фрагменте кода. Как именно будет выглядеть результирующий массив?
Всё же, диапазоны в D — более мощная абстракция, чем срезы, которые в D тоже есть.
В расте используются итераторы, которые, несмотря на название (которое у меня вызывает ассоциации, в первую очередь с С++), представляют собой (в общем случае) как раз указатель плюс размер.
Наверное, я не совсем понял, что делается в D в том фрагменте кода. Как именно будет выглядеть результирующий массив?
string[ 3 ] cstring = [ "red", "blue", "green" ];
Перечисления по умолчанию имеют тип int.
В расте используются итераторы, которые, несмотря на название (которое у меня вызывает ассоциации, в первую очередь с С++), представляют собой (в общем случае) как раз указатель плюс размер.
Это классические срезы. Диапазоны же могут быть ленивыми, бесконечными, не иметь произвольного доступа, входными, выходными и тп. То есть срез — это частный случай конечного диапазона с произвольным доступом.
Это классические срезы.
Нет, это просто моё неудачное описание. Ну то есть, да, срезы представляются как указатель и размер, итераторы по массиву — так же. Но там, как и в диапазонах D, может быть функция (а значит и ленивость) генерирующая бесконечную последовательность.
Перечисления по умолчанию имеют тип int.
То есть, нужен просто массив строковых представлений перечисления? Думал, что нам надо именно в обе стороны мапить значения. С массивом это ведь не будет работать в общем случае — ведь в enum можно произвольные значения задавать.
В таком случае, как в Rust легко и "из коробки" сделать именно такое не знаю. Зато можно сделать иначе:
#[derive(Debug)]
enum Colors {
Red,
Blue,
Green,
}
println!("{:?}", Colors::Red);
let colors = [Colors::Red, Colors::Blue, Colors::Green];
println!("{:?}", colors);
А именно (автоматически) реализовать трейт Debug. Тогда сможем приводить значения перечисления к строке. Правда если захочется самим задать строковое представление, то придётся реализовывать трейт (Debug или Display) вручную.
На самом деле и в D это всего-лишь параметризированная структура:
Почитал подробнее… пожалуй, в этом подход языков опять различается. Скажем, в Rust для получения "обёрнутого" значения используется всё тоже сопоставление с образцом, а в D имеется ещё один хелпер-шаблон TypedefType. Опять же, tuple struct может использоваться не только для этого.
И правильно я понимаю, что по умолчанию Typedef для одинаковых типов будет использовать и одинаковый cookie
, то есть типы будут одинаковы? а
TypedefType позволяет получить оригинальный тип, если вы его не знаете. Если знаете, то можете просто привести к нему. Зачем тут сопоставление с образом?
Правильно:
alias TypeInt1 = Typedef!int;
alias TypeInt2 = Typedef!int;
// The two Typedefs are the same type.
static assert(is(TypeInt1 == TypeInt2));
alias MoneyEuros = Typedef!(float, float.init, "euros");
alias MoneyDollars = Typedef!(float, float.init, "dollars");
// The two Typedefs are _not_ the same type.
static assert(!is(MoneyEuros == MoneyDollars));
Зачем тут сопоставление с образом?
В Rust это общий механизм. Так можно "вытаскивать значения" из структур и кортежей, в том числе и из tuple struct:
struct Handle(i32);
let h = Handle(10);
let Handle(v) = h; // v: i32
Просто так скастовать структуру в произвольный тип не получится.
Лучше всё же не смешивать «векторы» (массивы чисел, с которыми можно делать что угодно) и векторы (математические объекты, подчиняющиеся строго определённым правилам преобразования). Скалярное и векторное (а лучше внешнее) умножение — это операции, имеющие смысл лишь для настоящих векторов, при этом поэлементное умножение векторов практически не нужно. Такие векторы должны описываться отдельными типами, с собственным набором операций. То же относится к «матрицам» и матрицам.
Это же крайне не эффективно. Даже длину строки не узнать, не пробежавшись по ней. Та же ситуация со срезами.
Но, по крайне мере, это более честно и не маскирует проблемы.
Представление строки в виде последовательности wchar_t так же, в общем случае, не позволяет узнать длину строки, не пробежавшись по ней.
Мало того, что wchar_t-строка в Windows является UTF-16, так еще и не очень понятно, что нужно считать длиной строки (например: последовательность U+0438 (и
), U+0306 (кратка) визуально выглядит как U+0439 (й
)).
Эти проблемы решаются нормализацией и отсутствием суррогатных пар (ucs4).
Дорого по памяти (хотя на linux wchar_t и так 4 байта обычно). И будут торчать length_nfd и length_nfc, чтобы всем хорошо было.
На мой взгляд, лучше оставить явную работу с unicode специализированным библиотекам.
Вы видели сколько весит icu? 35 MiB, если что. Для некоторых и glibc с частичной поддержкой юникода — уже много. Так что далеко не в каждом окружении оно надо. Да и зачем ваять заново, если есть icu?
ваять не надо, надо поддерживать на уровне языка
А реализация в библиотеке языка откуда должна взяться?
чтоб объект «строка юникода» был строкой, а не левым объектом, предоставленным либой с сырым буфером под капотом
А что такое строка юникода? Набор байт в одной из utf (8,16be,16le, 32be, 32le)? Набор codepoint'ов? Набор codepoint'ов в NFC? Может в NFD, NFKC или NFKD? Или набор глифов, если вам надо посчитать сколько позиций на экране занимает конкретная строка? Или заодно поддержку truetype надо втащить, чтобы можно было размер на экране посчитать и при использовании немоноширинного шрифта?
чтоб трансформация по мере надобности происходила, всякое такое
Что такое "трансформация по мере надобности"? Из одной кодировки в другую? Или из внутреннего представления в одной кодировки во внутреннее в другой?
ну и ничего не мешает выпиливать лишние зависимости из итогового бинарника, если в конкретной программе оно не понадобилось
И какие части, скажем, icu-charsets (или libicudata.so) можно выкинуть в iconv-like программе? Явно она использует только кодировку текущей локали.
У меня складывается впечатление, что вы просто не знакомы с этим доменом.
Я бы не сказал, что лишаем: такие вещи можно выносить в макросы/inline функции/расширения компилятора. Rust в этом плане имеет больше возможностей, чем C: макросы поадекватнее, расширения компилятора есть (и, возможно, API для них даже когда‐нибудь стабилизируют). Другое дело, что выигрыш нескольких тактов обычно не стоит того, чтобы заморачиваться, да и написать макрос, который бы оптимизировался так же хорошо, как и встроенная возможность, сложнее.
Встраивание iconv не самая хорошая идея: бинарный файл будет жирным, при том что iconv есть практически везде, где она может понадобиться.
короче, смотреть надо на то, что может компилятор сделать, имея знания о строках, и что из этого мы хотим, чтобы он делал. в iconv, например, мог бы сделать проверку в начале, совпадают ли входная и выходная кодировка, а потом собрать в одном бинарнике один и тот же алгоритм с трансформацией во внутреннее представление строк и без. круто? я не знаю, возможно, смотря что нам надо.
Скорее всего, там есть shortcut для совпадающих входной и выходной кодировок. Очевидно, что такой shortcut пишут либо в самом приложении, либо в библиотеке реализующей перекодировку. Но и то, и другое работает с массивами байт (или аналогом — срезом/итератором/потоком и т. п.). К компилятору и линковщику это отношения не имеет.
Вообще в той части намёк был на то, что программа, а тем более компилятор с линковщиком на этапе сборки не могут знать, поддержка каких кодировок может потребоваться. Только разработчик может знать нужны ли ему полноценные unicode collation, toUpper/toLower вне ASCII-диапазона и т. п.
нормализацию кодпоинтов хотелось бы оставить за рамками, но она может быть нужна, например, для работы с файловой системой
Нет, там она опасна. Файловые системы обычно оперируют строками байт для путей файлов. Иногда развлекаются с case insensitive, но это до добра доводит не всегда: CVE-2014-9390. Нормализация в этом месте даст возможность создать сторонними средствами файл, который невозможно будет открыть/удалить.
что компилятор мог бы делать со знаниями о длине строчек в пикселях — я не представляю, возможно что-то и мог бы, но скорее это задача гуя.
Определитесь всё же, вы говорите про поддержку юникода в компилятора/интерпретаторе или стандартной библиотеке? Т. к. для компиляторов и интерпретаторов поддержка юникода обычно сводится к "не препятствовать" и предоставлению convinient способов создания необходимых объектов на основе строковых литералов.
Если язык строго типизированный, а a
и b
разных типов (а они должны быть разных типов, если одно UTF8, а другое UTF32), то получаем TypeError
. Если a
и b
представлены как массивы байт, то на выходе, очевидно, получим массив байт, который частично UTF8, частично UTF32. Не слишком полезно, но это именно то, о чём вы просили.
Rust, который здесь обсуждают, строго типизирован. Разные типы под разные строчки можно организовать в любом языке, где пользовательские типы вообще есть; и сделать это на уровне отдельной библиотеки, так что замечание про поддержку на уровне системы типов языка я как‐то не понимаю. Единственная поддержка на уровне языка которая здесь пригодится — это пользовательские строковые константы (не знаю как в Rust, а в C++ такое есть; по наличию макроса println!
который выглядит как будто делает разбор строчки на входе могу предположить, что, как минимум, возможно написать макрос utf32nfc!("string literal")
).
Имя файла в *nix является ни чем иным как набором байт. Ни о какой нормализации не идёт и речи: как указали при создании, так оно и будет. Нормализация на уровне какой‐либо библиотеки просто выльется в недоступные файлы. В Windows нормализация производится системными функциями ввода‐вывода, так что опять не требует никакой поддержки ни со стороны языка, ни со стороны библиотеки.
Если язык строго типизированный, а a и b разных типов (а они должны быть разных типов, если одно UTF8, а другое UTF32), то получаем TypeError.
Не обязательно. Многие языки позволяют реализовывать операции с разными типами, Rust тут не исключение. Вот упрощённый пример из Rust by Example:
struct Foo;
struct Bar;
#[derive(Debug)]
struct FooBar;
impl ops::Add<Bar> for Foo {
type Output = FooBar;
fn add(self, _rhs: Bar) -> FooBar {
FooBar
}
}
fn main() {
println!("Foo + Bar = {:?}", Foo + Bar);
}
Собственно, "сложение строк" как раз реализовано для разных типов: String и &str.
Вообще‐то «строгость типизации» для языков с перегрузкой операторов определяется
тем, что затащили в стандарт, а не тем, что вы можете написать: так Python
считается языком со строгой типизацией потому что стандартные типы не позволяют
сложить число со строкой. Так что ваша перегрузка не показатель. То, что можно
сложить String с &str (но не наоборот) показывает, что язык не абсолютно строгий
(таких вообще, наверное, нет), но, к примеру
fn main() {
let s = 1u16;
let s2 = 2u8;
println!("{:?}", (s + s2));
}
не пройдёт. И, я уверен, если бы в стандартной
библиотеке были типы UTF8String и UTF32NFCString, то их нельзя было бы
складывать — именно потому, что непонятно, что должно быть на выходе — как
в примере выше непонятно, должно ли на выходе получиться uint8 или uint16.
Нестандартную библиотеку, которая позволяет такое можно смело клеймить как не
соответствующую духу языка.
Вообще‐то «строгость типизации» для языков с перегрузкой операторов определяется тем, что затащили в стандарт, а не тем, что вы можете написать
То, что можно сложить String с &str (но не наоборот) показывает, что язык не абсолютно строгий
Ну я не согласен с такой формулировкой. По моему, разница между сильной и слабой типизацией видна в (не)явных преобразованиях. Оператор — это по сути просто функция (про трейты в курсе, если что) и нет ничего удивительного, что она может принимать аргументы разных типов. Мы можем написать функцию foo принимающую условные число и строку в любом языке (если там есть функции) и на силу типизации это не повлияет. Не готов считать операторы особым случаем.
А вот если в функцию ожидающую число можно передать строку, то это другое дело.
Но вообще я не хотел ввязываться в спор о строках и ещё меньше хочу спорить о типизации. Сказать хотел только то, что в Rust никто не запрещает операторам работать с разными типами и это уже делается, в том числе, "в стандарте".
man!(C => D => Rust)