All streams
Search
Write a publication
Pull to refresh

Comments 25

Дело привычки конечно, но код на rust читается так себе.

Смотря с чем сравнивать и какой код сравнивать. Шаблоны и прочее метапрограмирование нигде не читается хорошо. Линейный код в виде цепочек обычно чище, будь то Java или плюсы или как тут - в Rust.

Шаблоны и прочее метапрограмирование

Шаблоны в смысле параметрического полиморфизма отлично читаются в любом ML-подобном языке. В нормальных завтипизированных языках это вообще обычно просто частный случай передачи аргумента в функцию (просто аргумент — тип).

Метапрограммирование — ну, дженерики норм (особенно если навесить pattern synonyms, если вам вдруг однобуквенные конструкторы не оч).

Метапрограммирование — ну, дженерики норм

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

Метапрограммирование на основе рефлексии в Haskell емнип можно сделать нормальным, но хаскелл среднестатистическому программисту в принципе читать сложнее. Даже условная Scala или Erlang в этом плане попроще будут. Но в среднем без предварительного переобучения в ФП языки довольно сложно, т.к. классические приёмы управления потоком программы растворяются в других концептах.

Метапрограммирование на основе рефлексии в Haskell емнип можно сделать нормальным, но хаскелл среднестатистическому программисту в принципе читать сложнее.

Чем сложнее? Как раз минимальный синтаксис, но не слишком минимальный (то есть (не (лисп))).

Даже условная Scala или Erlang в этом плане попроще будут.

С чтением эрланга у меня мало опыта, implicit val а[_] от(скалы): _ => { у[меня] = new глаза[меня](вытекают[меня]) }. ИМХО, конечно.

Как раз минимальный синтаксис

когда xs в вашем patP что-то там do это ни разу не даёт пояснений о предметной области - это что-то про авторизацию, про обработку контейнеров или про рисование крестиков-ноликов на экране. Чем плохи одно-двубуквенные переменные кажется многие объясняли уже не единожды, чем плоха польская нотация - тоже. Но при этом это нормальная практика хаскеля.

В погоне за математической генерализацией, примеры бизнес логики растворяются в этих абстрактных байндингах типов. Например, без бОльшего ковыряния в документации сходу догадаться про назначение тех же 'NoSourceUnpackedness,'NoSourceStrictness,'DecidedLazy из доки по вашей ссылки на мой взгляд не представляется возможным. Зачем и почему у типа появились какие-то перегрузки операторов? Зачем вообще необходимо иметь примеры раскрытия типа в примерах, а не в тех разделе где-нибудь рядом с Generic Representation Type?

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

С чтением эрланга у меня мало опыта,

Ocaml, Lean 4? У последнего вроде тоже зав.типы есть, но код там всё же ближе к обычному и проще догадаться, что будет происходить в произвольно выбранной строчке.

когда xs в вашем patP что-то там do это ни разу не даёт пояснений о предметной области - это что-то про авторизацию, про обработку контейнеров или про рисование крестиков-ноликов на экране

А когда это elements, то становится понятнее, о чём речь?

Собсна, о какой предметной области идёт речь при написании библиотечных функций вроде filter?

Чем плохи одно-двубуквенные переменные кажется многие объясняли уже не единожды, чем плоха польская нотация - тоже.

Так это зависит от контекста. Да, код чаще читается, чем пишется, но как-то так выходит (по крайней мере, в моём опыте), что в случае хаскеля и подобных код бегло (когда важны говорящие названия) читается куда чаще на уровне сигнатур типов и названий функций. А когда вдруг надо забуриться в поведение функции, то надо забуриваться глубоко в предметную логику, и локальный маппинг «m значит module » — очень дёшев.

Ну, опять же, личная практика и практика людей, с которыми я работал.

Но при этом это нормальная практика хаскеля.

Но это не синтаксис. Это вопрос соглашений в вашей кодовой базе.

Вон, например, что-то, где многобуквенных названий куда больше, чем одно/двухбуквенных.

Сравните интро в те же дженерики в Rust с туториалом хаскела.

А, теперь я понял причину вашего удивления!

