Spec-Zone.ru › Julia 1.4

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

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 поставляется с предварительно определёнными правилами отправки «catch-all» для математических операторов, вызываемых, когда для некоторой комбинации типов операндов нет специфической реализации. Эти правила «catch-all» сначала преобразуют все операнды к общему типу с использованием определяемых пользователем правил преобразования, а затем вызывают специализированную реализацию оператора для полученных значений, теперь имеющих одинаковый тип. Определяемые пользователем типы могут легко участвовать в этой системе продвижения, определяя методы для преобразования в другие типы и из других типов, а также предоставляя несколько правил продвижения, определяющих, к каким типам они должны преобразовываться при смешивании с другими типами.

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

Стандартный способ получения значения определённого типа 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.

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

Преобразование типов (promotion) относится к преобразованию значений смешанных типов к одному общему типу. Хотя это не строго необходимо, обычно подразумевается, что общий тип, к которому преобразуются значения, может достоверно представлять все исходные значения. В этом смысле термин "преобразование типов" уместен, поскольку значения преобразуются к типу "большего" размера – то есть к типу, который может представлять все входные значения в одном общем типе. Однако важно не путать это с объектно-ориентированным (структурным) супертипированием или понятием абстрактных супертипов в 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> 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.4.2/manual/conversion-and-promotion/

Spec-Zone.ru

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