Разработка через типы (Type-Driven Development, TDD) — это методология программирования, при которой вы начинаете с написания определений типов, которые служат точной спецификацией вашей программы. Вместо того чтобы сначала писать код, а потом добавлять тесты или типы, вы используете компилятор как интерактивного ассистента, который направляет вас к созданию программы, удовлетворяющей заданным типам.

Idris — это чистый функциональный язык программирования, созданный Эдвином Брэди, который идеально подходит для этого подхода благодаря поддержке зависимых типов и прочих расширенных проверок на стадии компиляции. Большинство языков так не умеет, но Type-Driven Development поможет нам и без этого.

Философия Type Driven Development

  1. Тип данных это утверждение о данных.

  2. Сигнатура функции это утверждение о том, какое преобразование данных возможно.

  3. Реализация (код) это доказательство утверждений средствами компилятора.

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

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

Некоторые языки, например Python, JS, PHP, позволяет писать код без типов. Но если только вам не нужно сдать код уже "вчера" не советую игнорировать помощь компилятора: типы позволяют более точно описать, спроектировать код и даже быстрее его написать.

Попробуем сравнить такие метрики как

  • Время проектирования (момент, после которого разработчик может больше не соприкасаться с предметной областью, а руководствоваться только ТЗ)

  • Время до выкатки на прод

  • Частота ошибок на проде (и как следствие, ущерб бизнесу и затраты на сопровождение)

Вспомним, что такое тип

  1. Это множество допустимых значений

  2. Это структура, способы хранения в памяти и методы доступа к ней

  3. Это утверждение о данных

Рабочий процесс в Idriss (раз уж я ссылаюсь на эту книгу)

  1. Напишите сигнатуру типа (контракт)

  2. Определите функцию с дыркой (заглушка для компилятора, но в отличие от затычек типа TODO она именованная поэтому IDE умеет с ней делать полезные штуки, подсказывать реализацию)

  3. Используйте разбор случаев через сопоставление с образцом. Компилятор гарантирует тотальность, что все случаи перечислены

  4. Заполним дырки, компилятор поможет исходя из описания. Вне Idris такого нет, но мы можем обратиться к LLM)

  5. Компиляция и проверка

Разберём на задаче, которая удивила меня скрытой сложностью, реализация которой превысила оценки в 3-5 раз.После некоторой дозы адреналинчика от такого я решил потратить еще некоторое время и разобраться как же решать такие задачи за гарантированно короткое время.

Формулировка задачи на пальцах
Обновить справочник, данный в Указе Президента РФ от 31.12.2005 N 1574 (ред. от 02.04.2025) "О Реестре должностей федеральной государственной гражданской службы".
Технические детали.
Написать программу чтения docx, там таблицы, одна начитается с определенного заголовка. 2 столбца. Если объединены, то там ветка иерархии: раздел, подраздел, глава, группа, категория. Если нет, то там имя должности и код.
У должности есть код вида 01-3-3-041, для иерархического представления веткам начиная с самого корня, нужно присвоить код вида 01-3-3-0R0.
Формат aa.b.c.dRe, где aa раздел, e подраздел, d глава, b группа, c категория.
Они могут быть пропущены и для определения что написано в строке мы читаем текст. Например: глава 1. Раздел всегда состоит из 2 строк.
В конце мы должны составить SQL для записи id, названий, parent id, code. 2500 строк в таблице.

Специально не причесывал формулировки - вот так задача пришла.

Пример входа

Наименование должности

Регистрационный номер (код)

Раздел 1

Перечень

должностей в Администрации Президента Российской Федерации

1. Должности категории "руководители"

Высшая группа должностей

Руководитель Администрации Президента Российской Федерации

01-1-1-001

Первый заместитель Руководителя Администрации Президента Российской Федерации

01-1-1-001.1

(введено Указом Президента РФ от 23.12.2008 N 1808)

Заместитель Руководителя Администрации Президента Российской Федерации

01-1-1-002

Пример соответствующего ему выхода

