Обобщенное программирование

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

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

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

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

Выбранный подход по синтаксису более всего похож на реализованный в языке Julia (т.е. нет отдельного синтаксиса для шаблонов), но позволяет определять конкретные специалиазции как в C++, Rust во время компиляции, а для динамической типищации используется динамическая диспетчеризация выбора конкретной релиазции, что похоже на применения обощеного программирования в Pyton с использованиям C/C++ библиотек.

Имена функций подчиняются общим правилами именования объектов, поэтому её имя является единой “точкой входа” для выбора конкретной реализации машинного кода (динамиечская диспетчеризация), но при отсуствии нужной специалиазции она не генерируется, а выбирается наиболее подходящая из специализированных ранее. Но такой подход не мешает динамически создавать специалиазаии, если среда выполнения поддерживает JIT компиляцию.

Однако обратная сторона прямой иинтеграции с нативным кодом С++ делает необходимым наличие нативного кода сразу нескольких функции с типизированнми аргументами (с так называемым манглингом имен для С++). Из-за этого в языке реализована следующая модель обощеного программирования:

Модель обощеного программирования

⚠️ НЕ РЕАЛИЗОВАНО: описанная модель обобщённого программирования (обобщённая функция-диспетчер с приёмом аргументов в виде словаря, рантайм-диспетчеризация по типам, специализация <:T1-T2> / <:T1+T2>, отдельный синтаксис шаблонов) в текущей версии НЕ реализована в описанном виде. Обобщения реализуются через типовые параметры и подстановку в методах коллекций/диапазонов (см. include/types/MEMORY.md), а не через рантайм-диспетчер

Каждая функция языка имеет как миниму две части машинного кода.

  • Обобщенная функция диспетчер - самая выскооуровневая функция - точка входа, которая принимает любые не типизированные аргументы в виде одного словаря аргументов. Её назначение - реализация возможности вызова любой функции в REPL с динамической типизацией аргументов при любом количествае передаваемых агрументов. Анализ переданных аргументов и вбыор нужной нативной реалиазции происходит в рантайме.
  • Как минимум одна нативная релаиазция типизированной функции, которую может вызвать выскооруровневая функция фиспетчер после разкрытия и анализаи переданных.

Такая архитерутра вызовов позвоялет с одной тороны, сохранить для рантайма полную информацию о типах данных и именах аргументов при вызове функции. Но это делается за счет анализа типов данных во время выполнения.

Тогда как для функции с типизированными параметрами всегда остается возможность прямого вызова машинного кода с минимальными накладными расходами. Но это происходит за счет потери информации времени компиляции о типах и именах аргументов.

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

Примеры

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

    # Типы аргументов не проверяются (функцию можно вызвать с любыми типами)
    def max_val(a, b):
        if a > b:
            return a
        else:
            return b

    # Одна функция и проверка типов аргументов производится в ранатайме 
    # как и обработка возможных ошибок из-за несовместимых типов
    max_val(3, 5)
    max_val(3.0, 5.0)
    max_val("a","b")

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

    // Проверка типов во время компиляции
    template<typename T>
    T max_val(T a, T b) {
        return a > b ? a : b;
    }

    // Во время выполнения нет информации о типах
    // Отдельная реализация для каждого типа данных
    // и применения манглинга имен для выбора нужной функции 
    max_val<int>(3, 5);          // генерирует: int max_val(int, int)
    max_val<double>(3.0, 5.0);   // генерирует: double max_val(double, double)
    max_val<std::string>("a","b");// генерирует: string max_val(string, string)

Для реализация первого типа обощенного программирования в стиле Python выполняется очень просто. Достаточно не указывать конкретный тип данных (или указать :Any, т.е. любой тип), тогда компилятор создаст машинный код функции, аргументы которой принимают любой тип объекта. Соотвественно и проверка возможности их сравнения будт выполняться только в рантайме.

    # Типы аргументов не проверяются (функцию можно вызвать с любыми типами)
    @max_val(a, b: Any) := {
        @if( a > b) {
            return a;
        }
        return b;
    };

    # Одна функция и проверка типов аргументов производится в ранатайме 
    # как и обработка возможных ошибок из-за несовместимых типов
    max_val(3, 5)
    max_val(3.0, 5.0)
    max_val("a","b")

Однако при конкретизации обработываемых типов, реализация приближается к используемой в C++

    # Обработываемые типы данных
    :MaxTypes := <Integer, Double, Any>;
    # Проверка типов происходит во время компиляции
    max_val( a:MaxTypes, b:MaxTypes):MaxTypes := {
        @if( a > b) {
            @return a;
        }
        @return b;
    };

    # Для специалиазций создаются отдельные реализации для каждого типа
    max_val(3, 5);      #  вызывается int max_val(int, int)
    max_val(3.0, 5.0);  #  вызывается double max_val(double, double)

    # Для остальных тьипов вызывается специалиазции Any,
    # у которой присуствует информации о типах данных
    # Проверка типов аргументов производится в ранатайме 
    # как и обработка возможных ошибок из-за несовместимых типов
    max_val("a","b");

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

    # Типы аргументов не проверяются (функцию можно вызвать с любыми типами)
    @max_val(a, b) := {
        @if( a > b) {
            return a;
        }
        return b;
    };

    # Одна функция и проверка типов аргументов производится в ранатайме 
    # как и обработка возможных ошибок из-за несовместимых типов
    array := :Int32[100](... rand() ...); # заполнение тензора значениями функции rand() 
    max := array[0];
    index := 1;
    while(index < len(array)){
        max = max_val(max, array[index]); # Вызов функции max_val с типизированными аргументами
        # функция не только можен быть типизирована автоматически, но и в случае аггресивной 
        # оптимизации копидлятор может полностью раскрыть вызов функции в инлайн код
        index += 1;
    }
    @print(max);