Преобразование и продвижение
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's dispatch и системы типов очень хорошо справляются. «Автоматическое» продвижение математических операндов просто возникает как специальное применение: Julia поставляется с предварительно определёнными общими правилами диспетчеризации для математических операторов, вызываемых, когда не существует конкретной реализации для какой-либо комбинации типов операндов. Эти общие правила сначала преобразуют все операнды в общий тип с использованием определяемых пользователем правил продвижения, а затем вызывают специализированную реализацию рассматриваемого оператора для полученных значений, теперь имеющих один и тот же тип. Определяемые пользователем типы могут легко участвовать в этой системе продвижения, определив методы преобразования в другие типы и из других типов, и предоставив несколько правил продвижения, определяющих, к каким типам они должны преобразовываться при смешивании с другими типами.
Преобразование
Преобразование значений в различные типы выполняется функцией 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
Преобразование не всегда возможно, в этом случае генерируется ошибка no method, указывающая, что convert не знает, как выполнить запрошенное преобразование:
julia> convert(AbstractFloat, "foo") ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat This may have arisen from a call to the constructor AbstractFloat(...), since type constructors fall back to convert methods.
Некоторые языки рассматривают парсинг строк как чисел или форматирование чисел как строк как преобразования (многие динамические языки даже автоматически выполнят преобразование для вас), однако Julia этого не делает: хотя некоторые строки могут быть распарсены как числа, большинство строк не являются допустимыми представлениями чисел, и только очень ограниченный подмножество из них таковы. Поэтому в Julia для выполнения этой операции необходимо использовать специализированную функцию parse(), что делает ее более явной.
Определение новых преобразований
Чтобы определить новое преобразование, просто предоставьте новый метод для convert(). Это всё, что нужно. Например, метод преобразования вещественного числа в булево значение выглядит так:
convert(::Type{Bool}, x::Real) = x==0 ? false : x==1 ? true : throw(InexactError())
Тип первого аргумента этого метода — синглтон-тип, Type{Bool}, единственный экземпляр которого — Bool. Таким образом, этот метод вызывается только тогда, когда первый аргумент — значение типа Bool. Обратите внимание на синтаксис, используемый для первого аргумента: имя аргумента опущено перед символом ::, и указан только тип. Это синтаксис в Julia для аргумента функции, тип которого указан, но значение которого никогда не используется в теле функции. В этом примере, поскольку тип является синглтоном, никогда не будет необходимости использовать его значение внутри тела. При вызове метод определяет, является ли числовое значение истинным или ложным в качестве булевого значения, сравнивая его с единицей и нулём:
julia> convert(Bool, 1)
true
julia> convert(Bool, 0)
false
julia> convert(Bool, 1im)
ERROR: InexactError()
Stacktrace:
[1] convert(::Type{Bool}, ::Complex{Int64}) at ./complex.jl:31
julia> convert(Bool, 0im)
false
Подписи методов для методов преобразования часто намного сложнее, чем этот пример, особенно для параметрических типов. Приведенный выше пример предназначен для обучения и не отражает фактическое поведение Julia. Вот фактическая реализация в Julia:
convert(::Type{T}, z::Complex) where {T<:Real} =
(imag(z) == 0 ? convert(T, real(z)) : throw(InexactError()))
Случай изучения: Преобразования рациональных чисел
Продолжая изучение типа Julia Rational, вот преобразования, объявленные в rational.jl, сразу после объявления типа и его конструкторов:
convert(::Type{Rational{T}}, x::Rational) where {T<:Integer} = Rational(convert(T,x.num),convert(T,x.den))
convert(::Type{Rational{T}}, x::Integer) where {T<:Integer} = Rational(convert(T,x), convert(T,1))
function convert(::Type{Rational{T}}, x::AbstractFloat, tol::Real) where T<:Integer
if isnan(x); return zero(T)//zero(T); end
if isinf(x); return sign(x)//zero(T); end
y = x
a = d = one(T)
b = c = zero(T)
while true
f = convert(T,round(y)); y -= f
a, b, c, d = f*a+c, f*b+d, a, b
if y == 0 || abs(a/b-x) <= tol
return a//b
end
y = 1/y
end
end
convert(rt::Type{Rational{T}}, x::AbstractFloat) where {T<:Integer} = convert(rt,x,eps(x))
convert(::Type{T}, x::Rational) where {T<:AbstractFloat} = convert(T,x.num)/convert(T,x.den)
convert(::Type{T}, x::Rational) where {T<:Integer} = div(convert(T,x.num),convert(T,x.den))
Первые четыре метода преобразования предоставляют преобразования в рациональные типы. Первый метод преобразует один тип рационального числа в другой тип рационального числа, преобразуя числитель и знаменатель в соответствующий целочисленный тип. Второй метод выполняет то же преобразование для целых чисел, принимая знаменатель равным 1. Третий метод реализует стандартный алгоритм приближения числа с плавающей точкой отношением целых чисел с заданной точностью, а четвёртый метод применяет его, используя машинную эпсилон в заданном значении в качестве порога. В общем случае следует a//b == convert(Rational{Int64}, a/b).
Последние два метода преобразования предоставляют преобразования из рациональных типов в типы с плавающей точкой и целочисленные типы. Для преобразования в тип с плавающей точкой достаточно преобразовать числитель и знаменатель в этот тип с плавающей точкой, а затем разделить. Для преобразования в целочисленный тип можно использовать оператор div для усеченного целочисленного деления (округления к нулю).
Продвижение
Продвижение относится к преобразованию значений смешанных типов в один общий тип. Хотя это не строго необходимо, обычно подразумевается, что общий тип, к которому преобразуются значения, может корректно представлять все исходные значения. В этом смысле термин «продвижение» уместен, так как значения преобразуются в «более большой» тип — т. е. тип, который может представлять все входные значения в одном общем типе. Однако важно не путать это с объектно-ориентированным (структурным) супертипированием или понятием 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 почти нет вызовов 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:
promote_rule(::Type{UInt8}, ::Type{Int8}) = Int
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, UInt16) 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)
Первое правило гласит, что повышение рационального числа с любым другим типом целых чисел повышает его до рационального типа, тип числителя/знаменателя которого является результатом повышения типа числителя/знаменателя с другим типом целых чисел. Второе правило применяет ту же логику к двум различным типам рациональных чисел, в результате чего получается рациональное число с повышением их соответствующих типов числителя/знаменателя. Третье и последнее правило диктует, что повышение рационального числа с плавающей точкой приводит к тому же типу, что и повышение типа числителя/знаменателя с плавающей точкой.
Эта небольшая группа правил повышения, вместе с методами преобразования, обсуждаемыми выше, достаточно для того, чтобы рациональные числа полностью естественно взаимодействовали со всеми другими числовыми типами Julia — целыми числами, числами с плавающей точкой и комплексными числами. Предоставляя соответствующие методы преобразования и правила повышения таким же образом, любой определенный пользователем числовой тип может взаимодействовать так же естественно с предопределенными числовыми типами Julia.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.6/manual/conversion-and-promotion/