INSERT INTO [table_name] (DICT_ID, DICT_PARENT, REGNUMBER, NAME, NNUMBER, ORCL_ID, S_SECTION_NAME)
VALUES
('aa125016-1fbe-47d0-a855-f29f72588577', NULL, '01-0-0-0R0', 'Раздел 01 Перечень должностей в Администрации Президента Российской Федерации', 1, NULL, 'Перечень должностей в Администрации Президента Российской Федерации'),
('7396d297-e501-4efd-b350-009ff6acb4a6', 'aa125016-1fbe-47d0-a855-f29f72588577', '01-1-0-0R0', '1. Должности категории "руководители"', 2, NULL, 'Раздел 01 Перечень должностей в Администрации Президента Российской Федерации'),
('9c0af301-56d5-4ea5-a7cb-7fc28b5f4992', '7396d297-e501-4efd-b350-009ff6acb4a6', '01-1-1-0R0', 'Высшая группа должностей', 3, NULL, 'Раздел 01 Перечень должностей в Администрации Президента Российской Федерации'),
('5b0c7bf0-a455-420d-b0e9-32d51b8c69e8', '9c0af301-56d5-4ea5-a7cb-7fc28b5f4992', '01-1-1-001', 'Руководитель Администрации Президента Российской Федерации', 4, NULL, 'Раздел 01 Перечень должностей в Администрации Президента Российской Федерации'),
('cc9c0ca8-8367-4fb9-9479-310e5915b3df', '9c0af301-56d5-4ea5-a7cb-7fc28b5f4992', '01-1-1-001.1', 'Первый заместитель Руководителя Администрации Президента Российской Федерации', 5, NULL, 'Раздел 01 Перечень должностей в Администрации Президента Российской Федерации'),
('0a20cc3c-b652-48fd-9bd4-93382826de4a', '9c0af301-56d5-4ea5-a7cb-7fc28b5f4992', '01-1-1-002', 'Заместитель Руководителя Администрации Президента Российской Федерации', 6, NULL, 'Раздел 01 Перечень должностей в Администрации Президента Российской Федерации'),

Плевое дело! )))

Первый раз реализация заняла у меня почти 5 дней на C# (точнее, 3 дня с диким овертаймом). Около 1000 строк кода (с комментариями) и несколько применений тяжелого рефакторинга с переписыванием немалой доли уже написанного.

После овертайма
После овертайма

Попробуем решить ее на Rust по этой методике, по уму.
Сравним с решением без типов на Python.

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

Проектирование

  • Делай раз: Начни с конца (требуемого результата, внешнего API). Определи финальный результат.

  • Делай два: Спроси себя: "Какая функция создаёт этот результат и что ей для этого нужно?"

  • Делай три: Повтори шаг второй для новых входных данных. Продолжи "конвейер типов"

Почему это "driven" (хотя и слабее чем в Idriss) ?

  • Направление: Типы задают направление проектирования. Мы не блуждаем, а целенаправленно движемся от финального типа данных к исходному.

  • Дробление: Требования к типам для каждой функции автоматически дробят большую задачу на маленькие, управляемые подзадачи.

  • Ясность: Вместо того чтобы хранить состояние в переменных (current_sectionhierarchy_stack), мы делаем его явным через типы данных, которые передаются между шагами конвейера. Вся сложность "упакована" в структуру данных, а не размазана по императивному коду.

Шаг 0: Конечная Цель (SQL Запросы)

Мы знаем, что в самом конце нам нужны строки с SQL-запросами.

Утверждение (Тип данных): SQL-запрос — это просто строка.

type SqlQuery = String

Не буду подробно формулировать остальные утверждения, они будут видны в коде

Дальнейшие шаги

  • Шаг 1: Откуда берутся SqlRecord?

  • Шаг 2: Откуда берутся DocumentItem?

  • Шаг 3: Откуда берутся RawTableRow?

  • Шаг 4: Сборка всего вместе (Главная функция). Точка.

Вот рыба кода, которая получилась за полчаса. Добавим к ней нефункциональные требования и получится ТЗ. Если вы засунете этот код в LLM то получите рабочий код реализации. Типы перечислены в порядке проектирования.

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

// 1. SQL-код — просто строка
type Sql = String;

// 2. SQL содержит пакетный INSERT сущностей
fn build_batch_insert(entities: &[Entity]) -> Sql;

// 3. Сущность
type Number  = i32;
type Uid = String;
// Вместо String используем newtype Code с валидацией
pub struct Code(String);

// Формат "aa.b.c.d.xxx" для листьев и "aa.b.c.d.eRf" для веток
// проверка формата (может и не нужно...)
impl Code {
    pub fn new(s: &str) -> Result<Self, CodeError> {
        if regex::Regex::new(r"^\d{2}-\d-\d-\d{3}(?:\.\d+)?$")?.is_match(s) {
            Ok(Self(s.to_string()))
        } else {
            Err(CodeError::InvalidFormat)
        }
    }
}

