Spec-Zone.ru › Julia 1.6

Преобразование и продвижение типов

В Julia есть система продвижения аргументов математических операторов к общему типу, о которой упоминалось в различных разделах, включая Целые и числа с плавающей точкой, Математические операции и элементарные функции, Типы и Методы. В этом разделе мы объясним, как работает эта система продвижения, а также как расширить её для новых типов и применить к функциям помимо встроенных математических операторов. Традиционно языки программирования делятся на два лагеря по отношению к продвижению арифметических аргументов:

  • Автоматическое продвижение для встроенных арифметических типов и операторов. В большинстве языков встроенные числовые типы, используемые как операнды арифметических операторов с инфиксной синтаксической конструкцией, например, +, -, *, и /, автоматически продвигают к общему типу для получения ожидаемых результатов. C, Java, Perl и Python, например, правильно вычисляют сумму 1 + 1.5 как значение с плавающей точкой 2.5, даже если один из операндов + является целым числом. Эти системы удобны и достаточно тщательно спроектированы, чтобы они были в основном незаметны программисту: мало кто сознательно думает об этом продвижении при написании такого выражения, но компиляторы и интерпретаторы должны выполнить преобразование перед сложением, так как целые числа и числа с плавающей точкой не могут быть сложены напрямую. Сложные правила для таких автоматических преобразований неизбежно являются частью спецификаций и реализаций таких языков.
  • Отсутствие автоматического продвижения. Этот лагерь включает Ada и ML — очень «строгие» статически типизированные языки. В этих языках каждое преобразование должно быть явно указано программистом. Таким образом, выражение 1 + 1.5 станет ошибкой компиляции как в Ada, так и в ML. Вместо этого необходимо написать real(1) + 1.5, явно преобразовав целое число 1 в число с плавающей точкой перед выполнением сложения. Однако явное преобразование во всех случаях неудобно, поэтому даже Ada имеет определённую степень автоматического преобразования: целочисленные литералы автоматически продвигают к ожидаемому целочисленному типу, а литералы с плавающей точкой — к соответствующим типам с плавающей точкой.

В некотором смысле, Julia попадает в категорию «без автоматического продвижения»: математические операторы — это всего лишь функции со специальным синтаксисом, и аргументы функций никогда не преобразуются автоматически. Тем не менее, можно наблюдать, что применение математических операций к широкому спектру смешанных типов аргументов — это просто крайний случай полиморфной множественной отправки — то, для чего системы отправки и типов Julia идеально подходят. «Автоматическое» продвижение математических операндов просто возникает как специальное приложение: Julia поставляется с предварительно определёнными универсальными правилами отправки для математических операторов, которые вызываются, когда для некоторой комбинации типов операндов нет конкретной реализации. Эти универсальные правила сначала продвигают все операнды к общему типу с использованием определяемых пользователем правил продвижения, а затем вызывают специализированную реализацию оператора для полученных значений, теперь имеющих одинаковый тип. Определяемые пользователем типы могут легко участвовать в этой системе продвижения, определяя методы преобразования в и из других типов и предоставляя несколько правил продвижения, определяющих, к каким типам они должны продвигать при смешивании с другими типами.

Преобразование

Стандартный способ получения значения определенного типа T — вызов конструктора типа T(x). Однако существуют случаи, когда удобно преобразовать значение из одного типа в другой без явного запроса программиста. Один пример — присвоение значения массиву: если A является Vector{Float64}, выражение A[1] = 2 должно работать, автоматически преобразуя 2 из Int в Float64, и храня результат в массиве. Это делается с помощью функции convert.

Функция convert обычно принимает два аргумента: первый — объект типа, а второй — значение, которое нужно преобразовать в этот тип. Возвращаемое значение — значение, преобразованное в экземпляр заданного типа. Самый простой способ понять эту функцию — посмотреть её в действии:

julia> x = 12
12

julia> typeof(x)
Int64

julia> xu = convert(UInt8, x)
0x0c

julia> typeof(xu)
UInt8

julia> xf = convert(AbstractFloat, x)
12.0

julia> typeof(xf)
Float64

julia> a = Any[1 2 3; 4 5 6]
2×3 Matrix{Any}:
 1  2  3
 4  5  6

julia> convert(Array{Float64}, a)
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 4.0  5.0  6.0

Преобразование не всегда возможно, в таком случае выбрасывается MethodError, указывающее, что convert не знает, как выполнить запрошенное преобразование:

julia> convert(AbstractFloat, "foo")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
[...]

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

Когда вызывается convert?

Следующие языковые конструкции вызывают convert:

  • Присвоение массиву выполняет преобразование в тип элемента массива.
  • Присвоение полю объекта выполняет преобразование в объявленный тип поля.
  • Создание объекта с помощью new выполняет преобразование в объявленные типы полей объекта.
  • Присвоение переменной с объявленным типом (например, local x::T) выполняет преобразование в этот тип.
  • Функция с объявленным возвращаемым типом преобразует возвращаемое значение в этот тип.
  • Передача значения в ccall преобразует его в соответствующий тип аргумента.

