Как реализовать паттерн newtype в Julia?

Вопрос или проблема

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

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

Одним из таких языков является Rust, который имеет концепцию кортежных структур.

struct SecurityId(i64);

struct SecurityInMarketId(i64);

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

Кстати, если вы знаете альтернативные названия для этого паттерна, пожалуйста, оставьте комментарий.

В Julia есть псевдонимы типов.

const MyType = DataFrame

К сожалению, это не пример сильной типизации, потому что Julia не рассматривает MyType как независимый тип от DataFrame. Это действительно просто псевдоним имени.

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

function exampleFunction(df::DataFrame)
    println("DataFrame")
end

function exampleFunction(df::MyType)
    println("MyType")
end

Запуск этого кода приведет к созданию 1 функции exampleFunction с только 1 (а не 2) методами. Если вы выполните

julia> exampleFunction(DataFrame())
MyType

как показано выше, будет напечатано MyType. Это демонстрирует, что вторая реализация метода exampleFunction заменила оригинальное определение.

В C++, насколько я помню, единственный способ реализовать этот паттерн сильной типизации – это обернуть тип в структуру (или класс).

class SecurityInMarketId {
    private:
    int _security_in_market_id;
};

В стороне: На самом деле есть и другие способы сделать это с помощью метапрограммирования шаблонов. Однако этот подход, вероятно, усложнит понимание и отладку. Это не простое решение, как предоставляет Rust.


Единственный способ, который я нашел сделать это в Julia, по сути такой же. Обернуть существующий тип в структуру.

mutable struct SecurityInMarketId
    _security_in_market_id::Int64
end

Этот подход имеет недостатки. В то время как он предоставляет новый тип для системы типов, необходимо сделать выбор из двух компромиссов:

  1. Доступ к (предположительно) «подразумеваемым» приватным данным
  2. Переопределить все необходимые функции для вновь определенного структурного типа SecurityInMarketId. Например, если мы оборачиваем DataFrame, нам, вероятно, потребуется реализация функций для доступа к элементам, фильтрации, выбора столбцов и строк, чтения и записи в файл и т. д. Может быть большое количество функций, которые требуют новых реализаций просто для использования функций внутреннего типа данных, который мы обернули в структуру.

Опция 2 явно требует значительного количества шаблонного кода для написания. Код, который не имеет функциональности, просто перенаправляет вызовы функций к внутренним данным структуры.

Опция 1 приводит к «уродливому» внешнему виду API. Например

println("SecurityInMarketId=$(security_in_market_id._security_in_market_id)")

Каков будет канонический способ реализовать это в Julia? (Или это просто обертывание в структуру и принятие одного из двух вышеуказанных компромиссов?)


Почему паттерн сильной типизации?

Основная цель этого – служить защитным барьером в контексте, где есть вызовы функций, которые принимают несколько параметров одного и того же типа данных.

Это, вероятно, произойдет, когда используется много «идентификаторов».

Например, я использовал SecurityId и SecurityInMarketId. Это концептуально разные вещи, и очевидно, что это ошибка использовать неправильный идентификатор или поменять их местами при вызове функции.

function example(securityId::Int64, securityInMarketId::Int64) ... end

Мы можем предотвратить использование неправильной переменной в неверном слоте с помощью паттерна сильной типизации.

Ответ или решение

Как реализовать паттерн Newtype в Julia

Паттерн Newtype или сильно типизированный паттерн, популярно внедряемый в таких языках программирования, как Rust, позволяет создать новый тип, который будет логически отличаться от существующего, но при этом будет основан на уже имеющемся типе. Таким образом удается избежать путаницы между разными значениями одного и того же типа данных, например между идентификаторами. В Julia, отсутствие встроенной поддержки этого паттерна подразумевает, что его необходимо реализовать с помощью оберток (структур). Давайте рассмотрим процесс реализации Newtype в Julia более подробно.

Основные этапы реализации

  1. Определение структуры:
    В Julia вы можете создать новый тип данных, созданный на основе существующего, при помощи создания изменяемой или неизменяемой структуры. Например, простой пример создания идентификатора безопасности:

    mutable struct SecurityId
       _id::Int64
    end

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

  2. Создание вспомогательных функций:
    При реализации нового типа может возникнуть необходимость предоставить функции для доступа к приватному полю. Можно создать функции доступа:

    function get_id(security_id::SecurityId)
       return security_id._id
    end
  3. Определение методов:
    Чтобы ваш новый тип работал корректно с функциями, которые применяются к исходному типу, необходимо переопределить эти функции для нового типа. Например, если вам нужно реализовать арифметические операции:

    function Base.show(io::IO, sid::SecurityId)
       print(io, "SecurityId($sid._id)")
    end

    Это позволит вам корректно выводить объект с вашим новым типом.

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

    function example(securityId::SecurityId, marketId::SecurityInMarketId)
       println("Primary security ID: ", get_id(securityId))
       println("Market ID: ", get_id(marketId))
    end

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

Ограничения и компромиссы

Хотя создание нового типа с использованием структур является эффективным инструментом для внедрения нового типа, в этом подходе есть ограничения и компромиссы:

  • Доступ к данным: Прямой доступ к внутреннему полю через префикс (например, _id) может привести к не лучшему API. Возможно, потребуется множество функций для доступа и обработки данных.

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

Альтернативные решения

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

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...