// тут небольшой фокус: я не знал сразу, каких данных достаточно на вход
// поэтому, тип для состояния определился на второй итерацией 
// по аналогии с find_parent
struct State {
    idx: usize, 
    nodes: &[Entity]
}

// формат aa.b.c.dRe больше определен в функции, а не в типе
// где aa раздел, e подраздел, d глава, b группа, c категория
// смотри BranchCat 
// куски кода (aa, b...) веток определяются из его number и number родителей (выше по иерархии) 
fn get_code (state: State) -> Code 

struct Entity {
    number:     Number,
    uid:    Uid,
    parent: Option<Uid>,
    name:   String,
    code:   Code,// или все таки тут String, а сложную валидацию в типе оставить на этап считывания кодов?
}

// 4. Одна сущность ⇔ один узел
type Node = Entity;// или fn node_to_entity(n: &Node) -> Entity;

// 5. Узел содержит id и name (см. Entity)

// 6. Узел бывает листом (прмежуточный уровень иерархии) 
// или веткой (конечный уровень, должность)
enum NodeKind { Leaf(Leaf), Branch(Branch) }

// 7. Лист (должность)
struct Leaf { name: String, code: Code } 

// 8. Ветка
struct Branch { name: String, cat: BranchCat }

// 9. Категории веток
enum BranchCat { Section, Subsection, Chapter, Category, Group }

// 10. Всю информацию берём из «правильной» таблицы
fn entities_from_proper(t: &ProperTable) -> Vec<Entity>;

// 11. Правильная (очищенная) таблица, приведенная к виду одна строка таблицы ⇔ одна сущность
struct Row { col1: String, col2: String }
struct ProperTable(Vec<Row>);

// 12. Одна строка таблицы ⇔ одна сущность
fn row_to_entity(r: &Row) -> Entity;

// 13. Тип узла определяется содержимым строки
fn detect_kind(r: &Row) -> NodeKind;

// 14. Уровень в иерархии
fn level(k: &NodeKind) -> usize;

// 15. Правило поиска родителя: родитель = последний пришедший элемент, 
// уровень которого ниже уровня текущего
fn find_parent(state: State) -> Option<Uid>;

// 16. Если родителя нет, пишем NULL в SQL
fn parent_or_null(p: Option<Uid>) -> String;

// тут еще добавится: из файла мы вытащим таблицы, выберем нужную и приведем к ProperTable

После проектирования типов 1-9 в ходе проектирования мы допишем сигнатуры функций 10-16 (на самом деле, их значительно больше, ещё штук 5 публичных и штук 20 приватных). По большей части функции вторичны, они очевидны из типов.

Небольшой фокус: я без понятия из чего я буду генерить код, что же подавать на вход для функции get_code? Пока отдам затычку, даже название типа я придумал позднее. Когда я дошел до find_parent я понял, что они похожи, что нужно подать на вход состояние. Какое состояние? Да всё и подам! Весь массив и индекс текущего элемента. Наверное это и есть дырки для типов, которые описаны в книге про Idris.

Функция find_parent. Решение искать его отдельной функцией, а не хранить (попутно определяя его где-то внутри метода парсинга строки) развяжет нам руки и не потребует смешивания разных предметных областей и дальнейшей зависимости от них, на чём я сильно погорел в первый раз.

Я не уверен что на входе массив узлов. На входе может использоваться любая обёртка - массив или список или стек или дерево, любая на которой мы сможем определить операцию getPreviousElement(). Мы можем поиграть с оберткой / контейнером. Но по большому счету лень, оставим массив.

Хотя Rust не имеет таких мощных средств type-driven development как Idris, он использует систему типов для:

  1. Предотвращения ошибок и невалидных состояний данных на этапе компиляции

  2. Гарантий обработки всех возможных случаев через enum и pattern matching

  3. Валидации данных на уровне типов через конструкторы

  4. Четкого разделения ответственности и предотвращения смешивания разных предметных областей

  5. Безопасного рефакторинга (хотя и с некоторым усложнением кода)

Типы + сигнатуры = ТЗ

Мы только что спроектировали всю программу, почти не написав реальной логики.

Post mortem analysis

Почему же в первый подход мне не удалось сделать всё так просто и изящно? Не закончив проектирование я бросался на реализацию, получалось смешение 5 полновесных предметных областей (а в rust добавится еще и необходимость вручную парсить XML внутри DOCX из-за кривых библиотек). Это переполняло мою оперативную память.

