Spec-Zone.ru › Julia 1.5

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

В 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> convert(UInt8, x)
0x0c

julia> typeof(ans)
UInt8

julia> convert(AbstractFloat, x)
12.0

julia> typeof(ans)
Float64

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

julia> convert(Array{Float64}, a)
2×3 Array{Float64,2}:
 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 любой тип Number в любой другой тип, вызвав конструктор с 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, но помимо этого вызовы promote в Julia Base практически не нужны. Наиболее распространённое использование promote происходит во внешних методах конструкторов, предоставленных для удобства, чтобы позволить вызовам конструкторов со смешанными типами делегировать работу внутреннему типу с полями, преобразованными к подходящему общему типу. Например, вспомним, что rational.jl предоставляет следующий внешний метод конструктора:

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

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

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

julia> typeof(ans)
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–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.5.3/manual/conversion-and-promotion/

Spec-Zone.ru

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