Потому что это (вместе с хаскельными дженериками) — три сильно разные концепции?

  • Дженерики в Rust — это просто параметрический полиморфизм в хаскеле. Это когда вы пишете showMany :: Show a ⇒ [a] → String. По ним (особенно в варианте Haskell98, без сложных расширений) вообще никакой документации не нужно, и синтаксис у них лайтовый («слово с маленькой буквы — переменная типа»). А по сложным расширениям даже официальная дока (не туториал) тоже вполне лайтовая: например или вот.

  • Дженерики в хаскеле — это, гм, наблюдение, что (почти) любой тип данных изоморфен рекурсивной структуре из сумм, произведений и ещё пары мелочей. Поэтому можно (почти) любой тип данных замапить на это обобщённое представление, обработать его обобщённым образом, и, если надо, размапить обратно. Именно так работает deriving FromJSON для автовывода десериализации из JSON библиотекой Aeson: оно говорит «компилятор, дай мне обобщённое представление данного типа через toRep», компилятор его даёт (вместе с метаинформацией вроде имён полей, или их строгости вот, как вы заметили), и Aeson уже обрабатывает его.
    Это на самом деле охренеть как удобно, потому что все ваши обобщённые функции превращаются в няшную приятную рекурсивную обработку структур данных. Когда мне на работе надо было набросать инфраструктурную либу для работы с тамошним аналогом sqlite, я дженерик-поддержку (де)сериализации произвольных типов сделал минут за 20.
    Согласно тому, что я знаю о расте, прямых аналогов дженериков там нет.

  • Template Haskell — это вообще тупо кодогенерация. Это код на хаскеле, который выполняется во время компиляции и может генерировать любой другой код. Через TH можно сделать поддержку строковой интерполяции, например (чтобы [i|My name is #{name}, my age is #{age}|] разворачивалось в "My name is " ++ name ++ ", my age is " ++ show age), или TH'ем можно сделать всё то же, что можно сделать дженериками (и у компилятора там чуть больше простора для оптимизаций).
    FromJSON тоже можно было бы сделать TH'ем, но код на TH писать куда менее приятно, чем на дженериках.
    TH — это плюс-минус растовские процедурные макросы.

Написанный до этого и, возможно, уже устаревший ответ на другие ваши замечания

В погоне за математической генерализацией, примеры бизнес логики растворяются в этих абстрактных байндингах типов. Например, без бОльшего ковыряния в документации сходу догадаться про назначение тех же 'NoSourceUnpackedness,'NoSourceStrictness,'DecidedLazy из доки по вашей ссылки на мой взгляд не представляется возможным.

Так здесь бизнес-логика — это как компилятор видит ваш код. Эта документация нужна тем, кто пишет библиотеки для метапрограммирования. Эта дока — для тех, кто пишет аналог serde из раста (aeson конкретно для жсона, например), а не тех, кто пользуется аналогом serde. Те, кто пользуется, просто пишет deriving (Generic, Aeson.FromJSON, Aeson.ToJSON) и в ус не дует.

Поэтому совершенно неудивительно, что в задаче по ковырянию в представлении ваших типов вылезают детали вроде того, являются ли поля распакованными в конструктор или нет, являются ли они строгими или нет, и почему.

Это будет в любой докуменатции.

Зачем вообще необходимо иметь примеры раскрытия типа в примерах, а не в тех разделе где-нибудь рядом с Generic Representation Type?

Потому что вся эта страница — про generic representation type?

Надеюсь, это всё сняло некоторые ваши вопросы.

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

Template Haskell 

хаскельный аналог макросов. С рефлексией и работой на типах, я так понимаю.

 структуре из сумм, произведений и ещё пары мелочей

вот этот момент не очень ясен- суммы-произведения на типах же, правильно? Это типа как в расте если хочется безопасного мультитрединга, то нужно в ограничения делать наSend+Sync, то есть буквально конъюнкция типов для местного верификатора. А что из себя представляет произведение в таком случае? Автосгенерённые сочетания для все вариантов перестановок, для пущей коммутативности?

вот этот момент не очень ясен- суммы-произведения на типах же, правильно?

Это A в аббревиатуре ADT — алгебра.

Когда у вас есть data Foo = MakeFoo Int Double String, то Foo эквивалентен туплу (то есть, произведению — и в смысле мощностей множеств, и в смысле теорката, и в интуитивном смысле) из Int, Double и String. Если же у вас data Foo = C1 ... | C2 ... | C3 ..., то это сумма вариантов из всех возможностей построить многоточие после C1, либо после C2, либо после C3.

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

Хм. ИМХО таки

largest :: Ord a => [a] -> a

читабельнее, чем

fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T

То, что Rust делали С++ники, наложило неизгладимый отпечаток!

Кстати, не нашёл ни одного полезного случая когда нужны завтипы. Ведь они могут что-то доказать только когда число (длина массива и т.п.) известна на стадии компиляции. А это прям редко, чаще это то что пришло снаружи.

Проверить в рантайме пришедшее снаружи и затем передать проверенное в код с завтипами?

да, но завтипы тогда избыточны
достаточно как в typescript refinement-типа (brand-типа) - тип с токеном, токен выдан рантайм проверкой 

type Aes256Key = Uint8Array & { __brand: "Aes256Key" };

declare function makeAesKey(bytes: Uint8Array): Aes256Key;

function encrypt(key: Aes256Key, data: string) {
  // Здесь НЕ надо проверять длину! 
  // Тип гарантирует: key.length === 32
}

// Использование:
const rawKey = getKeyFromNetwork(); // Uint8Array
if (rawKey.length !== 32) handleError();
encrypt(makeAesKey(rawKey), "secret"); // "Доказали" рантаймово

Но можно случайно потерять handleError() и вызвать makeAesKey(rawKey) с невалидными данными, наверное?

Я не знаком с языками с завтипами, но теоретически предположу, что достаточно умный компилятор должен быть способен догадаться, что проверка в рантайме реально выполнена, подобно такому:

function double(n: number | null): number {
  if (n == null) {
    throw new Error('no null pls');
  }
  // Здесь typescript понимает, что уже доказано,
  // что n не может быть null
  return n * 2;
}

В этом примере не завтипы, но, надеюсь, суть моей мысли вы поняли

Ведь они могут что-то доказать только когда число (длина массива и т.п.) известна на стадии компиляции.

Нет, в том-то и дело, что настоящие завтипы позволяют зависимости от рантайм-данных (в отличие от их эмуляции через DataKinds и ещё сотню расширений в хаскеле, или статические завтипы а-ля ATS, или path-dependent types в скале, или какой там академический термин для control flow analysis в TS). Просто рантайм-значения потребуют рантайм-проверок.

В завтипизированном языке вы можете написать

-- Для начала три библиотечных функции.
-- Тотальная функция с очевидной семантикой
-- Fin n — тип чисел, меньших `n`:
index : Vect a n → Fin n → a

-- Проверка, что число меньше другого
-- Dec P — либо свидетельство (элемент типа) P,
-- либо свидетельство ¬P,
-- тип вроде Dec P = Yes P | No (¬ P):
(<?) : (lhs : Nat)
     → (rhs : Nat)
     → Dec (lhs < rhs)

-- Если число меньше границы, то из него можно сделать `Fin`:
toFin : (n : Nat)
      → (n < b)
      → Fin b

-- REST-метод для индексации массива,
-- всё очевидно в рантайме:
indexEndpoint : Vect a len → MonadRest ()
indexEndpoint {len} someArray = do
  -- куда уж рантаймнее?
  idx <- param Nat "idx"
  case idx <? len of
    Yes proof → sendReply $ index someArray (toFin idx proof)
    No _contra → sendError "out of bounds!"

Зачем всё это нужно по сравнению с вашим вариантом TS рядом?

Во-первых, я не заметил, где у вас проверка, что длина — действительно 32 байта. Более того, тип функции

declare function makeAesKey(bytes: Uint8Array): Aes256Key;

говорит, что она физически эту проверку делать не может (ну или кидает исключение, но этого в типах нет).

Во-вторых, после того, как вы сделали эту рантайм-проверку один раз, вы дальше можете доказывать статически, что если она проходит, то какие-то зависящие от неё вычисления тоже проходят проверку, и дополнительно их делать не надо. Скажем, если idx выше меньше n, то idx * 2 / 3 (или idx & (idx - 1), или что угодно подобное) тоже меньше n, и это уже проверять в рантайме не надо, это можно просто доказать.

В-третьих, это всё не ограничивается такими проверками. Например, можно написать завтипизированный корректный printf , где тип самой функции может зависеть от рантайм-форматной строки (пользоваться этим в рантайме, конечно, будет неудобно, но смысл понятен, надеюсь).

Для человека, в целом знакомым с программированием но малознакомым конкретно с Rust - да.
Если же добавить в определение задачи условие о том, что человек должен быть знаком с технологий, код на которой пытается понять то сравнение получится сильно не в пользу того же C++. Особенно с хайповым в последние 10-15 лет template-only подходом и его наигрязнейшими хаками.

С плюсами знаком, и согласен, что их читать то еще удовольствие.

По мне, так очень хорошо Rust читается.

Главное, что сигнатуры функций не врут и ошибка входит в контракт (сигнатуру). Жаль, IO эффекты не проверяются компилятором на уровне сигнатур, но хорошим тоном весь IO делать асинхронным что видно в сигнатуре.

Мусорить в контрактах (лайфтаймы всякие) не надо без необходимости и они будут читаемы.

В сравнении с процедурным Си, или с тем же C# выглядит не очень, имхо.

Если писать на раст в процедурном стиле на раст то конечно.

А если в ФП то ой. И конечные автоматы уже встроены в синтаксис языка Rust, что гораздо удобнее и безопаснее чем писать свои

+1 я бы на месте разработчиков Раста изменил немного синтаксис/стмволы заимствования, вместо &mut , использовать другой символ…

Конечно, не хочется обижать автора статьи, но первая её часть читается как «Вредные советы» Г. Остера. Когда осознаёшь, что автор всерьёз предлагает клонировать всё подряд, кроме пары базовых случаев, становится страшно.
Это делает Ваши программы на Rust не просто менее производительными, чем они могли быть. Это делает их значительно менее производительными, чем программы на языках со сборщиком мусора. Постоянные и бездумные копирования данных более, чем нивелируют тот бонус производительности, который даёт AOT-компиляция в машинный код. Играть в кошки-мышки с borrow checker'ом постоянно и ненужно: написать мало-мальски сложную программу на одних только заимствованиях едва ли получится, но есть ведь способы успокоить borrow checker, избежав копирования данных: всякие там RefCell и счётчики ссылок.

Аргумент про растущие тактовые частоты и дешёвую оперативную память – очень странный. Оптимизация до сих пор имеет место и до сих пор полезна и важна. Другой вопрос, который меня беспокоит: зачем вообще Вы пытаетесь использовать инструмент, которым не только не умеете эффективно пользоваться, но и отказываетесь учиться, опуская руки и прикрываясь за смирением с тем, что Вы посредственный программист? Раз уж Вы так хотите прибедняться и называете себя посредственностью, почему не возьмёте инструмент под стать – что-нибудь попроще, без borrow checker'а, без лайфтаймов, со сборщиком мусора?

Спасибо за развернутый комментарий! Обратите внимание на теги к статье.

Нет. Копировать структуры в раст легко и дешево. Внутри функции компилятор сам выведет без копирования. А параметры в функции лучше передавать по значению, как в Паскале. Потом разберётесь и будете оптимизировать.

Раст полезен не только производительностью. Он помогает доказывать утверждения (бизнес правила) в типах и корректность выражений используя pattern match и конечные автоматы.

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

  • Высокая параллельность обработки и вообще high load

  • Критичность ошибок, необходимость более надёжных средств, чем просто тесты

  • Жёсткие сроки: на раст больше уйдёт на проектирование (прогнозируемое время), но доказательство корректности / отладка (самое непредсказуемое по времени) будет гораздо короче

Sign up to leave a comment.

Articles