Почему же я их смешивал? Почему нарушал принцип единственной ответственности? Как хранить состояние между при переходе к следующей строке? Стек? Дерево? Текущий раздел, подраздел и так для каждого типа? Будем генерить код при парсинге или это ответственность в момент генерации SQL? Многие предположения сломались при переходе к другому куску кода и приходилось переделывать. Мозг забывал что он придумал несколько часов назад в другом месте. В общем, при проектировании нужно оперировать только минимально возможными утверждениями. Type driven development для этого случая идеален.

А вот ООП показывает себя не очень хорошо, хотя казалось бы для statefull ООП идеален. Слишком нелепых абстракций (HierarchyProcessor, PositionProcessor - как только появляется класс с именем "процессор", значит вы уже пошли куда-то не туда). Слишком многословно, слишком сложно рефакторить при необходимости. Посмотрите сами в репозиторий. Тем не менее, это классический Enterprise стиль.

Попробуем без типов

Теперь вопросы кодирования на Python без типов

Для чистоты будем кодировать в словарях - никаких типов! И посмотрим, где же прячутся эти утверждения (1-16).

Кода немного: здесь лишь основные требования. И всё же python вариант компактней. Правда, сами утверждения теперь написаны в комментариях, что заняло много места.

Куда делись утверждения, выраженные в типах?

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

Если я совсем не буду предварительно формулировать утверждения (ТЗ) что произойдет? Правильно, без ТЗ результат ХЗ.

Посмотрим код Python

Краткий LLM generated code
###############################################################################
# 1. «SQL-код  просто строка  type Sql String»
#    Ниже в build_batch_insert мы возвращаем обычный str. Никаких классов.
###############################################################################

###############################################################################
# 2. «SQL содержит пакетный INSERT сущностей
#      fn buildbatchinsertentities Entity Sql»
#    Ф-ция build_batch_insert(entities) берёт список сущностей (dict’ов) и
#    строит пакетный INSERT как строку (см. реализацию ↓).
###############################################################################

###############################################################################
# 3. «Сущность
#       type Number i32
#       type Uid    String
#       type Code   String  (формат aabcdxxx / aabcdeRf)
#     …  struct Entity { number, uid, parent, name, code }»
#    В питоне сущность — просто dict с этими ключами. Проверка кода делается
#    regex’ами, т.к. никакого компилятора-гаранта нет (см. detect_kind).
###############################################################################

###############################################################################
# 4. «Одна сущность  один узел  type Node = Entity»
#    Мы не делаем отдельный тип. Любой dict-Entity выступает узлом.
###############################################################################

###############################################################################
# 5. «Узел содержит id и name см Entity»
#    В dict-Entity есть поля 'uid' (id) и 'name'.
###############################################################################

###############################################################################
# 6. «Узел бывает листом … или веткой …
#      enum NodeKind LeafLeaf | BranchBranch»
#    NodeKind реализован как dict {'kind': 'leaf'|'branch', …}. См. detect_kind.
###############################################################################

###############################################################################
# 7. «Лист  struct Leaf { name String, code String }»
#    Когда kind == 'leaf', в NodeKind храним name и code ровно так.
###############################################################################

###############################################################################
# 8. «Ветка  struct Branch { name String, cat BranchCat }»
#    Когда kind == 'branch', в NodeKind лежат name, code, cat.  См. detect_kind.
###############################################################################

###############################################################################
# 9. «Категории веток enum BranchCat Section|Subsection|Chapter|Category|Group»
#    В коде это просто строка из заданного набора (см. _CAT_BY_DEPTH).
###############################################################################

###############################################################################
# 10. «Всю информацию берём из правильной таблицы
#       fn entitiesfrompropert ProperTable VecEntity»
#     Ф-ция entities_from_proper(table) принимает список Row и выдаёт список
#     Entity-dict’ов.
###############################################################################

###############################################################################
# 11. «Правильная очищенная таблица:
#        struct Row { col1 String, col2 String }
#        struct ProperTable = Vec<Row>»
#     В питоне Row — dict, ProperTable — list[dict]. Ключи любые,
#     ниже используем 'number','uid','name','code'.
###############################################################################

###############################################################################
# 12. «Одна строка таблицы  одна сущность
#       fn rowtoentityr Row Entity»
#     Реализовано функцией row_to_entity(row).
###############################################################################

