Spec-Zone.ru › Julia 1.3

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

В 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 любой тип 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 есть определения универсальных методов преобразования для ряда других арифметических и математических функций, но за пределами этого, в 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.3.1/manual/conversion-and-promotion/

Spec-Zone.ru

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