Pull to refresh

Comments 62

При всем уважении к проделанной работе, это издевательство, а не код. Не было бы у сообщества запроса на новый язык, если что-то уже написанное можно было бы вот так взять и сконвертировать в другой синтаксис.

Ну если семантически языки похожи то можно в целом, но это явно не про раст и c#)

Но сегодня же СУББОТА!

Автогенерация кода на Расте — прекрасная идея, все= ниуя не понятно =)
Но сегодня же СУББОТА!

Вот именно, пятница была вчера.

Если это коммерческий продукт, то почему он не написан на компилируемом языке и с помощью C API предоставляет интерфейс всем желающим, от PHP до Java?

Как мне кажется, даже в случае, если просто очень хочется писать на C# и решено делать некие конвертеры под самые разнообразные языки, то стоило попробовать изначально сделать для C\C++\Rust. А полученное уже можно было бы подключить и к php, python'у, и к чему угодно через FFI, сгенерировав заголовочные файлы для всех необходимых языков.

Да, понимаю, что для какого-нибудь C написать такой конвертер сложнее, чем для Java. Но теперь, если конвертер будет доделан на rust'е, то (наверное) особого смысла поддерживать такое большое количество языков нет, по крайней мере python и C прекрасно умеют в FFI.

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

«и в целом создаётся впечатление, что Rust и C/C++ близки по скорости. Но существенно разнятся по сложности написания кода.»
И по сложности отладки.
пришлось мне в коде SDK C# заменить все long на int, благо их оказалось немного

То есть я всё понимаю, но вот это… То есть что вы собираетесь делать если вам этот самый long действительно нужен? Что вы будете делать если вам нужен unsigned тип, а Java их не поддерживает?

И самое главное зачем всё это? То есть одноразово для миграции я ещё могу понять. Но зачем такое делать постоянно?
Постоянно — потому что разработка продолжается на C#, и при этом автоматически получаются эквивалентный код на других языках. Для SDK это полезно, чтобы не заморачиваться с интеграцией, а встраивать в свои проекты код на «родном» языке.

Язык получается не родной. То что вы делаете это perevodite с русского na angliskij kak to taк.

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

Ну так а зачем вам нужен этот "эквивалентный код на других языках"? Какую задачу вы этим решаете? Какая задача с вашей точки зрения оправдывает такое "кастрирование" С#? Или это просто самоцель?

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

Обычно подобные задачи решают при помощи выставления надлежащего FFI-интерфейса. И да, компонент, который подлежит встраиванию в проекты на разных языках, всё-таки больше имеет смысл писать на нативном языке.

Вы это сейчас серьёзно? В таком случае надо как минимум тестировать что ваш "эквивалентный код" всё ещё выдаёт "эквивалентный результат".


И я действительно не понимаю в чём проблематика интеграции кода на разных ЯП в одном проекте? Мы это регулярно делаем. И сложности подобной интеграции на мой взгляд даже близко не оправдывают то, как вы "кастрируете" С# чтобы ваш способ работал. Это не С# получается а непонятно что…

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

Вы смеётесь что-ли? Если у вас какая-то ошибка в вашем "переводчике", то вообще нельзя исключить что "переведённые" автотесты будут проходить на ура, но результат аыполнения функции в той же Java всё равно может отличаться от результата в С#.
Более того я на такое уже натыкался когда у нас джуны "бездумно" портировали код расчёта CRC из С# в Java.


И мне теперь реально интересно что у вас за случай такой, что вам хватает настолько урезанного функционала С#. И нужен ли вам тогда вообще С# как таковой?

Я в статье указывал, что у меня SDK (Pullenti) по по лингвистической обработке естественного языка (морфология, семантика, именованные сущности). Так уж исторически сложилось, что он разрабатывается на C#.

Ну вот у вас на странице везде упоминается скорость обработки данных как один из важных критериев.


И даже ни разу не видев вашего кода я готов с вами поспорить что используя нормальный С# вместо вашего урезанного варианта можно будет выжать из системы гораздо больше в этом плане.


