Spec-Zone.ru › Julia 1.1

Преобразование и повышение

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

Преобразование не всегда возможно, в этом случае выбрасывается ошибка «нет метода», указывающая на то, что 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> 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–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.1.1/manual/conversion-and-promotion/

Spec-Zone.ru

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