Преобразование против создания

Обратите внимание, что поведение convert(T, x) кажется почти идентичным T(x). В действительности, так обычно и есть. Однако есть ключевое семантическое различие: поскольку convert может вызываться неявно, его методы ограничены случаями, которые считаются «безопасными» или «неожиданными». convert будет преобразовывать только между типами, представляющими один и тот же базовый вид (например, различные представления чисел или различные кодировки строк). Обычно оно также без потерь; преобразование значения в другой тип и обратно должно привести к точно такому же значению.

Существует четыре основных типа случаев, где конструкторы отличаются от convert:

Конструкторы для типов, не связанных с их аргументами

Некоторые конструкторы не реализуют концепцию «преобразования». Например, Timer(2) создаёт таймер на 2 секунды, что на самом деле не является «преобразованием» из целого числа в таймер.

Изменяемые коллекции

convert(T, x) ожидается, что вернёт исходное x если x уже типа T. В отличие от этого, если T — это изменяемый тип коллекции, то T(x) всегда должен создавать новую коллекцию (копируя элементы из x).

Обёртки типов

Для некоторых типов, которые «обёртывают» другие значения, конструктор может обернуть свой аргумент внутри нового объекта, даже если он уже имеет требуемый тип. Например Some(x) оборачивает x для обозначения того, что значение присутствует (в контексте, где результатом может быть Some или nothing). Тем не менее, сам x может быть объектом Some(y), в этом случае результатом будет Some(Some(y)), с двумя уровнями оборачивания. convert(Some, x), с другой стороны, просто вернёт x так как уже является Some.

Конструкторы, не возвращающие экземпляры своего типа

В очень редких случаях может иметь смысл, чтобы конструктор T(x) возвращал объект не типа T. Это может произойти, если тип-обёртка является своим собственным обратным (например, Flip(Flip(x)) === x) или для поддержки старого синтаксиса вызова для обратной совместимости при реструктуризации библиотеки. Но convert(T, x) всегда должен возвращать значение типа T.

Определение новых преобразований

При определении нового типа вначале все способы его создания должны быть определены как конструкторы. Если становится ясно, что неявное преобразование будет полезным, и некоторые конструкторы удовлетворяют вышеуказанным критериям «безопасности», тогда можно добавить методы convert. Эти методы обычно довольно просты, так как им нужно только вызвать соответствующий конструктор. Такое определение может выглядеть так:

convert(::Type{MyType}, x) = MyType(x)

Тип первого аргумента этого метода — Type{MyType}, единственный экземпляр которого — MyType. Таким образом, этот метод вызывается только тогда, когда первый аргумент — значение типа MyType . Обратите внимание на синтаксис, используемый для первого аргумента: имя аргумента опущено перед символом ::, и указан только тип. Это синтаксис в Julia для аргумента функции, тип которого задан, но значение которого не нужно ссылаться по имени.

Все экземпляры некоторых абстрактных типов по умолчанию считаются «достаточно похожими», поэтому универсальное определение convert предоставляется в Julia Base. Например, это определение указывает на то, что допустимо преобразовать любой тип convert в любой другой, вызвав конструктор с 1 аргументом:

convert(::Type{T}, x::Number) where {T<:Number} = T(x)

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

convert(::Type{T}, x::T) where {T<:Number} = x

Аналогичные определения существуют для AbstractString, AbstractArray и AbstractDict.

Продвижение

Продвижение относится к преобразованию значений смешанных типов в один общий тип. Хотя это и не строго необходимо, обычно подразумевается, что общий тип, в который преобразуются значения, может достоверно представлять все исходные значения. В этом смысле термин «продвижение» уместен, поскольку значения преобразуются в «более широкий» тип — тип, который может представлять все входные значения в едином общем типе. Однако важно не путать это с объектно-ориентированным (структурным) супертипированием или понятием абстрактных супертипов в Julia: продвижение не имеет ничего общего с иерархией типов и все связано с преобразованием между альтернативными представлениями. Например, хотя каждое значение Int32 также может быть представлено как значение Float64, Int32 не является подтипом Float64.

Продвижение к общему «более широкому» типу выполняется в Julia функцией promote, которая принимает любое количество аргументов и возвращает кортеж из такого же количества значений, преобразованных в общий тип, или выводит исключение, если продвижение невозможно. Наиболее распространенный случай использования продвижения — преобразование числовых аргументов в общий тип:

julia> promote(1, 2.5)
(1.0, 2.5)

julia> promote(1, 2.5, 3)
(1.0, 2.5, 3.0)

