# Масштабное проектирование: Решение проблемы некоммутативных преобразований в breakshaft ## Содержание 1. [Постановка проблемы](#1-постановка-проблемы) 2. [Анализ текущей ситуации](#2-анализ-текущей-ситуации) 3. [Варианты решений](#3-варианты-решений) 4. [Сравнительная таблица](#4-сравнительная-таблица) 5. [Рекомендации](#5-рекомендации) --- ## 1. Постановка проблемы ### 1.1. Что такое некоммутативность в breakshaft? **Некоммутативное преобразование** — ситуация, когда существует несколько путей преобразования типов, дающих **разные результаты**. #### Пример: ```python @repo.mark_injector() def int_to_a_v1(i: int) -> A: return A(i * 10) # Путь 1: A(420) из int=42 @repo.mark_injector() def int_to_a_v2(i: int) -> A: return A(i + 100) # Путь 2: A(142) из int=42 def consumer(dep: A) -> int: return dep.a # Два пути дают разные результаты: 420 vs 142 ``` ### 1.2. Почему это проблема? | Аспект | Проблема | |--------|----------| | **Детерминизм** | Один и тот же код может давать разные результаты | | **Отладка** | Сложно понять, какой путь был выбран | | **Предсказуемость** | Поведение зависит от внутреннего порядка обхода графа | | **Тестирование** | Тесты могут проходить/падать недетерминированно | ### 1.3. Где возникает в коде? ``` src/breakshaft/ ├── graph_walker.py │ ├── generate_callgraph() # Построение графа │ ├── explode_callgraph_branches() # Комбинаторный взрыв вариантов │ └── filter_exploded_callgraph_branch() # Фильтрация (выбор пути) └── convertor.py └── get_callseq() # Проверка force_commutative ``` **Критическое место** — `filter_exploded_callgraph_branch()`: - Использует эвристики для выбора пути - Порядок обхода не гарантирован - При `force_commutative=True` выбрасывает ошибку если >1 пути --- ## 2. Анализ текущей ситуации ### 2.1. Текущий алгоритм выбора пути ```python # graph_walker.py: filter_exploded_callgraph_branch() # Эвристики (применяются последовательно): template_metrics = [ lambda x: len(x.consumed_from_types), # 1. Максимум потреблённых типов lambda x: x.consumed_cumsum, # 2. Максимум кумулятивного потребления lambda x: -x.invokes, # 3. Минимум вызовов ] # Если после фильтрации >1 варианта: if len(variants) > 1: # Сортировка по имени функции (недетерминировано!) variants.sort(key=lambda x: universal_qualname(x.injector.fn)) ``` ### 2.2. Проблемы текущей реализации | Проблема | Описание | Влияние | |----------|----------|---------| | **P1. Недетерминированная сортировка** | `universal_qualname()` не гарантирует порядок | Разные результаты на разных машинах | | **P2. Эвристики не семантические** | Выбор по метрикам графа, не по логике | Может выбрать "неправильный" путь | | **P3. Комбинаторный взрыв** | `explode_callgraph_branches()` генерирует все варианты | O(n!) сложность | | **P4. Нет кэширования** | Граф пересчитывается каждый раз | Усугубляет P3 | | **P5. Нет явного приоритета** | Все инжекторы равны | Невозможно указать "предпочтительный" путь | ### 2.3. Статистика (из тестов) ``` test_performance_many_injectors: 20 инжекторов → ~0.5 сек test_performance_many_injectors: 50 инжекторов → TIMEOUT (комбинаторный взрыв) ``` --- ## 3. Варианты решений ### Вариант 1: Явные приоритеты инжекторов #### Описание Добавить параметр `priority` к `mark_injector()`. При выборе пути предпочитать инжекторы с высшим приоритетом. #### Реализация ```python # Использование @repo.mark_injector(priority=10) # Высокий приоритет def int_to_a_preferred(i: int) -> A: return A(i * 10) @repo.mark_injector(priority=1) # Низкий приоритет def int_to_a_fallback(i: int) -> A: return A(i + 100) # Изменения в моделях @dataclass(frozen=True) class ConversionPoint: fn: Callable injects: type # ... priority: int = 0 # Новый параметр # Изменения в graph_walker.py def filter_exploded_callgraph_branch(variants, priority_injectors=None): # Сортировка по приоритету variants.sort(key=lambda x: -x.injector.priority) ``` #### Сильные стороны | + | Описание | |---|----------| | **Контроль** | Разработчик явно указывает предпочтения | | **Детерминизм** | Приоритеты дают однозначный выбор | | **Гибкость** | Можно менять приоритеты без изменения кода | | **Обратная совместимость** | `priority=0` по умолчанию не ломает существующий код | #### Слабые стороны | - | Описание | |---|----------| | **Сложность API** | Новый параметр для изучения | | **Конфликты приоритетов** | Одинаковые приоритеты → снова недетерминизм | | **Не решает комбинаторный взрыв** | Всё ещё генерируются все варианты | | **Субъективность** | Приоритеты могут быть произвольными | #### Оценка сложности - **Код**: ~50 строк изменений - **Тесты**: ~10 новых тестов - **Риск**: Низкий --- ### Вариант 2: Именованные пути (Named Paths) #### Описание Разработчик явно именовывает пути преобразования и выбирает их по имени. #### Реализация ```python # Регистрация именованных путей @repo.mark_path(name="multiply") def int_to_a_mult(i: int) -> A: return A(i * 10) @repo.mark_path(name="add") def int_to_a_add(i: int) -> A: return A(i + 100) # Выбор пути при использовании fn = repo.get_conversion( (int,), consumer, path_preference="multiply" # Явный выбор пути ) # Или глобальная настройка repo.set_path_preference("multiply") ``` #### Сильные стороны | + | Описание | |---|----------| | **Полный контроль** | Разработчик всегда выбирает путь | | **Читаемость** | Код явно показывает какой путь используется | | **Документированность** | Имена путей служат документацией | | **Тестируемость** | Можно тестировать разные пути явно | #### Слабые стороны | - | Описание | |---|----------| | **Бойлерплейт** | Нужно именовать каждый путь | | **Сложность** | Управление именами в больших проектах | | **Конфликты имён** | Нужна проверка уникальности | | **Не решает комбинаторный взрыв** | Генерация всех вариантов остаётся | #### Оценка сложности - **Код**: ~150 строк изменений - **Тесты**: ~15 новых тестов - **Риск**: Средний --- ### Вариант 3: Стратегии выбора пути (Path Selection Strategies) #### Описание Встроенные стратегии выбора пути с возможностью расширения. #### Реализация ```python from breakshaft import PathStrategy # Встроенные стратегии: # - SHORTEST: кратчайший путь (минимум вызовов) # - LONGEST: длиннейший путь (максимум преобразований) # - FIRST: первый найденный (быстро, недетерминировано) # - EXPLICIT: только явные пути (ошибка если несколько) # - CUSTOM: пользовательская функция fn = repo.get_conversion( (int,), consumer, path_strategy=PathStrategy.SHORTEST # или PathStrategy.LONGEST ) # Пользовательская стратегия def my_strategy(paths: list[Path]) -> Path: # Логика выбора return min(paths, key=lambda p: p.complexity) fn = repo.get_conversion( (int,), consumer, path_strategy=PathStrategy.CUSTOM(my_strategy) ) ``` #### Сильные стороны | + | Описание | |---|----------| | **Гибкость** | Разные стратегии для разных случаев | | **Расширяемость** | Пользовательские стратегии | | **Явность** | Стратегия видна в коде вызова | | **Переиспользование** | Стратегии можно переиспользовать | #### Слабые стороны | - | Описание | |---|----------| | **Сложность API** | 5+ стратегий для изучения | | **Не решает комбинаторный взрыв** | Стратегия применяется после генерации | | **Производительность** | Некоторые стратегии дорогие | #### Оценка сложности - **Код**: ~200 строк изменений - **Тесты**: ~20 новых тестов - **Риск**: Средний --- ### Вариант 4: Ограничение глубины графа (Depth Limiting) #### Описание Ограничить максимальную глубину/сложность графа преобразований. #### Реализация ```python repo = ConvRepo( max_depth=5, # Максимум 5 преобразований в цепочке max_branches=10, # Максимум ветвей в узле max_total_paths=100 # Максимум путей для рассмотрения ) # Или при вызове fn = repo.get_conversion( (int,), consumer, max_depth=3 # Переопределение для конкретного вызова ) ``` #### Сильные стороны | + | Описание | |---|----------| | **Защита от взрыва** | Гарантированная верхняя граница сложности | | **Производительность** | Предсказуемое время выполнения | | **Простота** | Один параметр для настройки | #### Слабые стороны | - | Описание | |---|----------| | **Ограничения** | Может отсечь валидные пути | | **Не детерминизм** | Не решает проблему выбора пути | | **Настройка** | Нужно подбирать значения | #### Оценка сложности - **Код**: ~80 строк изменений - **Тесты**: ~8 новых тестов - **Риск**: Низкий --- ### Вариант 5: Кэширование графов (Graph Caching) #### Описание Кэшировать построенные графы преобразований для повторного использования. #### Реализация ```python from breakshaft import LRUCache, PersistentCache # Кэш в памяти (LRU) repo = ConvRepo(cache=LRUCache(max_size=1000)) # Персистентный кэш (на диске) repo = ConvRepo(cache=PersistentCache(path=".breakshaft_cache")) # Декоратор для кэширования @repo.cached def get_converter(from_types, to_type): return repo.get_conversion(from_types, to_type) ``` #### Сильные стороны | + | Описание | |---|----------| | **Производительность** | Повторные вызовы мгновенные | | **Масштабируемость** | Работает с большим числом инжекторов | | **Прозрачность** | Кэш прозрачен для пользователя | #### Слабые стороны | - | Описание | |---|----------| | **Память** | Кэш потребляет память | | **Инвалидация** | Сложность при изменении инжекторов | | **Не решает выбор пути** | Кэширует выбранный путь, но не детерминирует выбор | #### Оценка сложности - **Код**: ~250 строк изменений - **Тесты**: ~25 новых тестов - **Риск**: Высокий (состояние, гонки) --- ### Вариант 6: Статический анализ графа (Static Graph Analysis) #### Описание Анализировать граф на этапе регистрации инжекторов, обнаруживать проблемы заранее. #### Реализация ```python # Предварительный анализ repo = ConvRepo(validate_on_register=True) @repo.mark_injector() def int_to_a_v1(i: int) -> A: return A(i * 10) @repo.mark_injector() def int_to_a_v2(i: int) -> A: return A(i + 100) # Warning: Ambiguous path detected! # Явная валидация warnings = repo.validate() for w in warnings: print(f"Warning: {w}") # Warning: Ambiguous path for A: 2 injectors found # Разрешение конфликта repo.resolve_ambiguity(A, preferred=int_to_a_v1) ``` #### Сильные стороны | + | Описание | |---|----------| | **Раннее обнаружение** | Ошибки на этапе регистрации | | **Документированность** | Явное разрешение конфликтов | | **Безопасность** | Невозможно создать неоднозначность | #### Слабые стороны | - | Описание | |---|----------| | **Сложность** | Анализ графа дорог | | **Жёсткость** | Может быть слишком ограничительно | | **Не решает комбинаторный взрыв** | Анализ добавляет overhead | #### Оценка сложности - **Код**: ~300 строк изменений - **Тесты**: ~30 новых тестов - **Риск**: Высокий --- ### Вариант 7: Версионирование путей (Path Versioning) #### Описание Каждый путь имеет версию, можно выбирать конкретную версию. #### Реализация ```python @repo.mark_injector(version="1.0") def int_to_a(i: int) -> A: return A(i * 10) @repo.mark_injector(version="2.0") # Новая версия def int_to_a(i: int) -> A: return A(i + 100) # Выбор версии fn = repo.get_conversion( (int,), consumer, path_version="1.0" # Использовать старую версию ) # Или диапазон fn = repo.get_conversion( (int,), consumer, path_version=">=1.0,<3.0" ) ``` #### Сильные стороны | + | Описание | |---|----------| | **Эволюция** | Плавный переход между версиями | | **Совместимость** | Старый код продолжает работать | | **Контроль** | Явный выбор версии | #### Слабые стороны | - | Описание | |---|----------| | **Сложность** | Управление версиями | | **Бойлерплейт** | Версии для каждого пути | | **Не решает комбинаторный взрыв** | Все версии генерируются | #### Оценка сложности - **Код**: ~200 строк изменений - **Тесты**: ~20 новых тестов - **Риск**: Средний --- ### Вариант 8: Комбинированный подход (Hybrid Solution) #### Описание Комбинация нескольких подходов для максимального эффекта. #### Реализация ```python repo = ConvRepo( # Приоритеты по умолчанию default_priority=0, # Кэширование cache=LRUCache(max_size=500), # Ограничения max_depth=10, max_total_paths=1000, # Стратегия по умолчанию default_strategy=PathStrategy.SHORTEST, # Валидация validate_on_register=True, ) # Гибкое переопределение fn = repo.get_conversion( (int,), consumer, priority_override={int_to_a_v1: 10}, # Приоритет для конкретных strategy=PathStrategy.EXPLICIT, cache_key="my_custom_key" ) ``` #### Сильные стороны | + | Описание | |---|----------| | **Максимальная гибкость** | Все инструменты доступны | | **Масштабируемость** | Работает с большими графами | | **Контроль** | Полный контроль над поведением | #### Слабые стороны | - | Описание | |---|----------| | **Сложность** | Много параметров для настройки | | **Обучение** | Крутая кривая обучения | | **Риск ошибок** | Неправильная конфигурация | #### Оценка сложности - **Код**: ~500 строк изменений - **Тесты**: ~50 новых тестов - **Риск**: Высокий --- ### Вариант 9: Декларативное описание графа (Declarative Graph) #### Описание Полностью декларативное описание путей преобразования вместо автоматического вывода. #### Реализация ```python # Декларативное описание repo.define_graph({ "paths": [ { "name": "multiply_path", "steps": [ {"from": int, "to": A, "using": int_to_a_mult}, {"from": A, "to": B, "using": a_to_b}, ], "priority": 10 }, { "name": "add_path", "steps": [ {"from": int, "to": A, "using": int_to_a_add}, ], "priority": 1 } ] }) # Выбор пути по имени fn = repo.get_conversion( (int,), consumer, path_name="multiply_path" ) ``` #### Сильные стороны | + | Описание | |---|----------| | **Полный контроль** | Явное описание всех путей | | **Детерминизм** | Никакой неявной логики | | **Документированность** | Граф виден в коде | | **Нет комбинаторного взрыва** | Только явные пути | #### Слабые стороны | - | Описание | |---|----------| | **Бойлерплейт** | Много кода для описания | | **Потеря автоматизма** | Нет автоматического вывода путей | | **Сложность поддержки** | Изменение графа требует правки описания | #### Оценка сложности - **Код**: ~400 строк изменений - **Тесты**: ~40 новых тестов - **Риск**: Высокий (меняет парадигму) --- ### Вариант 10: Машинное обучение для выбора пути (ML-Based Selection) #### Описание Использовать ML для предсказания "лучшего" пути на основе истории использования. #### Реализация ```python from breakshaft import MLPathSelector repo = ConvRepo( path_selector=MLPathSelector( training_data="usage_history.json", features=["execution_time", "memory_usage", "success_rate"] ) ) # ML выбирает путь на основе: # - Истории успешных выполнений # - Времени выполнения # - Потребления памяти # - Контекста (типы, размеры данных) fn = repo.get_conversion((int,), consumer) # Путь выбирается автоматически на основе модели ``` #### Сильные стороны | + | Описание | |---|----------| | **Адаптивность** | Учится на использовании | | **Оптимизация** | Выбирает эффективные пути | | **Автоматизм** | Не требует ручной настройки | #### Слабые стороны | - | Описание | |---|----------| | **Сложность** | ML модель + обучение | | **Непредсказуемость** | ML может выбрать неожиданно | | **Зависимость от данных** | Нужна история для обучения | | **Overhead** | Предсказание модели | #### Оценка сложности - **Код**: ~600 строк изменений - **Тесты**: ~60 новых тестов - **Риск**: Очень высокий --- ## 4. Сравнительная таблица | Вариант | Детерминизм | Производительность | Сложность | Обратная совместимость | Риск | |---------|-------------|-------------------|-----------|----------------------|------| | **1. Приоритеты** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | Низкий | | **2. Именованные пути** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | Средний | | **3. Стратегии** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | Средний | | **4. Ограничение глубины** | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | Низкий | | **5. Кэширование** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Высокий | | **6. Статический анализ** | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | Высокий | | **7. Версионирование** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | Средний | | **8. Комбинированный** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Высокий | | **9. Декларативный** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | Высокий | | **10. ML** | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Очень высокий | ### Легенда: - ⭐⭐⭐⭐⭐ — Отлично - ⭐⭐⭐⭐ — Хорошо - ⭐⭐⭐ — Удовлетворительно - ⭐⭐ — Плохо - ⭐ — Очень плохо --- ## 5. Рекомендации ### 5.1. Краткосрочные решения (быстрая победа) **Вариант 1 + Вариант 4**: Приоритеты + Ограничение глубины ```python # Минимальные изменения для детерминизма @repo.mark_injector(priority=10) def preferred_converter(...): ... repo = ConvRepo(max_depth=10) ``` **Преимущества:** - ~130 строк кода - Низкий риск - Обратная совместимость - Решает 80% проблем ### 5.2. Среднесрочные решения (баланс) **Вариант 3 + Вариант 5**: Стратегии + Кэширование ```python repo = ConvRepo( cache=LRUCache(1000), default_strategy=PathStrategy.SHORTEST ) ``` **Преимущества:** - Хорошая производительность - Гибкость для пользователей - Решает проблему комбинаторного взрыва ### 5.3. Долгосрочные решения (полное решение) **Вариант 8 (Комбинированный) с элементами Варианта 9** ```python repo = HybridConvRepo( priorities=True, strategies=True, cache=True, validation=True, declarative_mode=False # Опционально ) ``` **Преимущества:** - Полное решение проблемы - Масштабируемость - Гибкость ### 5.4. Дорожная карта ``` Фаза 1 (2 недели): ├── Приоритеты инжекторов ├── Ограничение глубины └── Тесты Фаза 2 (4 недели): ├── Стратегии выбора пути ├── Базовое кэширование └── Документация Фаза 3 (6 недель): ├── Статический анализ ├── Продвинутое кэширование ├── Декларативный режим (опционально) └── Полное тестирование ``` --- ## 6. Заключение ### 6.1. Выводы 1. **Нет серебряной пули** — каждый вариант имеет компромиссы 2. **Комбинированный подход** даёт лучший результат 3. **Начинать с простого** — приоритеты + ограничения 4. **Итеративное улучшение** — добавлять функции постепенно ### 6.2. Риски | Риск | Вероятность | Влияние | Митигация | |------|-------------|---------|-----------| | Ломает обратную совместимость | Низкая | Высокое | Поэтапное внедрение | | Усложнение API | Средняя | Среднее | Хорошая документация | | Производительность | Низкая | Высокое | Бенчмарки на каждом этапе | | Комбинаторный взрыв | Средняя | Высокое | Ограничения + кэш | ### 6.3. Следующие шаги 1. **Выбрать подход** для Фазы 1 2. **Создать PR** с приоритетами и ограничениями 3. **Собрать фидбэк** от пользователей 4. **Итеративно улучшать** --- *Документ создан для breakshaft v0.1.6* *Дата: 2026-03-28*