###############################################################################
# 13. «Тип узла определяется содержимым строки
#       fn detectkindr Row NodeKind»
#     Реализовано функцией detect_kind(row) по regex кода.
###############################################################################

###############################################################################
# 14. «Уровень в иерархии  fn levelk NodeKind usize»
#     Реализовано функцией level(kind) — количество точек в коде.
###############################################################################

###############################################################################
# 15. «Правило поиска родителя:
#       родитель — последний пришедший элемент,
#       уровень которого ниже уровня текущего
#       fn findparentidx usize nodes Entity OptionUid»
#     Логика живёт в _fill_parents() и find_parent().
###############################################################################

###############################################################################
# 16. «Если родителя нет  пишем NULL в SQL
#       fn parentornullp OptionUid String»
#     Реализовано функцией parent_or_null(uid) — None → 'NULL'.
###############################################################################

import re
from itertools import takewhile

# ---------------- Регулярки, справочники (вспом. для 3,6,7,8,9) --------------

_LEAF_RE   = re.compile(r"^[a-z]{2}(?:\.[a-z]){3}\.[a-z]{3}$")     # формат листа
_BRANCH_RE = re.compile(r"^[a-z]{2}(?:\.[a-z]){3}\.[a-z][A-Z]f$")  # формат ветки

# depth (0..4) → категория ветки             #9
_CAT_BY_DEPTH = {0: "Section",
                 1: "Subsection",
                 2: "Chapter",
                 3: "Category",
                 4: "Group"}

# --------------------------- 14. уровень -------------------------------------

def level(kind):                                         #14
    """NodeKind → целое: сколько «.» в коде."""
    return kind["code"].count(".")

# ------------------------- 16. None → SQL NULL --------------------------------

def parent_or_null(uid):                                 #16
    return "NULL" if uid is None else f"'{uid}'"

# ------------------------- 13. строка → NodeKind ------------------------------

def detect_kind(row):                                    #13
    """
    Row → dict-NodeKind.
    Либо {'kind':'leaf',   name, code}
         {'kind':'branch', name, code, cat}
    """
    code = row["code"]

    if _LEAF_RE.fullmatch(code):
        return {"kind": "leaf", "name": row["name"], "code": code}          #7
    if _BRANCH_RE.fullmatch(code):
        depth = code.count(".") - 4
        return {"kind": "branch",
                "name": row["name"],
                "code": code,
                "cat": _CAT_BY_DEPTH.get(depth, "Group")}                   #8/9
    raise ValueError(f"Bad code format: {code}")                             #3

# --------------------- 12. Row → Entity (без parent) -------------------------

def row_to_entity(row):                                   #12
    """
    Делает Entity-dict из Row. parent будет заполнен позже.
    Здесь код сильно упрощен по сравнению с реальным!
    Но все равно из-за совмещения нескольких требований (3, 5, 12) длина кода сократилась
    Содержит:
        number, uid, parent, name, code                    #3,5
    """
    return {
        "number": row["number"],      #3 Number → int
        "uid":    row["uid"],         #3 Uid → str
        "parent": None,               # будет заполнен _fill_parents  #15
        "name":   row["name"],        #5
        "code":   row["code"],        #3 Code → str
    }

# --------------- 15. заполняем parent по правилу «последний ниже» -------------

def _fill_parents(entities):                              #15
    """
    Правило: родитель — последний элемент с уровнем < текущего.
    Реализовано стеком (level, uid).
    """
    stack = []  # [(level, uid)]
    for e in entities:
        cur_lvl = e["code"].count(".")
        # убираем из стека всё не меньше текущего уровня
        while stack and stack[-1][0] >= cur_lvl:
            stack.pop()

        e["parent"] = stack[-1][1] if stack else None
        stack.append((cur_lvl, e["uid"]))

def find_parent(idx, ents):                               #15 (публичная)
    """Отдаём uid родителя или None."""
    return ents[idx]["parent"]

# ------------------------------ 10. ProperTable → Vec<Entity> -----------------

def entities_from_proper(table):                          #10
    """
    Принимает ProperTable (list[Row])  #11
    Возвращает list[Entity] со стоящими parent-uid.
    """
    entities = [row_to_entity(r) for r in table]          #12
    _fill_parents(entities)                               #15
    return entities

# ------------------------------ 2. Entity-list → SQL str ----------------------

# айай, нужен get_code а его нет
# ну просто прибавим еще 30-50 строк
              
