Spec-Zone.ru › Julia 0.7

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

В 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 для аргумента функции, тип которого указан, но значение которого не нужно ссылаться по имени. В этом примере, поскольку тип является типом-синглтоном, мы уже знаем его значение без обращения к имени аргумента.

Все экземпляры некоторых абстрактных типов по умолчанию считаются «достаточно похожими», что позволяет в Julia Base предоставить универсальное определение convert. Например, это определение утверждает, что допускается преобразование любого типа 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.

Продвижение

Промоция относится к преобразованию значений смешанных типов в один общий тип. Хотя это не строго необходимо, обычно подразумевается, что общий тип, в который преобразуются значения, может верно представлять все исходные значения. В этом смысле термин "промоция" уместен, так как значения преобразуются в "более" большой тип – то есть в тип, который может представить все входные значения в одном общем типе. Однако важно не путать это с объектно-ориентированным (структурным) супертипированием или представлением Джулии об абстрактных супертипах: промоция не имеет отношения к иерархии типов и целиком связана с преобразованием между альтернативными представлениями. Например, хотя каждое значение Int32 также может быть представлено как значение Float64, Int32 не является подтипом Float64.

Промоция к общему "более" большому типу выполняется в Джулии функцией 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{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, Int64)
Int64

Внутренне promote_type используется внутри promote для определения того, к какому типу должны быть преобразованы значения аргументов для промоции. Тем не менее, оно может быть полезным и само по себе. Любопытный читатель может прочитать код в promotion.jl, который определяет полную механику промоции примерно в 35 строках.

Примеры промоции рациональных чисел

Наконец, мы завершаем наше текущее изучение рационального типа чисел Джулии, который достаточно сложным образом использует механизм промоции со следующими правилами промоции:

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 для чисел, достаточно для того, чтобы рациональные числа могли полностью естественно взаимодействовать со всеми другими числовыми типами Джулии – целыми числами, числами с плавающей точкой и комплексными числами. Предоставление соответствующих методов преобразования и правил промоции таким же образом позволяет любому пользовательскому числовому типу взаимодействовать столь же естественно с предопределёнными числовыми типами Джулии.

© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v0.7.0/manual/conversion-and-promotion/

Spec-Zone.ru

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