Потому что например я уверен что async/await или даже обычное распараллеливание ваш "переводчик" не умеет и поэтому вы их не используете в полном объёме. Если вообще используете…

Полученный в результате трансформации код является кодом на расте лишь формально. Он не предоставляет гарантий, на которые рассчитывают разработчик на расте, и будет падать на всех этих unwrap’ах, вызывая за собой креш всего приложения там, где код на си шарпе всего лишь бросал бы исключение, которое летит до кетч-блока.


То есть, это как если бы конвертер с другого языка в си шарп делал бы Environment.Exit(1); по малейшему поводу вместо бросания исключения.

Если уж так отказываться от некоторых шарповских плюшек, то почему сразу не взять haxe.org как основной язык разработки?
Сборка под js, java, c#, php, python, lua есть из коробки (не без оговорок конечно, но это было бы несравнимо проще, чем писать свой конвертер с нуля).
Для rust можно было бы наверное собрать шаред библиотеку из с++ таргета (вопрос поиска инструмента для генерации биндингов из с++).
Наверное, можно и так поступить, но у меня уже есть код на C#, который создавался (и, главное, отлаживался) в течение почти 10 лет, и уже сразу не возьмёшь этот haxe. Но можно подумать о конвертере C#->Haxe!!!
Очень странный подход, как выше написали уже.
Rust ещё не устоялся, изменения выходят с интервалом 1..2 месяца, (пруфы: май, июнь, июль). С точки зрения практичности, Вы потратили время зря, т.к. обратная совместимость в Расте мало поддерживается.
Из C#-подобных языков я бы упомянул Vala и D lang.
Вам имеет смысл перейти на один из языков, генерирующих нативные бинарники, и гарантирующие C (не C++! ) ABI, и распространять библиотеки в таком виде.
Для Python`истов сгенерить обёртки в CFFI или SWIG, Джавистам завернуть в вызовы JNI, и так далее.
При транспиляции любого сложного кода Вы не сможете гарантировать одинаковое и надёжное поведение Ваших библиотек.
Генерируемый код действительно работает эквивалентно, что гарантируется кучей автотестов (более 1000), которые также переводятся на конечный язык и отрабатывают без ошибок, как и в исходном C#
Ясно.
А можете подсказать, что почитать, чтоб легче «врубиться» в обработку естественных языков? Я сейчас игрушечными примерами балуюсь, и хочу плотно заняться NLP.
А есть гарантия, что переведенный на другой язык тест останется корректным?
Если тест выдаёт результат, полностью совпадающий с ожидаемым, то это и есть гарантия (не 100%, разумеется, но близко к этому). А для чего же ещё нужны автотесты!?

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

Так и есть — для входного текста проверяется именно выход алгоритма, чтобы результат анализа полностью совпадал с ожидаемым. Это не тесты типа Assert.IsTrue(...), а полноценное сравнение сложных структур (например, выделенных сущностей), всё должно полностью совпадать.

А где гарантия, что логика сравнения этих сложных структур перенесена правильно?

А где гарантия, что исходный код на C# функционирует правильно?
А вы его только автотестами проверяете? И при этом никто не проверяет правильность самих автотестов? Ну то есть хотя бы первый раз?
Разумеется, проверяет человек. Берётся текст, на нём отрабатывает алгоритм, и человек оценивает (в первый раз, при создании теста), правильный ли результат. После утверждения тест является рабочим.
То есть на C# у вас код проверен «вручную», а на Java нет.

При этом нет гарантий что ваши автотесты действительно правильно «переводятся» на Java и что «логика сравнения этих сложных структур перенесена правильно».
Ну если на Java те же тесты для тех же текстов получают одинаковый результат, с точностью до байта, как на C#, то куда уж правильнее.
Так вы всё-таки каждый тест проверяете что он на Java «получает одинаковый результат, с точностью до байта»? Или вы просто это предполагаете на основании того что они ошибки не выдают?
Разумеется, проверяется результат с точностью до байта, а не true\false или сам факт запуска.
Как и кем проверяется? «Вручную» каждый тест после «перевода»?

Ещё раз: если у вас ошибка где-то в «переводчике», то у вас может такое получится что функция будет неправильно «считать байты» и автотест будет неправильно «считать байты». Но при этом результаты функции и автотеста между собой будут совпадать.
Автотест содержит пару {T, R0}, где T — текст, R0 — эталонный результат (сущности с их атрибутами). Пусть R1 = A(T) — результат применения алгоритма A к тексту T. Тест считается удачным, если R0 = R1. Проверка сравнения строк идёт штатными функциями конечного языка, тут ошибки быть не может. Если R0 = R1 на всех конечных языках, то конвертер работает неплохо. Как минимум, результирующие SDK одинаково функционируют на автотестах.
Автотест содержит пару {T, R0}, где T — текст, R0 — эталонный результат (сущности с их атрибутами). Пусть R1 = A(T) — результат применения алгоритма A к тексту T. Тест считается удачным, если R0 = R1.

И это по вашему не «тесты типа Assert.IsTrue»?..

Проверка сравнения строк идёт штатными функциями конечного языка, тут ошибки быть не может

Ещё как может. Сравнение строк в различных языках или даже в одном языке при разных «культурах» это та ещё песня…

Если R0 = R1 на всех конечных языках, то конвертер работает неплохо.

«Неплохо» я вижу. Но на мой взгляд такого «неплохо» достаточно мало чтобы довериться одним только автотестам.
Результат — это сущности с атрибутами, две сущности равны, если у них атрибуты полностью совпадают. Например, для персоны к атрибутам относятся фамилия, имя, отчество, должность и пр. Значения атрибутов могут быть как строковыми, так и ссылками на другие сущности, то есть имеем а общем случае на выходе граф. И вот эти конструкции должны полностью совпадать.
Rust ещё не устоялся, изменения выходят с интервалом 1..2 месяца
Вы потратили время зря, т.к. обратная совместимость в Расте мало поддерживается

Релизы компилятора выходят раз в шесть недель. Но какое отношение релизы компилятора имеют к совместимости? С релиза версии 1.0 в 2015 году гарантируется обратная совместимость на уровне исходников.

гарантируется обратная совместимость

Спасибо, не знал.
Но, вообще-то, при выходе новых версий компилятора ошибки / баги возможны.

Сам не пробовал, но вроде бы в проекте можно, по аналогии с версиями библиотек, зафиксировать и версию компилятора/языка.

Да, в директорию проекта просто подкладывается конфигурационный файл с указанной версией. На практике все крупные проекты, с которыми я работал, используют stable версию языка – все фичи в которой сначала неопределенное время тестировались в nightly канале, затем 6 недель в beta.

Да, безусловно, и они случаются. Но, во-первых, они так же быстро исправляются, а во-вторых, каждая фича проходит несколько этапов тестирования, в том числе на реальных проектах. У команды разработчиков языка есть даже возможность пересобрать все открытые библиотеки и приложения и посмотреть, что сломалось.

Как-то ради интереса попробовал взять код lua и чем-то готовым сконвертировать в rust. Получился рабочий, но очень страшный код с unsafe и указателям на каждом шагу. Потом я пришёл к выводу, что в любом случае предстоит огромная работа по переписыванию на идиооматичный раст, и местами возникал соблазн поменять архитектуру. Так как делалось всё из интереса, до конца доводить не стал.
По ощущениям, концепция владения очень сильно влияет на способ написания кода и реально усложняет конвертацию.
Прикольно, что автор пошёл с другой стороны и поменял код до конвертации, а не после.

Базовое отличие Rust от других языков. Это С\С++, Паскаль, Фортран и др. Возникает утечка памяти, если delete не вызвать.

В Rust освобождение памяти происходит тоже автоматически, но сразу при окончании жизни объекта, например, когда он выходит из области видимости

Это называется RAII, и в C++ естественно эта фича есть.


Си++, Java, Python и пр., но когда оказывается, что после добавления в список объект нельзя использовать: it = new ...(); list.add(it); it.val = ..., а вот так можно: it = new ...(); it.val = ...; list.add(it);, то это обескураживает

В C++ это тоже есть, move constructor. Использование объекта после std::move конечно не вызовет ошибку компиляцию, но вот падение программы может.


В общем я бы предложил убрать по крайней мере, C++ из списка "нормальных" языков,
на остальных языках не программировал много поэтому, может и по отношению к ним, в некоторых вопросах, Rust на самом деле не является уникальным.

В C#, Java и др. строки есть последовательность символов char размером 16 бит (в Си 8 бит, в Си++ 8 и 16), что есть ограничение с точки зрения разработчиков Rust. Сейчас Unicode уже 32-битный, а вдруг в будущем он вообще станет 64-битным? А они будут к этому готовы.

И UTF-8, и UTF-16 это кодировки с переменной длинной символа — в обеих кодировках максимальная длина символа 32 бита. Непонятно, где здесь ограничение.
UTF-8 была скорее всего выбрана из-за её большей популярности (даже Windows не так давно стал её поддерживать)

Думаю не столько из-за популярности, сколько из соображения удобства и эффективности: ASCII-текст в UTF-8 кодируется только одним байтом, а не двумя, как в UTF-16.

Выбор UTF-8 обусловлен исторически, тем, что это сейчас стандартная кодировка для 32-битного юникода, ее принимают все, кто переходил на юникод после того, как он стал 32-битным.
Системы, перешедшие на юникод раньше других, когда он был 16-битным, переходили на 16-битный вариант, чтобы сохранить сишную строку, как последовательность char'ов, увеличив ее размер «всего» в два раза. Windows NT была среди этих операционок и в то время это была революция, в положительном смысле этого слова.
Однако опыт этих систем выявил много проблем 16-битного представления: только LE/BE чего стоит. А потом и юникод стал 32-битным, и все последующие операционные системы и языки выбирали UTF-8, как единственную систему, одинаковую в любой архитектуре, не содержащую непредставимых в ASCII байтов и оптимизирующую память. Про это даже Джоел писал…
Rust и C# слишком разные языки, чтобы написать сколь угодно рабочий конвертер между ними. То что считается идиоматичным в C#, считается антипаттерном в Rust. Динамический полиморфизм в расте хоть и есть, но он убивает производительность, так как компилятор не может zero-cost оптимизировать его.
Или чтобы реализовать перекрёстные ссылки между объектами класса Foo, нужно использовать конструкцию Option<Rc<RefCell<Foo>>>, а для доступа к полю val этого класса вызывать foo.unwrap().borrow().val.

Простое правило написания кода на Rust: если вы используете RefCell, скорее всего вы делаете что-то не так. RefCell выносит проверки заимствования в runtime, что опять же негативно влияет на оптимизации.
Выскажу своё личное мнение: мне представляется, что прогресс в области программирования идёт в направлении оптимизации труда программиста, чтобы меньшими усилиями достигать большего эффекта. Случай Rust уникален тем, что не вписывается в эту тенденцию. Удивительным для меня фактом является рост его популярности (сейчас Rust вошёл в 20-ку). Но почему? Вот вопрос.

Случай Rust в некоторых случаях полагает написание дополнительного кода для проверки его компилятором с целью обеспечения безопасности программы (те же lifetimes), в некоторых случаях наоборот к уменьшению кода (например тип-суммы отлично справляются с заменой паттерна visitor в ООП, уменьшая количество кода в разы). Ваша проблема в том, что вы писали неидиоматичный код. Хотя это скорее проблема проекта, так как транслировать идиоматичный C# в идиоматичный Rust кажется мне невозможным.
По производительности на моей задаче Rust не произвёл впечатления — выигрыш по сравнению с C# получился всего в 2 раза.

Удивлен что с тем кодом который вы показали ниже, у вас вообще получилась прибавка к производительности.
В Rust освобождение памяти происходит тоже автоматически, но сразу при окончании жизни объекта, например, когда он выходит из области видимости. Скажем, если внутри блока создать объект {… let x = Foo {… это конструктор};… }, то память автоматически освободится при выходе управления из этого блока.

Уточню: в данном случае память выделяется на стеке, а не на куче. Для выделения на куче следует использовать сырые или умные указатели, вроде Box, Rc, Arc.
Решение — реализовать класс (struct в терминологии Rust), содержащий как вектор символов, так и сам string.

Решение крайне плохое. Вы по сути аллоцируете два раза все ваши NString, так как Clone::clone копирует все данные. Клонирование в таких случаях будет больно бить по производительности, и может считаться неидиоматичным применением. Возможно, следовало бы создать свой тип строки, либо же поискать готовые решения на crates.io.
Например, вот метод Substring(int start, int len) для получения подстроки:

Вопрос, нужно ли вам в данном примере владение NString на выходе. Я думаю, что скорее всего не нужно было, достаточно было возвращать &str.
Причём получилось 3 обёртки для каждого типа в зависимости от типа элементов: для простых типов, для ссылок &T и для владений T.

Скорее всего достаточно один раз было написать коллекцию, с использование generic-параметра. Какой-то go-style у вас вышел.
В Rust нет привычных всем null и базового класса object, практически отсутствует и приведение типов. То, что в C# любой тип можно «упаковать» в object и затем «распаковать» его — для Rust это за пределами возможного.

На этом моменте Вас должно было озарить, что писать конвертер в Rust из c# — плохая идея. То что вы нагородили ниже крайне плохо оптимизируется компилятором, и выносит кучу ненужных проверок в runtime.
Обратим внимание: для шарпового obj = cnt.First на Rust получается obj = ObjValue::from_item(Some(Rc::clone(cnt.borrow().get_first().as_ref().unwrap()))). Что говорите, это жесть? Нет, это Раст!

Говорите это Rust? Нет, это попытка натянуть сову на глобус. В обычном Rust-коде за такое оторвали бы руки.
Аналогом класса C# в Rust выступает struct

Не совсем корректно. В Rust нет понятия класса. Аналогом struct из Rust'a в C# скорее всего выступит тот же самый struct, с некоторыми различиями.

Ваш IItem с динамическим полиморфизмом это также крайне неидиоматичный код. Мало того, что динамический полиморфизм следует использовать только в случаях с крайней необходимостью, когда ничего другого уже не работает, так в данном случае достаточно было бы использовать generic-параметр, что делается в коллекциях и в самом C#.
Короче, когда таких ссылок становится много, наступает lifetime-hell, как я его назвал. Да, Rust тоже внёс свой вклад в коллекцию этих хеллов!

Это если бездумно вставлять lifetimes, не собо понимая зачем они нужны.

В целом по статье сложилось впечатление что автор незнаком не только с Rust, но и с C#. Как я знаю, использование object в C# тоже не считается хорошим тоном. Также на лицо неумение автора применять generic-параметры, которые также присутсвуют и в C#.
Вопрос к автору, а вы Rustbook открывали хотя бы?..
Постараюсь ответить на последний абзац. Rustbook я открывал, знакомство с Rust весьма поверхностное, ограниченное месяцем, реальных проектов на нём не делал. С языком C# знаком чуть больше. Да, я сам стараюсь object не использовать, но иногда без него никак. Например, object Tag в классах WinForm (это пришло ещё из Delphi, кажется), чтобы пользователь мог записать туда что угодно. Про generic я тоже кое-что слышал, даже иногда использую их, когда настроение хорошее :).
У любого языка есть своя идеология и концепция, диктующие то, как с использованием этого языка предполагается решать задачи. Писать автоматический конвертер не прочувствовав этих концепций чревато тем, что на выходе вы получите монстра Франкенштейна, который вызовет у всех только чувство отторжения. Если уж вы решили создать монстра Франкенштейна, то лучше абстрагироваться от желания проецировать отношение к своему детищу на прежних владельцев частей тела, из которых он собран.
Вы правы, но в случае генерируемого SDK пользователю не надо туда влезать и разбираться с кодом, а достаточно использовать внешние функции, которые для Java и JavaScript мало отличаются от оригинала, для Python и Rust получается немного коряво.
Да и любой переводчик с естественного языка на язык представляется таким «Франкенштейном», особенно на ранних этапах своего развития. Понятно, что носитель обоих языков переведёт лучше. Но это дорого, и где их взять то, на всех?
Sign up to leave a comment.

Articles