julia> promote(2, 3//4)
(2//1, 3//4)

julia> promote(1, 2.5, 3, 3//4)
(1.0, 2.5, 3.0, 0.75)

julia> promote(1.5, im)
(1.5 + 0.0im, 0.0 + 1.0im)

julia> promote(1 + 2im, 3//4)
(1//1 + 2//1*im, 3//4 + 0//1*im)

Значения с плавающей точкой выводятся до наибольшего из типов аргументов с плавающей точкой. Целочисленные значения выводятся до большего размера либо машинного слова, либо наибольшего целочисленного типа аргумента. Смешанные целые и значения с плавающей точкой выводятся до типа с плавающей точкой, достаточно большого для хранения всех значений. Целые числа, смешанные с рациональными, выводятся до рациональных чисел. Рациональные числа, смешанные с числами с плавающей точкой, выводятся до чисел с плавающей точкой. Комплексные значения, смешанные с действительными, выводятся до соответствующего типа комплексного значения.

На самом деле, это все, что нужно для использования продвижения. Остальное — просто вопрос умелого применения, наиболее типичным «умным» применением является определение универсальных методов для числовых операций, таких как арифметические операторы +, -, * и /. Вот некоторые из универсальных определений методов, приведенные в promotion.jl:

+(x::Number, y::Number) = +(promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)

Эти определения методов означают, что при отсутствии более конкретных правил для сложения, вычитания, умножения и деления пар числовых значений значения выводятся до общего типа, а затем снова пробуются. Вот и все: нигде более не нужно беспокоиться о продвижении к общему числовому типу для арифметических операций — это происходит автоматически. В promotion.jl есть определения универсальных методов продвижения для ряда других арифметических и математических функций, но помимо этого, в Julia Base почти не требуется вызовов promote . Наиболее распространенные использования promote встречаются во внешних методах конструкторов, предоставляемых для удобства, чтобы разрешить вызовы конструкторов со смешанными типами делегировать внутреннему типу с полями, приведенными к соответствующему общему типу. Например, вспомните, что rational.jl предоставляет следующий внешний метод конструктора:

Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)

Это позволяет работать следующим вызовам:

julia> x = Rational(Int8(15),Int32(-5))
-3//1

julia> typeof(x)
Rational{Int32}

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

Определение правил продвижения

Хотя, в принципе, можно определить методы для функции promote напрямую, это потребовало бы множества избыточных определений для всех возможных перестановок типов аргументов. Вместо этого поведение promote определяется в терминах вспомогательной функции, называемой promote_rule, для которой можно предоставить методы. Функция promote_rule принимает пару объектов типа и возвращает другой объект типа, таким образом, экземпляры типов аргументов будут приведены к возвращаемому типу. Таким образом, определив правило:

promote_rule(::Type{Float64}, ::Type{Float32}) = Float64

заявляется, что при продвижении 64-битных и 32-битных значений с плавающей точкой вместе они должны быть приведены к 64-битным значениям с плавающей точкой. Тип продвижения не обязательно должен быть одним из типов аргументов. Например, следующие правила продвижения оба встречаются в Julia Base:

promote_rule(::Type{BigInt}, ::Type{Float64}) = BigFloat
promote_rule(::Type{BigInt}, ::Type{Int8}) = BigInt

В последнем случае результатом типа является BigInt, поскольку BigInt является единственным типом, достаточно большим, чтобы хранить целые числа для арифметики целых чисел произвольной точности. Также обратите внимание, что не нужно определять как promote_rule(::Type{A}, ::Type{B}), так и promote_rule(::Type{B}, ::Type{A}) — симметрия подразумевается способом использования promote_rule в процессе продвижения.

Функция promote_rule используется в качестве строительного блока для определения второй функции, называемой promote_type, которая, принимая любое количество объектов типа, возвращает общий тип, к которому должны быть приведены эти значения, как аргументы к promote. Таким образом, если нужно узнать, в отсутствие фактических значений, к какому типу приведет коллекция значений определенных типов, можно использовать promote_type:

julia> promote_type(Int8, Int64)
Int64

Внутри promote_type используется внутри promote для определения, к какому типу аргументов значений следует преобразовать для продвижения. Однако это может быть полезно само по себе. Любознательный читатель может прочитать код в promotion.jl, который определяет весь механизм продвижения примерно в 35 строках.

Пример: продвижения рациональных чисел

Наконец, мы завершаем наш текущий пример рационального числа Julia, которое относительно сложно использует механизм продвижения со следующими правилами продвижения:

promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{Rational{S}}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:AbstractFloat} = promote_type(T,S)

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

Эта небольшая группа правил продвижения, вместе с конструкторами типа и по умолчанию методом convert для чисел, достаточно для того, чтобы рациональные числа полностью естественно взаимодействовали со всеми другими числовыми типами Julia — целыми числами, числами с плавающей точкой и комплексными числами. Предоставление соответствующих методов преобразования и правил продвижения таким же образом позволяет любому определенному пользователем числовому типу взаимодействовать так же естественно с предопределенными числовыми типами Julia.

© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.6.0/manual/conversion-and-promotion/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API