Spec-Zone.ru › Julia 0.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 поставляется с предварительно определёнными правилами диспетчеризации по умолчанию для математических операторов, которые вызываются, когда для некоторой комбинации типов операндов не существует конкретной реализации. Эти правила по умолчанию сначала преобразуют все операнды к общему типу с использованием определяемых пользователем правил преобразования, а затем вызывают специализированную реализацию данного оператора для полученных значений, которые теперь имеют одинаковый тип. Пользовательские типы могут легко участвовать в этой системе продвижения, определив методы преобразования к другим типам и из них, а также предоставив несколько правил продвижения, определяющих, к каким типам они должны быть продвинуты при смешивании с другими типами.

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

Преобразование значений в различные типы выполняется функцией 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

Преобразование не всегда возможно, в этом случае выбрасывается ошибка отсутствия метода, указывающая на то, что 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()
 in convert(::Type{Bool}, ::Complex{Int64}) at ./complex.jl:23
 ...

julia> convert(Bool, 0im)
false

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

convert{T<:Real}(::Type{T}, z::Complex) = (imag(z)==0 ? convert(T,real(z)) :
                                           throw(InexactError()))

julia> convert(Bool, 1im)
ERROR: InexactError()
 in convert(::Type{Bool}, ::Complex{Int64}) at ./complex.jl:18
 ...

Случайное изучение: Преобразования рациональных чисел

Чтобы продолжить наше изучение типа Rational Julia, вот преобразования, объявленные в rational.jl, сразу после объявления типа и его конструкторов:

convert{T<:Integer}(::Type{Rational{T}}, x::Rational) = Rational(convert(T,x.num),convert(T,x.den))
convert{T<:Integer}(::Type{Rational{T}}, x::Integer) = Rational(convert(T,x), convert(T,1))

function convert{T<:Integer}(::Type{Rational{T}}, x::AbstractFloat, tol::Real)
    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{T<:Integer}(rt::Type{Rational{T}}, x::AbstractFloat) = convert(rt,x,eps(x))

convert{T<:AbstractFloat}(::Type{T}, x::Rational) = convert(T,x.num)/convert(T,x.den)
convert{T<:Integer}(::Type{T}, x::Rational) = 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{T<:Integer,S<:Integer}(::Type{Rational{T}}, ::Type{S}) = Rational{promote_type(T,S)}
promote_rule{T<:Integer,S<:Integer}(::Type{Rational{T}}, ::Type{Rational{S}}) = Rational{promote_type(T,S)}
promote_rule{T<:Integer,S<:AbstractFloat}(::Type{Rational{T}}, ::Type{S}) = 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.5/manual/conversion-and-promotion/

Spec-Zone.ru

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