def build_batch_insert(entities):                         #2  (и 1)
    """
    Принимает list[Entity]                                #3
    Возвращает str со INSERT … VALUES  (…) , (…) ;        #1
    """
    values = ",\n".join(
        f"({e['number']}, '{e['uid']}', {parent_or_null(e['parent'])}, "
        f"'{e['name']}', '{e['code']}')"
        for e in entities
    )
    return (
        "INSERT INTO entities "
        "(number, uid, parent_uid, name, code)\nVALUES\n" + values + ";"
    )

# ------------------------------ Демонстрация ----------------------------------

if __name__ == "__main__":
    # 11. ProperTable = список Row, Row = dict с колонками строки
    proper_table = [
        {"number": 1, "uid": "u1", "name": "Sec A",  "code": "aa.b.c.d.abc"},
        {"number": 2, "uid": "u2", "name": "Sub A",  "code": "aa.b.c.d.abc"},
        {"number": 3, "uid": "u3", "name": "Post X", "code": "aa.b.c.d.eRf"},
    ]

    ents = entities_from_proper(proper_table)   #10
    sql  = build_batch_insert(ents)             #2
    print(sql)

Критерий

Python (dict, без типов)

Rust (типизация, ФП)

C# (типизация, ООП)

C# (без проектирования)

Проектирование (часов)

2

5

6

0

Реализация без LLM (часов) до первого сквозного теста

7

10 (+ 2 часа на парсинг docx)

9

12

Тестирование и отладка (часов)

8-16 как повезет

3-6

3-6

8-20 как повезет

Итого время до продакшна

17-25

18-21 (+2)

18-21

20-32

Количество багов на проде

10-20

2-5

2-5

5-10

Цифры - что-то по памяти, что-то оценочное, но близко к реальности. Кода на самом деле больше, я уж не стал выписывать все подробности - на Python порядка 500 строк. C#, Rust - 800.

Выводы:

  1. При определённых навыках и квалификации вы будете писать на Python быстрее, но только в знакомой предметной области! Быстрее, только если вы уже знаете где расставлены ловушки и уже знаете половину ответа. Проектирование в типах даст вам более предсказуемое время реализации даже в сложной и незнакомой предметной области.

  2. Код Python простой и читаемый до определенного предела - пока код не вырос и к разработке не подключились другие программисты.

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

  4. Rust все же более многословный и шумный язык, но это почти не сказывается на проектировании, только на последних стадиях.Для проектирования сложной логики он почти так же прекрасен как Scala )

  5. Rust очень слаб в библиотеках работы с docx, xlsx, поэтому усложняет чтение файла, да и борьба с жесткими правилами владения тоже отнимает время. Это дополнительное время в таблице не указывал.

  6. C# гораздо лучше в библиотеках, но ООП вносит сумятицу на стадии проектирования: проектирование в классах и менее компактное и создает проблемы разделения ответственности при обеспечении консистентности данных нескольких сущностей и неуклюжие способы устранения зависимостей. Проявились ли недостатки в таком коде? Да, но слабо, видимо масштаб не столь велик.

  7. Вручную задача была бы выполнена быстрее (всего то 2500 позиций справочника), но количество ошибок из-за ручной работы неприемлемо велико

Что же будет если объём кода вырастет до 3000-5000 строк на один логически связанный процесс?

  1. Скрытая бизнес логика

  2. Невозможность полноты тестирования. Вы будете постоянно жить в дебаггере!

  3. Невозможность удержать код в голове во время ревью.

В таком случае уже можно сказать однозначно: python проиграет даже если использовать его средства "типизации" и инструментарий.

Пишите в комментариях если хотите завершенный код Rust. Хотя мне и так все ясно.

Комментарии (3)


  1. vzvzvz
    19.06.2025 08:29

    Начали с упоминания малоизвестного языка. Потом сразу сменили тему.

    Только ли код вам LLM генерили?


    1. Dhwtj Автор
      19.06.2025 08:29

      Вы хотите примеры на Idriss? зачем?

      Книга про Idriss, но я пытаюсь понять как это применить на практике с максимальной пользой

      На C# решение рабочее - см репо

      На Rust типы в статье, есть черновик кода - он вам нужен?

      На Python просто посмотреть не проще ли без типов. Проще. Но хуже.


      1. vzvzvz
        19.06.2025 08:29

        Я не имел в виду всё, что вы тут придумали.