Conversion and Promotion
Julia, matematik operatörlerinin argümanlarını ortak bir türe yükseltmek için bir sisteme sahiptir; bu, Integers and Floating-Point Numbers, Mathematical Operations and Elementary Functions, Types ve Methods gibi çeşitli diğer bölümlerde bahsedilmiştir. Bu bölümde, bu yükseltme sisteminin nasıl çalıştığını, yeni türlere nasıl genişletileceğini ve yerleşik matematiksel operatörler dışında işlevlere nasıl uygulanacağını açıklıyoruz. Geleneksel olarak, programlama dilleri aritmetik argümanların yükseltilmesi konusunda iki gruba ayrılır:
- Yerleşik aritmetik türler ve operatörler için otomatik terfi. Çoğu dilde, yerleşik sayısal türler,
+
,-
,*
ve/
gibi infiks sözdizimi ile aritmetik operatörlere operand olarak kullanıldığında, beklenen sonuçları üretmek için otomatik olarak ortak bir türe terfi ettirilir. C, Java, Perl ve Python gibi diller,1 + 1.5
toplamını,+
operatörlerinden birinin bir tam sayı olmasına rağmen, doğru bir şekilde kayan nokta değeri2.5
olarak hesaplar. Bu sistemler, programcıya görünmez olacak kadar dikkatlice tasarlanmış ve kullanışlıdır: böyle bir ifadeyi yazarken bu terfinin gerçekleştiğini bilinçli olarak düşünen pek kimse yoktur, ancak derleyiciler ve yorumlayıcılar, tam sayılar ve kayan nokta değerlerinin olduğu gibi eklenemeyeceği için toplama işleminden önce dönüşüm yapmak zorundadır. Bu tür otomatik dönüşümler için karmaşık kurallar, dolayısıyla bu dillerin spesifikasyonları ve uygulamaları için kaçınılmaz olarak bir parçasıdır. - Otomatik terfi yok. Bu kamp Ada ve ML'yi içerir - çok "katı" statik olarak tiplenmiş diller. Bu dillerde, her dönüşüm programcı tarafından açıkça belirtilmelidir. Bu nedenle,
1 + 1.5
ifadesi hem Ada hem de ML'de bir derleme hatası olacaktır. Bunun yerine, tam sayı1
'i toplama işlemi yapmadan önce bir kayan nokta değerine açıkça dönüştürmek içinreal(1) + 1.5
yazmak gerekir. Ancak her yerde açık dönüşüm yapmak o kadar elverişsizdir ki, hatta Ada'nın bile belirli bir otomatik dönüşüm derecesi vardır: tam sayı literalleri otomatik olarak beklenen tam sayı türüne yükseltilir ve kayan nokta literalleri de benzer şekilde uygun kayan nokta türlerine yükseltilir.
Bir anlamda, Julia "otomatik terfi yok" kategorisine girer: matematiksel operatörler, özel sözdizimi ile işlevlerdir ve işlevlerin argümanları asla otomatik olarak dönüştürülmez. Ancak, çeşitli karışık argüman türlerine matematiksel işlemler uygulamanın, polimorfik çoklu yönlendirme durumunun aşırı bir durumu olduğunu gözlemleyebilirsiniz - bu, Julia'nın yönlendirme ve tür sistemlerinin özellikle iyi başa çıktığı bir şeydir. Matematiksel operandların "otomatik" terfisi, özel bir uygulama olarak ortaya çıkar: Julia, bazı operand türü kombinasyonları için belirli bir uygulama mevcut olmadığında devreye giren matematiksel operatörler için önceden tanımlanmış genel yönlendirme kurallarına sahiptir. Bu genel kurallar, önce tüm operandları kullanıcı tanımlı terfi kurallarını kullanarak ortak bir türe terfi ettirir ve ardından sonuçta elde edilen değerler için, artık aynı türde olan, ilgili operatörün özel bir uygulamasını çağırır. Kullanıcı tanımlı türler, diğer türlere dönüştürme yöntemleri tanımlayarak ve diğer türlerle karıştıklarında hangi türlere terfi etmeleri gerektiğini tanımlayan birkaç terfi kuralı sağlayarak bu terfi sistemine kolayca katılabilir.
Conversion
Belirli bir tür T
değerini elde etmenin standart yolu, türün yapıcısını çağırmaktır, T(x)
. Ancak, programcının bunu açıkça istemediği durumlarda bir değeri bir türden diğerine dönüştürmek uygun olabilir. Bir örnek, bir değeri bir diziye atamaktır: Eğer A
bir Vector{Float64}
ise, A[1] = 2
ifadesi, 2
'yi Int
'ten Float64
'ye otomatik olarak dönüştürerek ve sonucu dizide saklayarak çalışmalıdır. Bu, convert
fonksiyonu aracılığıyla yapılır.
convert
fonksiyonu genellikle iki argüman alır: ilki bir tür nesnesi, ikincisi ise o türe dönüştürülecek bir değerdir. Döndürülen değer, verilen türün bir örneğine dönüştürülmüş değerdir. Bu fonksiyonu anlamanın en basit yolu, onu eylemde görmektir:
julia> x = 12
12
julia> typeof(x)
Int64
julia> xu = convert(UInt8, x)
0x0c
julia> typeof(xu)
UInt8
julia> xf = convert(AbstractFloat, x)
12.0
julia> typeof(xf)
Float64
julia> a = Any[1 2 3; 4 5 6]
2×3 Matrix{Any}:
1 2 3
4 5 6
julia> convert(Array{Float64}, a)
2×3 Matrix{Float64}:
1.0 2.0 3.0
4.0 5.0 6.0
Dönüşüm her zaman mümkün değildir; bu durumda MethodError
atılır ve convert
'in istenen dönüşümü nasıl gerçekleştireceğini bilmediğini belirtir:
julia> convert(AbstractFloat, "foo")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
[...]
Bazı diller, dizeleri sayılar olarak ayrıştırmayı veya sayıları dizeler olarak biçimlendirmeyi dönüşümler olarak kabul eder (birçok dinamik dil, dönüşümü sizin için otomatik olarak bile gerçekleştirebilir). Bu, Julia'da böyle değildir. Bazı dizeler sayılar olarak ayrıştırılabilse de, çoğu dize geçerli sayı temsilleri değildir ve yalnızca çok sınırlı bir alt kümesi geçerlidir. Bu nedenle, Julia'da bu işlemi gerçekleştirmek için özel parse
fonksiyonu kullanılmalıdır, bu da işlemi daha belirgin hale getirir.
When is convert
called?
Aşağıdaki dil yapıları convert
çağrısını yapar:
- Diziye atama, dizinin eleman türüne dönüştürür.
- Bir nesnenin bir alanına atama yapmak, alanın bildirilen türüne dönüştürür.
new
nesnesinin tanımlı alan türlerine dönüştürülmesi.- Bir türü belirtilmiş bir değişkene atama (örneğin,
local x::T
) o türe dönüştürür. - Bir dönüş türü beyan edilmiş bir fonksiyon, dönüş değerini o türe dönüştürür.
ccall
değerini geçmek, onu karşılık gelen argüman türüne dönüştürür.
Conversion vs. Construction
convert(T, x)
işlevinin davranışının T(x)
ile neredeyse aynı olduğunu unutmayın. Gerçekten de genellikle öyledir. Ancak, önemli bir anlamsal fark vardır: convert
örtük olarak çağrılabildiğinden, yöntemleri "güvenli" veya "şaşırtıcı olmayan" durumlarla sınırlıdır. convert
, yalnızca aynı temel türdeki şeyleri temsil eden türler arasında dönüşüm yapar (örneğin, farklı sayı temsilleri veya farklı dize kodlamaları). Ayrıca genellikle kayıpsızdır; bir değeri farklı bir türe dönüştürmek ve tekrar geri döndürmek, tam olarak aynı değeri vermelidir.
Dört genel durumda yapıcılar convert
'ten farklıdır:
Constructors for types unrelated to their arguments
Bazı yapıcılar "dönüşüm" kavramını uygulamaz. Örneğin, Timer(2)
2 saniyelik bir zamanlayıcı oluşturur, bu da gerçekten bir tam sayıdan bir zamanlayıcıya "dönüşüm" değildir.
Mutable collections
convert(T, x)
orijinal x
'i döndürmesi beklenir eğer x
zaten T
tipindeyse. Aksine, eğer T
değiştirilebilir bir koleksiyon tipi ise, o zaman T(x)
her zaman yeni bir koleksiyon oluşturmalıdır (elemanları x
'den kopyalayarak).
Wrapper types
Bazı türler, diğer değerleri "saran" yapılar olduğunda, yapıcı, argümanını yeni bir nesne içine sarmış olabilir, bu argüman zaten istenen türde olsa bile. Örneğin, Some(x)
ifadesi, bir değerin mevcut olduğunu belirtmek için x
'i sarar (sonucun Some
veya nothing
olabileceği bir bağlamda). Ancak, x
kendisi Some(y)
nesnesi olabilir; bu durumda sonuç Some(Some(y))
olur ve iki katmanlı bir sarma gerçekleşir. Öte yandan, convert(Some, x)
ifadesi, x
zaten bir Some
olduğu için sadece x
'i döndürür.
Constructors that don't return instances of their own type
Çok nadir durumlarda, T(x)
yapıcısının T
türünde olmayan bir nesne döndürmesi mantıklı olabilir. Bu, bir sarmalayıcı türün kendi tersine sahip olduğu durumlarda (örneğin, Flip(Flip(x)) === x
) veya bir kütüphane yeniden yapılandırıldığında geriye dönük uyumluluk için eski bir çağrı sözdizimini desteklemek amacıyla olabilir. Ancak convert(T, x)
her zaman T
türünde bir değer döndürmelidir.
Defining New Conversions
Yeni bir tür tanımlarken, başlangıçta onu oluşturmanın tüm yolları yapıcılar olarak tanımlanmalıdır. Eğer örtük dönüşümün faydalı olacağı ve bazı yapıcıların yukarıdaki "güvenlik" kriterlerini karşıladığı açıksa, o zaman convert
yöntemleri eklenebilir. Bu yöntemler genellikle oldukça basittir, çünkü yalnızca uygun yapıcıyı çağırmaları gerekir. Böyle bir tanım şu şekilde görünebilir:
import Base: convert
convert(::Type{MyType}, x) = MyType(x)
Bu metodun ilk argümanının türü Type{MyType}
olup, tek örneği MyType
'dir. Bu nedenle, bu metod yalnızca ilk argüman MyType
tür değeriyse çağrılır. İlk argüman için kullanılan sözdizimine dikkat edin: ::
sembolünden önce argüman adı atlanmış ve yalnızca tür verilmiştir. Bu, Julia'da türü belirtilmiş ancak değeri isimle referans verilmesine gerek olmayan bir fonksiyon argümanı için kullanılan sözdizimidir.
Tüm soy türlerin örnekleri varsayılan olarak "yeterince benzer" kabul edilir, bu nedenle Julia Base'te evrensel bir convert
tanımı sağlanmıştır. Örneğin, bu tanım, herhangi bir Number
türünü başka birine dönüştürmenin geçerli olduğunu belirtir; bu, 1-argümanlı bir yapıcı çağrılarak yapılır:
convert(::Type{T}, x::Number) where {T<:Number} = T(x)::T
Bu, yeni Number
türlerinin yalnızca yapıcıları tanımlaması gerektiği anlamına gelir, çünkü bu tanım convert
işlemini onlar için halledecektir. Zaten istenen türde olan argümanlar için bir kimlik dönüşümü de sağlanmıştır:
convert(::Type{T}, x::T) where {T<:Number} = x
Benzer tanımlar AbstractString
, AbstractArray
ve AbstractDict
için mevcuttur.
Promotion
Promosyon, karışık türlerin değerlerini tek bir ortak türe dönüştürmeyi ifade eder. Bu kesinlikle gerekli olmasa da, genellikle değerlerin dönüştürüldüğü ortak türün, tüm orijinal değerleri sadık bir şekilde temsil edebileceği varsayılır. Bu anlamda, "promosyon" terimi uygundur çünkü değerler "daha büyük" bir türe dönüştürülür - yani, tüm giriş değerlerini tek bir ortak türde temsil edebilen bir türe. Ancak, bunun nesne yönelimli (yapısal) süper türleme ile veya Julia'nın soyut süper türler kavramıyla karıştırılmaması önemlidir: promosyon, tür hiyerarşisi ile hiçbir ilgisi yoktur ve alternatif temsiller arasında dönüştürme ile tamamen ilgilidir. Örneğin, her Int32
değeri, Float64
değeri olarak da temsil edilebilir, ancak Int32
, Float64
'ün bir alt türü değildir.
Julia'da ortak bir "büyütme" türüne terfi, promote
fonksiyonu ile gerçekleştirilir. Bu fonksiyon, herhangi bir sayıda argüman alır ve aynı sayıda değeri ortak bir türe dönüştürerek bir demet (tuple) döner veya terfi mümkün değilse bir istisna fırlatır. Terfi için en yaygın kullanım durumu, sayısal argümanları ortak bir türe dönüştürmektir:
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)
Kayan noktalı değerler, kayan nokta argüman türlerinin en büyüğüne yükseltilir. Tam sayılar, tam sayı argüman türlerinin en büyüğüne yükseltilir. Türler aynı boyutta ancak işaretlilik açısından farklıysa, işaretsiz tür seçilir. Tam sayılar ve kayan noktalı değerlerin karışımları, tüm değerleri tutacak kadar büyük bir kayan nokta türüne yükseltilir. Rasyonellerle karıştırılan tam sayılar, rasyonellere yükseltilir. Kayan noktalı değerlerle karıştırılan rasyoneller, kayan noktalara yükseltilir. Gerçek değerlerle karıştırılan karmaşık değerler, uygun türde karmaşık değere yükseltilir.
Bu, promosyonları kullanmanın gerçekten tek yolu. Geri kalan, akıllıca uygulama meselesidir; en tipik "akıllıca" uygulama, aritmetik operatörler +
, -
, *
ve /
gibi sayısal işlemler için genel yöntemlerin tanımlanmasıdır. İşte promotion.jl
içinde verilen bazı genel yöntem tanımları:
+(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)...)
Bu yöntem tanımları, sayısal değer çiftlerini toplama, çıkarma, çarpma ve bölme için daha spesifik kuralların yokluğunda, değerleri ortak bir türe yükseltip tekrar denemek gerektiğini belirtir. İşte bu kadar basit: aritmetik işlemler için ortak bir sayısal türe yükseltme konusunda başka bir yerde endişelenmeye gerek yoktur - bu otomatik olarak gerçekleşir. promotion.jl
içinde bir dizi diğer aritmetik ve matematiksel fonksiyonlar için genel yükseltme yöntemlerinin tanımları bulunmaktadır, ancak bunun ötesinde, Julia Base'de promote
çağrısına pek ihtiyaç yoktur. promote
'un en yaygın kullanımları, karışık türlerle yapılan yapıcı çağrıların, alanları uygun bir ortak türe yükseltilmiş bir iç türe devretmesine olanak tanımak için sağlanan dış yapıcı yöntemlerde gerçekleşir. Örneğin, rational.jl
aşağıdaki dış yapıcı yöntemini sağlar:
Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)
Bu, aşağıdaki gibi çağrıların çalışmasına olanak tanır:
julia> x = Rational(Int8(15),Int32(-5))
-3//1
julia> typeof(x)
Rational{Int32}
Çoğu kullanıcı tanımlı tür için, programcıların yapıcı işlevlere beklenen türleri açıkça sağlamalarını istemek daha iyi bir uygulamadır, ancak bazen, özellikle sayısal problemler için, otomatik olarak yükseltme yapmak kullanışlı olabilir.
Defining Promotion Rules
Her ne kadar promote
fonksiyonu için yöntemler doğrudan tanımlanabilse de, bu, tüm olası argüman türü permütasyonları için birçok gereksiz tanım gerektirecektir. Bunun yerine, promote
'un davranışı, promote_rule
adlı bir yardımcı fonksiyon cinsinden tanımlanmıştır; bu fonksiyon için yöntemler sağlanabilir. promote_rule
fonksiyonu, bir çift tür nesnesi alır ve argüman türlerinin örneklerinin döndürülen türe terfi edeceği başka bir tür nesnesi döner. Böylece, kuralı tanımlayarak:
import Base: promote_rule
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64
birinin, 64-bit ve 32-bit kayan nokta değerleri bir araya getirildiğinde, bunların 64-bit kayan nokta değerine yükseltilmesi gerektiğini belirtir. Yükseltme türü, argüman türlerinden biri olmak zorunda değildir. Örneğin, aşağıdaki yükseltme kuralları hem Julia Temelinde geçerlidir:
promote_rule(::Type{BigInt}, ::Type{Float64}) = BigFloat
promote_rule(::Type{BigInt}, ::Type{Int8}) = BigInt
Son durumunda, sonuç türü BigInt
'dır çünkü BigInt
, keyfi hassasiyetli tam sayı aritmetiği için yeterince büyük tam sayıları tutabilen tek türdür. Ayrıca, hem promote_rule(::Type{A}, ::Type{B})
hem de promote_rule(::Type{B}, ::Type{A})
tanımlamanıza gerek olmadığını unutmayın - simetri, promote_rule
'ün terfi sürecinde nasıl kullanıldığıyla ima edilir.
promote_rule
fonksiyonu, promote_type
adlı ikinci bir fonksiyonu tanımlamak için bir yapı taşı olarak kullanılır. Bu fonksiyon, herhangi bir sayıdaki tür nesneleri verildiğinde, bu değerlerin promote
fonksiyonuna argüman olarak verilmesi durumunda hangi ortak türe terfi edileceğini döndürür. Dolayısıyla, gerçek değerlerin yokluğunda, belirli türlerin bir koleksiyonunun hangi türe terfi edeceğini bilmek isteyen biri promote_type
fonksiyonunu kullanabilir:
julia> promote_type(Int8, Int64)
Int64
Not edin ki promote_type
'ı doğrudan aşırı yüklemiyoruz: bunun yerine promote_rule
'ı aşırı yüklüyoruz. promote_type
, promote_rule
'ı kullanır ve simetrikliği ekler. Bunu doğrudan aşırı yüklemek belirsizlik hatalarına neden olabilir. Aşırı yüklediğimiz promote_rule
, şeylerin nasıl yükseltilmesi gerektiğini tanımlar ve bunu sorgulamak için promote_type
'ı kullanırız.
İçsel olarak, promote_type
promote
içinde hangi tür argüman değerlerinin terfi için dönüştürülmesi gerektiğini belirlemek için kullanılır. Meraklı okuyucu, terfi mekanizmasını tanımlayan yaklaşık 35 satırda promotion.jl
kodunu okuyabilir.
Case Study: Rational Promotions
Sonunda, Julia'nın rasyonel sayı türü ile ilgili devam eden vaka çalışmamızı tamamlıyoruz. Bu tür, aşağıdaki terfi kuralları ile terfi mekanizmasını nispeten sofistike bir şekilde kullanmaktadır:
import Base: promote_rule
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)
İlk kural, bir rasyonel sayının herhangi bir başka tam sayı türü ile yükseltilmesinin, pay/payda türünün diğer tam sayı türü ile yükseltilmesinin sonucu olan bir rasyonel türüne yükseltileceğini belirtir. İkinci kural, iki farklı rasyonel sayı türüne aynı mantığı uygular ve sonuç olarak, ilgili pay/payda türlerinin yükseltilmesinin bir rasyonelini üretir. Üçüncü ve son kural, bir rasyoneli bir float ile yükseltmenin, pay/payda türünün float ile yükseltilmesiyle aynı türü vereceğini belirtir.
Bu küçük tanıtım kuralları, türün yapıcıları ve sayılar için varsayılan convert
yöntemi ile birlikte, rasyonel sayıların Julia'nın diğer tüm sayısal türleri – tam sayılar, kayan nokta sayıları ve karmaşık sayılar – ile tamamen doğal bir şekilde etkileşimde bulunmasını sağlamak için yeterlidir. Uygun dönüşüm yöntemleri ve tanıtım kurallarını aynı şekilde sağlayarak, herhangi bir kullanıcı tanımlı sayısal tür, Julia'nın önceden tanımlanmış sayısallarıyla aynı şekilde doğal bir şekilde etkileşimde bulunabilir.