Types
Tip sistemleri geleneksel olarak iki oldukça farklı gruba ayrılmıştır: her program ifadesinin programın yürütülmesinden önce hesaplanabilir bir türü olması gereken statik tip sistemleri ve türler hakkında hiçbir şeyin bilinmediği dinamik tip sistemleri. Dinamik tip sistemlerinde, program tarafından işlenen gerçek değerler mevcut olduğunda, yürütme zamanında türler hakkında bilgi edinilir. Nesne yönelimi, statik olarak tiplenmiş dillerde, kodun derleme zamanında değerlerin kesin türleri bilinmeden yazılmasına izin vererek bazı esneklik sağlar. Farklı türler üzerinde çalışabilen kod yazma yeteneğine çok biçimlilik (polimorfizm) denir. Klasik dinamik olarak tiplenmiş dillerdeki tüm kod polimorfiktir: yalnızca türleri açıkça kontrol ederek veya nesneler çalışma zamanında işlemleri desteklemeyi başaramadığında, herhangi bir değerin türleri kısıtlanır.
Julia'nın tür sistemi dinamiktir, ancak belirli değerlerin belirli türlerde olduğunu belirtme olanağı sunarak statik tür sistemlerinin bazı avantajlarını kazanır. Bu, verimli kod üretiminde büyük bir yardımcı olabilir, ancak daha da önemlisi, işlev argümanlarının türleri üzerinde yöntem dağıtımının dil ile derinlemesine entegre edilmesine olanak tanır. Yöntem dağıtımı Methods'da ayrıntılı olarak incelenmiştir, ancak burada sunulan tür sistemine dayanmaktadır.
Julia'da türler atlanıldığında varsayılan davranış, değerlerin herhangi bir türde olmasına izin vermektir. Bu nedenle, türleri açıkça kullanmadan birçok yararlı Julia fonksiyonu yazmak mümkündür. Ancak, ek bir ifade gücüne ihtiyaç duyulduğunda, daha önce "tütsüz" olan koda kademeli olarak açık tür anotasyonları eklemek kolaydır. Anotasyon eklemenin üç ana amacı vardır: Julia'nın güçlü çoklu dağıtım mekanizmasından yararlanmak, insan okunabilirliğini artırmak ve programcı hatalarını yakalamaktır.
Julia'nın type systems lingo'sunda tanımlanması: dinamik, nominatif ve parametrik. Genel türler parametreleştirilebilir ve türler arasındaki hiyerarşik ilişkiler explicitly declared şeklindedir, implied by compatible structure yerine. Julia'nın tür sisteminin özellikle belirgin bir özelliği, somut türlerin birbirini alt tür olarak almayacağıdır: tüm somut türler sonludur ve yalnızca soyut türlerin üst türleri olabilir. Bu başlangıçta gereksiz kısıtlayıcı gibi görünse de, birçok faydalı sonucu vardır ve şaşırtıcı derecede az dezavantajı vardır. Davranışı miras alabilmenin, yapıyı miras alabilmekten çok daha önemli olduğu ortaya çıkıyor ve her ikisini miras almak, geleneksel nesne yönelimli dillerde önemli zorluklara neden oluyor. Julia'nın tür sisteminin öncelikle belirtilmesi gereken diğer yüksek düzeydeki yönleri şunlardır:
- Nesne ve nesne olmayan değerler arasında bir ayrım yoktur: Julia'daki tüm değerler, tek bir, tamamen bağlı bir tür grafiğine ait olan gerçek nesnelerdir; bu grafikteki tüm düğümler türler olarak eşit derecede birinci sınıftır.
- Anlamlı bir "derleme zamanı tipi" kavramı yoktur: Bir değerin sahip olduğu tek tip, program çalışırken gerçek tipidir. Bu, nesne yönelimli dillerde, statik derleme ile çok biçimliliğin bu ayrımı önemli hale getirdiği durumlarda "çalışma zamanı tipi" olarak adlandırılır.
- Sadece değerlerin türleri vardır, değişkenler ise yalnızca değerlere bağlı isimlerdir; ancak basitlik açısından "değişkenin türü" demek, "değişkenin referans verdiği değerin türü" anlamında kullanılabilir.
- Hem soyut hem de somut türler, diğer türlerle parametreleştirilebilir. Ayrıca, sembollerle,
isbits
ifadesinin true döndüğü herhangi bir türün değerleriyle (temelde, C türleri gibi saklanan sayılar ve bool'lar veya diğer nesnelere işaretçi içermeyenstruct
lar) ve bunların demetleriyle de parametreleştirilebilir. Tür parametreleri, referans verilmesi veya kısıtlanması gerekmiyorsa atlanabilir.
Julia'nın tür sistemi güçlü ve ifade edici olacak şekilde tasarlanmıştır, aynı zamanda net, sezgisel ve rahatsız edici olmaktan uzaktır. Birçok Julia programcısı, türleri açıkça kullanan kod yazma ihtiyacı hissetmeyebilir. Ancak, bazı programlama türleri, tanımlı türlerle daha net, daha basit, daha hızlı ve daha sağlam hale gelir.
Type Declarations
::
operatörü, programlardaki ifadeler ve değişkenlere tür açıklamaları eklemek için kullanılabilir. Bunu yapmanın iki ana nedeni vardır:
- Bir doğrulama olarak, programınızın beklediğiniz gibi çalıştığını onaylamaya yardımcı olmak için ve
- Derleyiciye ek tür bilgisi sağlamak için, bu da bazı durumlarda performansı artırabilir.
Bir değeri hesaplayan bir ifadeye eklendiğinde, ::
operatörü "bir örnektir" şeklinde okunur. Sol taraftaki ifadenin değeri ile sağ taraftaki türün bir örneği olduğunu belirtmek için her yerde kullanılabilir. Sağ taraftaki tür somut olduğunda, sol taraftaki değerin o türü uygulama olarak taşıması gerekir - tüm somut türlerin nihai olduğunu hatırlayın, bu nedenle hiçbir uygulama başka birinin alt türü olamaz. Tür soyut olduğunda, değerin soyut türün alt türü olan somut bir tür tarafından uygulanması yeterlidir. Tür doğrulaması doğru değilse, bir istisna fırlatılır, aksi takdirde sol taraftaki değer döndürülür:
julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64
julia> (1+2)::Int
3
Bu, bir tür iddiasının herhangi bir ifadeye yerinde eklenmesine olanak tanır.
Bir atamanın sol tarafındaki bir değişkene eklendiğinde veya bir local
bildiriminde yer aldığında, ::
operatörü biraz farklı bir anlam taşır: değişkenin her zaman belirtilen türe sahip olacağını bildirir, bu da C gibi statik olarak tiplenmiş bir dildeki bir tür bildirimine benzer. Değişkene atanan her değer, convert
kullanılarak bildirilen türe dönüştürülecektir:
julia> function foo()
x::Int8 = 100
x
end
foo (generic function with 1 method)
julia> x = foo()
100
julia> typeof(x)
Int8
Bu özellik, bir değişkene yapılan atamalardan birinin beklenmedik bir şekilde türünü değiştirmesi durumunda ortaya çıkabilecek performans "sorunlarını" önlemek için faydalıdır.
Bu "beyan" davranışı yalnızca belirli bağlamlarda meydana gelir:
local x::Int8 # in a local declaration
x::Int8 = 10 # as the left-hand side of an assignment
ve ve mevcut kapsamın tamamına uygulanır, hatta bildirimden önce bile.
Julia 1.8 itibarıyla, tür bildirimleri artık global kapsamda kullanılabilir; yani, global değişkenlere tür notları eklenerek onlara erişim tür açısından kararlı hale getirilebilir.
julia> x::Int = 10
10
julia> x = 3.5
ERROR: InexactError: Int64(3.5)
julia> function foo(y)
global x = 15.8 # throws an error when foo is called
return x + y
end
foo (generic function with 1 method)
julia> foo(10)
ERROR: InexactError: Int64(15.8)
Fonksiyon tanımlarına da bildirimler eklenebilir:
function sinc(x)::Float64
if x == 0
return 1
end
return sin(pi*x)/(pi*x)
end
Bu işlevden döndürme, bir değişkenin belirlenmiş türüne atanması gibi davranır: değer her zaman Float64
'e dönüştürülür.
Abstract Types
Soyut türler örneklenemez ve yalnızca tür grafiğinde düğüm olarak hizmet ederler, böylece ilişkili somut türlerin kümelerini tanımlarlar: bunlar, onların soyundan gelen somut türlerdir. Örneklenmeleri olmasa da soyut türlerle başlıyoruz çünkü bunlar tür sisteminin belkemiğidir: Julia'nın tür sistemini yalnızca bir nesne uygulamaları koleksiyonu olmaktan daha fazlası yapan kavramsal hiyerarşiyi oluştururlar.
Integers and Floating-Point Numbers içinde, çeşitli somut sayısal değer türlerini tanıttık: Int8
, UInt8
, Int16
, UInt16
, Int32
, UInt32
, Int64
, UInt64
, Int128
, UInt128
, Float16
, Float32
, ve Float64
. Farklı temsil boyutlarına sahip olmalarına rağmen, Int8
, Int16
, Int32
, Int64
ve Int128
hepsi işaretli tam sayı türleridir. Benzer şekilde UInt8
, UInt16
, UInt32
, UInt64
ve UInt128
hepsi işaretsiz tam sayı türleridir, oysa Float16
, Float32
ve Float64,
tam sayılar yerine kayan nokta türleri olarak farklıdır. Bir kod parçasının anlamlı olması yaygındır; örneğin, yalnızca argümanları bir tür tam sayı olduğunda anlam kazanır, ancak hangi belirli tür tam sayı olduğuna gerçekten bağlı değildir. Örneğin, en büyük ortak bölen algoritması tüm türdeki tam sayılar için çalışır, ancak kayan nokta sayıları için çalışmaz. Soyut türler, somut türlerin uyum sağlayabileceği bir bağlam sağlayarak bir tür hiyerarşisinin inşasına olanak tanır. Bu, örneğin, bir algoritmayı belirli bir tam sayı türüne kısıtlamadan, herhangi bir tam sayı türüne kolayca program yapmanıza olanak tanır.
Soyut türler abstract type
anahtar kelimesi kullanılarak tanımlanır. Soyut bir tür tanımlamak için genel sözdizimleri şunlardır:
abstract type «name» end
abstract type «name» <: «supertype» end
abstract type
anahtar kelimesi, adı «name»
ile verilen yeni bir soyut tür tanıtır. Bu ad, isteğe bağlı olarak <:
ve zaten var olan bir tür ile takip edilebilir; bu, yeni tanımlanan soyut türün bu "ebeveyn" türün bir alt türü olduğunu belirtir.
Hiçbir süpertip verilmediğinde, varsayılan süpertip Any
'dir - tüm nesnelerin örnekleri olduğu ve tüm türlerin alt türleri olduğu önceden tanımlanmış soyut bir tür. Tür teorisinde, Any
genellikle "üst" olarak adlandırılır çünkü tür grafiğinin zirvesindedir. Julia ayrıca tür grafiğinin en alt noktasında yazılan önceden tanımlanmış bir soyut "alt" tür olan Union{}
'ye sahiptir. Bu, Any
'nin tam tersidir: hiçbir nesne Union{}
'nin bir örneği değildir ve tüm türler Union{}
'nin süpertipleridir.
Julia'nın sayısal hiyerarşisini oluşturan bazı soyut türleri göz önünde bulunduralım:
abstract type Number end
abstract type Real <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer <: Real end
abstract type Signed <: Integer end
abstract type Unsigned <: Integer end
Number
türü Any
'nin doğrudan bir alt türüdür ve Real
onun çocuğudur. Sırasıyla, Real
'in iki çocuğu vardır (daha fazlası vardır, ancak burada yalnızca ikisi gösterilmektedir; diğerlerine daha sonra geleceğiz): Integer
ve AbstractFloat
, dünyayı tam sayıların ve gerçek sayıların temsilleri olarak ayırmaktadır. Gerçek sayıların temsilleri, kayan nokta türlerini içerir, ancak aynı zamanda rasyonel gibi diğer türleri de içerir. AbstractFloat
yalnızca gerçek sayıların kayan nokta temsillerini içerir. Tam sayılar daha da Signed
ve Unsigned
çeşitlerine ayrılır.
<:
operatörü genel olarak "bir alt türdür" anlamına gelir ve yukarıdaki gibi bildirimlerde kullanıldığında, sağdaki türü yeni bildirilen türün doğrudan üst türü olarak tanımlar. Ayrıca, sol operatörünün sağ operatörünün bir alt türü olduğu durumlarda true
döndüren bir alt tür operatörü olarak ifadelerde de kullanılabilir:
julia> Integer <: Number
true
julia> Integer <: AbstractFloat
false
Soyut türlerin önemli bir kullanımı, somut türler için varsayılan uygulamalar sağlamaktır. Basit bir örnek vermek gerekirse, düşünün:
function myplus(x,y)
x+y
end
İlk olarak, yukarıdaki argüman bildirimlerinin x::Any
ve y::Any
ile eşdeğer olduğunu belirtmek gerekir. Bu fonksiyon, örneğin myplus(2,5)
olarak çağrıldığında, dispatcher verilen argümanlarla eşleşen en spesifik myplus
yöntemini seçer. (Birden fazla dispatch hakkında daha fazla bilgi için Methods'ya bakın.)
Varsayılarak yukarıda belirtilenlerden daha spesifik bir yöntem bulunmadığında, Julia daha sonra yukarıda verilen genel işlev temelinde iki Int
argümanı için özel olarak myplus
adında bir yöntem tanımlar ve derler, yani, örtük olarak tanımlar ve derler:
function myplus(x::Int,y::Int)
x+y
end
ve nihayet, bu belirli yöntemi çağırır.
Bu nedenle, soyut türler programcıların daha sonra birçok somut tür kombinasyonu tarafından varsayılan yöntem olarak kullanılabilecek genel işlevler yazmalarına olanak tanır. Çoklu dağıtım sayesinde, programcı varsayılan veya daha spesifik yöntemin kullanılıp kullanılmayacağı üzerinde tam kontrole sahiptir.
Önemli bir nokta, programcının soyut türlerin argümanlarına sahip bir işleve güvenmesi durumunda performansta bir kayıp olmayacağıdır, çünkü bu işlev, çağrıldığı her somut argüman türü demeti için yeniden derlenir. (Ancak, soyut türlerin kapsayıcıları olan işlev argümanları durumunda bir performans sorunu olabilir; bkz. Performance Tips.)
Primitive Types
Mevcut bir ilkel türü yeni bir bileşik türün içine sarmak, kendi ilkel türünüzü tanımlamaktan neredeyse her zaman daha tercih edilir.
Bu işlevsellik, Julia'nın LLVM'nin desteklediği standart ilkel türleri başlatmasına olanak tanımak için vardır. Tanımlandıktan sonra, daha fazlasını tanımlamak için çok az neden vardır.
Bir ilkel tür, verileri sıradan eski bitlerden oluşan somut bir türdür. İlkel türlerin klasik örnekleri tam sayılar ve kayan nokta değerleridir. Çoğu dilden farklı olarak, Julia kendi ilkel türlerinizi tanımlamanıza izin verir; yalnızca sabit bir yerleşik set sağlamakla kalmaz. Aslında, standart ilkel türlerin hepsi dilin kendisinde tanımlanmıştır:
primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end
primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end
primitive type Int8 <: Signed 8 end
primitive type UInt8 <: Unsigned 8 end
primitive type Int16 <: Signed 16 end
primitive type UInt16 <: Unsigned 16 end
primitive type Int32 <: Signed 32 end
primitive type UInt32 <: Unsigned 32 end
primitive type Int64 <: Signed 64 end
primitive type UInt64 <: Unsigned 64 end
primitive type Int128 <: Signed 128 end
primitive type UInt128 <: Unsigned 128 end
Temel bir türü tanımlamak için genel sözdizimleri şunlardır:
primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end
Bit sayısı, türün ne kadar depolama alanı gerektirdiğini gösterir ve isim, yeni türe bir ad verir. Bir ilkel tür, isteğe bağlı olarak bir süper türün alt türü olarak tanımlanabilir. Eğer bir süper tür belirtilmezse, türün varsayılan olarak Any
'yi doğrudan süper türü olarak alır. Yukarıdaki Bool
ifadesi, dolayısıyla bir boolean değerinin depolamak için sekiz bit aldığını ve Integer
'yı doğrudan süper türü olarak aldığını belirtir. Şu anda yalnızca 8 bitin katları olan boyutlar desteklenmektedir ve yukarıda kullanılanlardan farklı boyutlarla LLVM hatalarıyla karşılaşma olasılığınız yüksektir. Bu nedenle, boolean değerleri, gerçekte yalnızca bir bit gerektirse de, sekiz bitten daha küçük olarak tanımlanamaz.
Bool
, Int8
ve UInt8
türleri, sekiz bitlik bellek parçaları olarak aynı temsillere sahiptir. Ancak Julia'nın tür sistemi nominatif olduğundan, aynı yapıya sahip olmalarına rağmen birbirinin yerine geçemezler. Aralarındaki temel fark, farklı üst türlere sahip olmalarıdır: 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566
'nın doğrudan üst türü Integer
, 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566
'nın üst türü Signed
ve 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566
'nın üst türü Unsigned
'dır. 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566
, 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566
ve 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566
arasındaki diğer tüm farklar, bu türlerin argüman olarak verildiğinde işlevlerin nasıl tanımlandığı ile ilgilidir. Bu nedenle nominatif bir tür sisteminin gerekli olduğu söylenebilir: eğer yapı türü belirleseydi ve bu da davranışı dikte etseydi, o zaman 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566
'nın 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566
veya 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566
'dan farklı bir şekilde davranmasını sağlamak imkansız olurdu.
Composite Types
Composite types çeşitli dillerde kayıtlar, yapılar veya nesneler olarak adlandırılır. Bir bileşik tür, adlandırılmış alanların bir koleksiyonudur; bunun bir örneği tek bir değer olarak ele alınabilir. Birçok dilde, bileşik türler kullanıcı tarafından tanımlanabilen tek türdür ve bunlar Julia'da en yaygın kullanılan kullanıcı tanımlı türdür.
Ana akışkan nesne yönelimli dillerde, C++, Java, Python ve Ruby gibi, bileşik türlerin de onlarla ilişkili adlandırılmış işlevleri vardır ve bu kombinasyona "nesne" denir. Ruby veya Smalltalk gibi daha saf nesne yönelimli dillerde, tüm değerler nesnedir, bileşik olup olmadıklarına bakılmaksızın. C++ ve Java gibi daha az saf nesne yönelimli dillerde ise, tam sayılar ve kayan nokta değerleri gibi bazı değerler nesne değildir, oysa kullanıcı tanımlı bileşik türlerin örnekleri gerçek nesneler olup ilişkili yöntemlere sahiptir. Julia'da, tüm değerler nesnedir, ancak işlevler, üzerinde çalıştıkları nesnelerle birleştirilmez. Bu, Julia'nın bir işlevin hangi yöntemini kullanacağına çoklu dağıtım ile karar vermesi gerektiğinden gereklidir; bu, bir işlevin argümanlarının tüm türlerinin bir yöntemi seçerken dikkate alındığı anlamına gelir, yalnızca ilk olan değil (yöntemler ve dağıtım hakkında daha fazla bilgi için Methods'ya bakın). Bu nedenle, işlevlerin yalnızca ilk argümanlarına "ait" olması uygun olmaz. Yöntemleri işlev nesneleri içinde düzenlemek, her nesnenin "içinde" adlandırılmış yöntem torbaları bulundurmaktan çok daha faydalı bir dil tasarımı yönü haline gelir.
Bileşik türler, struct
anahtar kelimesi ile tanıtılır ve ardından isteğe bağlı olarak ::
operatörü kullanılarak türlerle anotasyon yapılmış bir alan adları bloğu gelir:
julia> struct Foo
bar
baz::Int
qux::Float64
end
Tipler olmayan alanlar varsayılan olarak Any
türündedir ve dolayısıyla herhangi bir türde değer tutabilirler.
Yeni Foo
türündeki nesneler, Foo
türü nesnesini alanları için değerlerle bir fonksiyon gibi uygulayarak oluşturulur:
julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)
julia> typeof(foo)
Foo
Bir tür bir fonksiyon gibi uygulandığında buna yapıcı denir. İki yapıcı otomatik olarak oluşturulur (bunlara varsayılan yapıcılar denir). Biri herhangi bir argümanı kabul eder ve bunları alanların türlerine dönüştürmek için convert
çağrısını yapar, diğeri ise argümanları tam olarak alan türleriyle eşleşecek şekilde kabul eder. Her ikisinin de oluşturulmasının nedeni, yeni tanımlar eklemeyi kolaylaştırmasıdır; bu, varsayılan bir yapıcıyı istemeden değiştirmeyi önler.
bar
alanı tür açısından kısıtlanmadığı için herhangi bir değer alabilir. Ancak, baz
için değer Int
'e dönüştürülebilir olmalıdır:
julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(23.5)
Stacktrace:
[...]
fieldnames
fonksiyonunu kullanarak alan adlarının bir listesini bulabilirsiniz.
julia> fieldnames(Foo)
(:bar, :baz, :qux)
Bir bileşik nesnenin alan değerlerine geleneksel foo.bar
notasyonu kullanarak erişebilirsiniz:
julia> foo.bar
"Hello, world."
julia> foo.baz
23
julia> foo.qux
1.5
struct
ile tanımlanan bileşik nesneler değişmezdir; inşaattan sonra değiştirilemezler. Bu başlangıçta garip görünebilir, ancak birkaç avantajı vardır:
- Daha verimli olabilir. Bazı yapılar, dizilere verimli bir şekilde yerleştirilebilir ve bazı durumlarda derleyici, değişmez nesneleri tamamen ayırmaktan kaçınabilir.
- Tipin yapıcıları tarafından sağlanan değişmezleri ihlal etmek mümkün değildir.
- Değişmez nesneler kullanan kod, düşünülmesi daha kolay olabilir.
Değiştirilemez bir nesne, alanlar olarak değiştirilebilir nesneler, örneğin diziler içerebilir. İçerideki nesneler değiştirilebilir kalmaya devam eder; yalnızca değiştirilemez nesnenin kendisine ait alanlar farklı nesnelere işaret edecek şekilde değiştirilemez.
Gerekli olduğunda, değiştirilebilir bileşik nesneler mutable struct
anahtar kelimesi ile tanımlanabilir, bir sonraki bölümde tartışılacaktır.
Eğer bir değişmez yapının tüm alanları ayırt edilemezse (===
), o zaman bu alanları içeren iki değişmez değer de ayırt edilemez.
julia> struct X
a::Int
b::Float64
end
julia> X(1, 2) === X(1, 2)
true
Composite türlerin örneklerinin nasıl oluşturulduğu hakkında daha fazla şey söylemek mümkündür, ancak bu tartışma hem Parametric Types hem de Methods'ya bağlıdır ve kendi bölümünde ele alınması yeterince önemlidir: Constructors.
Birçok kullanıcı tanımlı tür X
için, o türün örneklerinin broadcasting için 0 boyutlu "skalarlar" olarak davranmasını sağlamak amacıyla Base.broadcastable(x::X) = Ref(x)
adlı bir yöntem tanımlamak isteyebilirsiniz.
Mutable Composite Types
Eğer bir bileşik tür struct
yerine mutable struct
ile tanımlanmışsa, o zaman onun örnekleri değiştirilebilir:
julia> mutable struct Bar
baz
qux::Float64
end
julia> bar = Bar("Hello", 1.5);
julia> bar.qux = 2.0
2.0
julia> bar.baz = 1//2
1//2
An extra interface between the fields and the user can be provided through Instance Properties. This grants more control on what can be accessed and modified using the bar.baz
notation.
Değişim desteği sağlamak için, bu tür nesneler genellikle yığın üzerinde tahsis edilir ve sabit bellek adreslerine sahiptir. Değiştirilebilir bir nesne, zamanla farklı değerler tutabilen küçük bir konteyner gibidir ve bu nedenle yalnızca adresi ile güvenilir bir şekilde tanımlanabilir. Buna karşılık, değişmez bir türün bir örneği belirli alan değerleri ile ilişkilidir - alan değerleri, nesne hakkında her şeyi anlatır. Bir türü değiştirilebilir yapma kararı verirken, aynı alan değerlerine sahip iki örneğin kimlik açısından aynı kabul edilip edilmeyeceğini veya zamanla bağımsız olarak değişmeleri gerekip gerekmediğini sorun. Eğer aynı kabul edileceklerse, tür muhtemelen değişmez olmalıdır.
Özetlemek gerekirse, Julia'da değişmezliği tanımlayan iki temel özellik vardır:
- Değiştirilemez bir türün değerini değiştirmek yasaktır.
- Bit türleri için bu, bir değerin bir kez ayarlandığında bit deseninin asla değişmeyeceği ve bu değerin bir bit türünün kimliği olduğu anlamına gelir.
- Bileşik türler için bu, alanlarının değerlerinin kimliğinin asla değişmeyeceği anlamına gelir. Alanlar bit türleri olduğunda, bu, bitlerinin asla değişmeyeceği anlamına gelir; değerleri değişken türler olan diziler gibi alanlar için, bu, alanların her zaman aynı değişken değere atıfta bulunacağı anlamına gelir, oysa o değişken değerin içeriği kendisi değiştirilebilir.
- Değiştirilemez bir türe sahip bir nesne, değiştirilemezliği nedeniyle orijinal nesne ile bir kopya arasında programatik olarak ayırt edilmesini imkansız hale getirdiğinden, derleyici tarafından serbestçe kopyalanabilir.
- Özellikle, bu, yeterince küçük olan değişmez değerlerin, örneğin tam sayılar ve ondalık sayılar, genellikle işlevlere kayıtlar (veya yığın tahsis edilmiş) aracılığıyla iletildiği anlamına gelir.
- Değişken değerler, diğer yandan, yığın üzerinde tahsis edilir ve işlevlere yığın üzerinde tahsis edilmiş değerlere işaretçi olarak geçilir, derleyicinin bunun böyle olmadığını kesin olarak bildiği durumlar dışında.
Bir veya daha fazla alanı değiştirilebilir bir yapının, değişmez olduğu bilinen alanları varsa, bu alanları aşağıda gösterildiği gibi const
kullanarak tanımlayabilirsiniz. Bu, değişmez yapıların bazı, ancak tüm optimizasyonlarını etkinleştirir ve const
ile işaretlenmiş belirli alanlar üzerinde invariants uygulamak için kullanılabilir.
const
değişkenleri, değiştirilebilir yapıların alanlarını işaretlemek için en az Julia 1.8 gerektirir.
julia> mutable struct Baz
a::Int
const b::Float64
end
julia> baz = Baz(1, 1.5);
julia> baz.a = 2
2
julia> baz.b = 2.0
ERROR: setfield!: const field .b of type Baz cannot be changed
[...]
Declared Types
Üç tür (soyut, ilkel, bileşik) önceki bölümlerde tartışılanlar aslında birbirleriyle yakından ilişkilidir. Aynı temel özellikleri paylaşırlar:
- Açıkça belirtilmişlerdir.
- Onların isimleri var.
- Onlar açıkça süper türleri ilan ettiler.
- Parametreleri olabilir.
Bu paylaşılan özellikler nedeniyle, bu türler aynı kavramın örnekleri olarak içsel olarak DataType
olarak temsil edilir; bu, bu türlerin herhangi birinin türüdür:
julia> typeof(Real)
DataType
julia> typeof(Int)
DataType
Bir DataType
soyut veya somut olabilir. Eğer somut ise, belirli bir boyutu, depolama düzeni ve (isteğe bağlı olarak) alan adları vardır. Bu nedenle, bir ilkel tür, sıfırdan büyük boyuta sahip ancak alan adı olmayan bir DataType
'dır. Bir bileşik tür, alan adlarına sahip olan veya boş (sıfır boyut) olan bir DataType
'dır.
Sistemdeki her somut değer, bir DataType
örneğidir.
Type Unions
Bir tür birliği, argüman türlerinin herhangi birinin tüm örneklerini içeren özel bir soyut türdür ve Union
anahtar kelimesi kullanılarak oluşturulur:
julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}
julia> 1 :: IntOrString
1
julia> "Hello!" :: IntOrString
"Hello!"
julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64
Birçok dilin derleyicileri, türler hakkında düşünmek için dahili bir birleşim yapısına sahiptir; Julia bunu programcıya doğrudan sunar. Julia derleyicisi, Union
türleri ile birlikte az sayıda tür bulunduğunda verimli kod üretebilir [1], her olası tür için ayrı dallarda özel kod üreterek.
Union
türünün özellikle yararlı bir durumu Union{T, Nothing}
'dır; burada T
herhangi bir tür olabilir ve Nothing
tekil türdür ve tek örneği nothing
nesnesidir. Bu desen, diğer dillerdeki Nullable
, Option
or Maybe
türlerinin Julia eşdeğeridir. Bir fonksiyon argümanını veya bir alanı Union{T, Nothing}
olarak tanımlamak, onu ya T
türünde bir değere ya da değer olmadığını belirtmek için nothing
olarak ayarlamaya olanak tanır. Daha fazla bilgi için this FAQ entry'e bakın.
Parametric Types
Julia'nın tür sisteminin önemli ve güçlü bir özelliği parametreli olmasıdır: türler parametreler alabilir, böylece tür bildirimleri aslında yeni türlerin bir ailesini tanıtır - her olası parametre değeri kombinasyonu için bir tane. generic programming gibi bazı diller, veri yapıları ve bunları manipüle etmek için algoritmaların tam türleri belirtmeden tanımlanmasına izin verir. Örneğin, ML, Haskell, Ada, Eiffel, C++, Java, C#, F# ve Scala gibi dillerde bazı biçimlerde genel programlama mevcuttur, sadece birkaçını saymak gerekirse. Bu dillerden bazıları gerçek parametreli polimorfizmi destekler (örneğin, ML, Haskell, Scala), diğerleri ise genel programlamanın ad-hoc, şablon tabanlı stillerini destekler (örneğin, C++, Java). Farklı dillerdeki birçok genel programlama ve parametreli tür çeşitliliği ile Julia'nın parametreli türlerini diğer dillerle karşılaştırmaya çalışmayacağız, bunun yerine Julia'nın sistemini kendi başına açıklamaya odaklanacağız. Ancak, Julia'nın dinamik olarak türlendirilmiş bir dil olduğunu ve tüm tür kararlarını derleme zamanında vermesi gerekmediğinden, statik parametreli tür sistemlerinde karşılaşılan birçok geleneksel zorluğun nispeten kolay bir şekilde ele alınabileceğini belirtmek gerekir.
Tüm beyan edilen türler ( DataType
çeşitleri) parametreleştirilebilir ve her durumda aynı sözdizimini kullanır. Bunları şu sırayla tartışacağız: önce parametrik bileşik türler, sonra parametrik soyut türler ve nihayet parametrik ilkel türler.
Parametric Composite Types
Tip parametreleri, tür adından hemen sonra, süslü parantezler içinde tanıtılır:
julia> struct Point{T}
x::T
y::T
end
Bu beyan, iki "koordinat" tutan yeni bir parametrik türü, Point{T}
, tanımlar. T'nin ne olduğunu sorabilirsiniz? İşte parametrik türlerin tam olarak amacı: bu, aslında herhangi bir tür olabilir (veya aslında herhangi bir bit türünün bir değeri olabilir, ancak burada açıkça bir tür olarak kullanılmıştır). Point{Float64}
, Point
tanımındaki T
'yi Float64
ile değiştirdiğinizde elde edilen somut bir türdür. Böylece, bu tek beyan aslında sınırsız sayıda türü tanımlar: Point{Float64}
, Point{AbstractString}
, Point{Int64}
, vb. Bunların her biri artık kullanılabilir somut bir türdür:
julia> Point{Float64}
Point{Float64}
julia> Point{AbstractString}
Point{AbstractString}
Point{Float64}
türü, koordinatları 64-bit kayan nokta değerleri olan bir noktadır, Point{AbstractString}
türü ise "koordinatları" dize nesneleri olan bir "noktadır" (bkz. Strings).
Point
kendisi de geçerli bir tür nesnesidir ve Point{Float64}
, Point{AbstractString}
vb. gibi tüm örnekleri alt türler olarak içerir:
julia> Point{Float64} <: Point
true
julia> Point{AbstractString} <: Point
true
Diğer türler elbette bunun alt türleri değildir:
julia> Float64 <: Point
false
julia> AbstractString <: Point
false
Beton Point
türleri, T
değerlerinin farklı olması durumunda birbirinin alt türü asla değildir:
julia> Point{Float64} <: Point{Int64}
false
julia> Point{Float64} <: Point{Real}
false
Bu son nokta çok önemlidir: Float64 <: Real
olmasına rağmen Point{Float64} <: Point{Real}
YOKTUR.
Başka bir deyişle, tip teorisi dilinde, Julia'nın tip parametreleri değişmezdir, covariant (or even contravariant) olmaktan ziyade. Bunun pratik nedenleri vardır: Point{Float64}
'in herhangi bir örneği, kavramsal olarak Point{Real}
örneği gibi olsa da, iki tipin bellek içindeki temsilleri farklıdır:
Point{Float64}
örneği, 64-bit değerlerin hemen yan yana gelmesiyle kompakt ve verimli bir şekilde temsil edilebilir;Point{Real}
örneği,Real
örneklerinin herhangi bir çiftini tutabilmelidir.Real
örnekleri keyfi boyut ve yapıda olabileceğinden, pratiktePoint{Real}
örneği, ayrı ayrı tahsis edilmişReal
nesnelerine işaret eden bir çift işaretçi olarak temsil edilmelidir.
Point{Float64}
nesnelerini anlık değerlerle saklayabilme yeteneğinden kazanılan verimlilik, diziler durumunda muazzam bir şekilde artmaktadır: bir Array{Float64}
, 64-bit kayan nokta değerlerinin bitişik bir bellek bloğu olarak saklanabilirken, bir Array{Real}
, bireysel olarak tahsis edilmiş Real
nesnelerine işaret eden bir işaretçi dizisi olmalıdır – bu nesneler boxed 64-bit kayan nokta değerleri olabilir, ancak aynı zamanda Real
soyut türünün uygulamaları olarak tanımlanan keyfi olarak büyük, karmaşık nesneler de olabilir.
Point{Float64}
bir Point{Real}
alt türü olmadığından, aşağıdaki yöntem Point{Float64}
türündeki argümanlara uygulanamaz:
function norm(p::Point{Real})
sqrt(p.x^2 + p.y^2)
end
Doğru bir şekilde, T
'nin Real
alt türü olduğu Point{T}
türündeki tüm argümanları kabul eden bir yöntemi tanımlamak için:
function norm(p::Point{<:Real})
sqrt(p.x^2 + p.y^2)
end
(Eşdeğer olarak, function norm(p::Point{T} where T<:Real)
veya function norm(p::Point{T}) where T<:Real
olarak tanımlanabilir; bkz. UnionAll Types.)
Daha fazla örnek Methods içinde daha sonra tartışılacaktır.
Bir Point
nesnesi nasıl oluşturulur? Bileşik türler için özel yapıcılar tanımlamak mümkündür, bu Constructors bölümünde ayrıntılı olarak tartışılacaktır, ancak herhangi bir özel yapıcı bildirimi olmaksızın, yeni bileşik nesneler oluşturmanın iki varsayılan yolu vardır; biri tür parametrelerinin açıkça verildiği, diğeri ise nesne yapıcısına verilen argümanlarla ima edildiği bir yoldur.
Point{Float64}
türü, T
yerine Float64
ile tanımlanan Point
ile eşdeğer bir somut tür olduğundan, uygun bir yapıcı olarak uygulanabilir:
julia> p = Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(p)
Point{Float64}
Varsayılan yapıcı için, her alan için tam olarak bir argüman sağlanmalıdır:
julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.
[...]
julia> Point{Float64}(1.0, 2.0, 3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.
[...]
Sadece parametreli türler için bir varsayılan yapıcı oluşturulur, çünkü onu geçersiz kılmak mümkün değildir. Bu yapıcı, herhangi bir argümanı kabul eder ve bunları alan türlerine dönüştürür.
Birçok durumda, oluşturmak istediğiniz Point
nesnesinin türünü sağlamak gereksizdir, çünkü yapıcı çağrısındaki argümanların türleri zaten dolaylı olarak tür bilgisi sağlar. Bu nedenle, parametre türü T
'nin ima edilen değeri belirsiz olmadığında, Point
'i bir yapıcı olarak da uygulayabilirsiniz:
julia> p1 = Point(1.0,2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(p1)
Point{Float64}
julia> p2 = Point(1,2)
Point{Int64}(1, 2)
julia> typeof(p2)
Point{Int64}
Point
durumunda, T
türü yalnızca Point
'e verilen iki argümanın aynı türde olması durumunda belirsiz bir şekilde ima edilir. Bu durum söz konusu olmadığında, yapıcı MethodError
ile başarısız olacaktır:
julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
Point(::T, !Matched::T) where T
@ Main none:2
Stacktrace:
[...]
Karma yöntemleri, böyle karışık durumları uygun bir şekilde ele almak için tanımlanabilir, ancak bu Constructors'te daha sonra tartışılacaktır.
Parametric Abstract Types
Parametrik soyut tür bildirimleri, çok benzer bir şekilde, soyut türlerin bir koleksiyonunu bildirir:
julia> abstract type Pointy{T} end
Bu beyan ile Pointy{T}
, T
'nin her türü veya tam sayı değeri için ayrı bir soyut türdür. Parametrik bileşik türlerde olduğu gibi, her böyle örnek Pointy
'nin bir alt türüdür:
julia> Pointy{Int64} <: Pointy
true
julia> Pointy{1} <: Pointy
true
Parametrik soyut türler, parametrik bileşik türler gibi değişmezdir:
julia> Pointy{Float64} <: Pointy{Real}
false
julia> Pointy{Real} <: Pointy{Float64}
false
Pointy{<:Gerçek}
notasyonu, Julia'daki kovaryant bir türü ifade etmek için kullanılabilirken, Pointy{>:Int}
kontravaryant bir türün analoğu olarak kullanılabilir, ancak teknik olarak bunlar tür kümelerini temsil eder (bkz. UnionAll Types).
julia> Pointy{Float64} <: Pointy{<:Real}
true
julia> Pointy{Real} <: Pointy{>:Int}
true
Eski soyut türler, somut türler üzerinde yararlı bir tür hiyerarşisi oluşturmak için hizmet ettiğinden, parametrik soyut türler de parametrik bileşik türler ile ilgili olarak aynı amaca hizmet eder. Örneğin, Point{T}
'yi Pointy{T}
'nin bir alt türü olarak aşağıdaki gibi tanımlamış olabiliriz:
julia> struct Point{T} <: Pointy{T}
x::T
y::T
end
Verilen böyle bir bildirimle, her T
seçimi için Point{T}
, Pointy{T}
'nin bir alt türü olarak kabul edilir:
julia> Point{Float64} <: Pointy{Float64}
true
julia> Point{Real} <: Pointy{Real}
true
julia> Point{AbstractString} <: Pointy{AbstractString}
true
Bu ilişki de değişmezdir:
julia> Point{Float64} <: Pointy{Real}
false
julia> Point{Float64} <: Pointy{<:Real}
true
Parametrik soyut türler, örneğin Pointy
, belirli bir türün özelliklerini tanımlamak için kullanılır. Bu türler, belirli bir türün davranışını ve özelliklerini tanımlarken, aynı zamanda esneklik sağlar. Örneğin, yalnızca bir koordinat gerektiren bir nokta benzeri uygulama oluşturduğumuzda, bu türler, uygulamanın yalnızca belirli bir durumda nasıl çalışacağını tanımlamak için kullanılabilir.
julia> struct DiagPoint{T} <: Pointy{T}
x::T
end
Artık hem Point{Float64}
hem de DiagPoint{Float64}
, Pointy{Float64}
soyutlamasının uygulamalarıdır ve benzer şekilde T
türü için her diğer olası seçimde de geçerlidir. Bu, Point
ve DiagPoint
için uygulanmış olan tüm Pointy
nesneleri tarafından paylaşılan ortak bir arayüze programlama yapmayı sağlar. Ancak, bunu tam olarak göstermek mümkün değildir; bununla birlikte, bir sonraki bölümde, Methods'da yöntemler ve dağıtım tanıtılana kadar.
Belirli durumlarda, tür parametrelerinin tüm olası türler üzerinde serbestçe değişmesi mantıklı olmayabilir. Bu tür durumlarda, T
'nin aralığını şu şekilde kısıtlayabilirsiniz:
julia> abstract type Pointy{T<:Real} end
Böyle bir beyan ile, T
yerine Real
alt türü olan herhangi bir türü kullanmak kabul edilebilir, ancak Real
alt türü olmayan türler kullanılamaz:
julia> Pointy{Float64}
Pointy{Float64}
julia> Pointy{Real}
Pointy{Real}
julia> Pointy{AbstractString}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Type{AbstractString}
julia> Pointy{1}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got a value of type Int64
Parametrik bileşik türler için parametreler aynı şekilde kısıtlanabilir:
struct Point{T<:Real} <: Pointy{T}
x::T
y::T
end
Gerçek dünya örneği olarak, bu parametreli tür makinelerinin nasıl faydalı olabileceğine dair, Julia'nın Rational
değişmez türünün gerçek tanımını burada veriyoruz (basitlik açısından yapıcıyı burada atlıyoruz), tam bir tam sayı oranını temsil ediyor:
struct Rational{T<:Integer} <: Real
num::T
den::T
end
Sadece tam sayı değerlerinin oranlarını almak mantıklıdır, bu nedenle T
parametre türü Integer
alt türü ile sınırlıdır ve tam sayıların oranı, gerçek sayı doğrusunda bir değeri temsil eder, bu nedenle herhangi bir Rational
Real
soyutlamasının bir örneğidir.
Tuple Types
Tuplelar, bir fonksiyonun argümanlarının bir soyutlamasıdır - fonksiyonun kendisi olmadan. Bir fonksiyonun argümanlarının belirgin yönleri, bunların sırası ve türleridir. Bu nedenle, bir tuple türü, her bir parametrenin bir alanın türü olduğu parametreli değişmez bir türe benzer. Örneğin, 2 elemanlı bir tuple türü aşağıdaki değişmez türe benzer:
struct Tuple2{A,B}
a::A
b::B
end
Ancak, üç ana fark vardır:
- Tuple türleri herhangi bir sayıda parametreye sahip olabilir.
- Tuple türleri parametrelerinde kovaryanttır:
Tuple{Int}
Tuple{Any}
'nin bir alt türüdür. Bu nedenleTuple{Any}
soyut bir tür olarak kabul edilir ve tuple türleri yalnızca parametreleri somut olduğunda somut hale gelir. - Demetler alan adlarına sahip değildir; alanlara yalnızca indeksle erişilir.
Tuple değerleri parantezler ve virgüller ile yazılır. Bir tuple oluşturulduğunda, talep üzerine uygun bir tuple türü üretilir:
julia> typeof((1,"foo",2.5))
Tuple{Int64, String, Float64}
Kovaryansın sonuçlarını not edin:
julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true
julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false
julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false
Sezgisel olarak, bu, bir fonksiyonun argümanlarının türünün, fonksiyonun imzasının bir alt türü olmasıyla (imza eşleştiğinde) ilişkilidir.
Vararg Tuple Types
Bir tuple türünün son parametresi, herhangi bir sayıda son elemanı belirtmek için kullanılan özel değer Vararg
olabilir:
julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString, Vararg{Int64}}
julia> isa(("1",), mytupletype)
true
julia> isa(("1",1), mytupletype)
true
julia> isa(("1",1,2), mytupletype)
true
julia> isa(("1",1,2,3.0), mytupletype)
false
Ayrıca Vararg{T}
, T
türünden sıfır veya daha fazla öğeye karşılık gelir. Vararg demet türleri, varargs yöntemleri tarafından kabul edilen argümanları temsil etmek için kullanılır (bkz. Varargs Functions).
Özel değer Vararg{T,N}
(bir tuple türünün son parametresi olarak kullanıldığında) tam olarak N
adet T
türünde elemanı ifade eder. NTuple{N,T}
ise Tuple{Vararg{T,N}}
için pratik bir takma isimdir, yani tam olarak N
adet T
türünde eleman içeren bir tuple türüdür.
Named Tuple Types
Adlandırılmış demetler, NamedTuple
türünün örnekleridir ve iki parametreye sahiptir: alan adlarını veren bir sembol demeti ve alan türlerini veren bir demet türü. Kolaylık olması açısından, NamedTuple
türleri, bu türleri key::Type
bildirimleri aracılığıyla struct
benzeri bir sözdizimi ile tanımlamak için @NamedTuple
makrosu kullanılarak yazdırılır; burada atlanan ::Type
, ::Any
ile karşılık gelir.
julia> typeof((a=1,b="hello")) # prints in macro form
@NamedTuple{a::Int64, b::String}
julia> NamedTuple{(:a, :b), Tuple{Int64, String}} # long form of the type
@NamedTuple{a::Int64, b::String}
@NamedTuple
makrosunun begin ... end
biçimi, bildirimlerin birden fazla satıra bölünmesine izin verir (bir yapı bildirimiyle benzer şekilde), ancak aksi takdirde eşdeğerdir:
julia> @NamedTuple begin
a::Int
b::String
end
@NamedTuple{a::Int64, b::String}
Bir NamedTuple
türü, tek bir demet argümanı kabul eden bir yapıcı olarak kullanılabilir. Oluşturulan NamedTuple
türü, her iki parametrenin de belirtildiği somut bir tür veya yalnızca alan adlarını belirten bir tür olabilir:
julia> @NamedTuple{a::Float32,b::String}((1, ""))
(a = 1.0f0, b = "")
julia> NamedTuple{(:a, :b)}((1, ""))
(a = 1, b = "")
Eğer alan türleri belirtilmişse, argümanlar dönüştürülür. Aksi takdirde argümanların türleri doğrudan kullanılır.
Parametric Primitive Types
Temel türler parametrik olarak da tanımlanabilir. Örneğin, işaretçiler, Julia'da şu şekilde tanımlanacak olan temel türler olarak temsil edilir:
# 32-bit system:
primitive type Ptr{T} 32 end
# 64-bit system:
primitive type Ptr{T} 64 end
Bu bildirimlerin tipik parametrik bileşik türlere kıyasla biraz garip olan özelliği, T
tür parametresinin türün tanımında kullanılmamasıdır – bu sadece soyut bir etiket olup, yalnızca tür parametreleriyle farklılaştırılan, aynı yapıya sahip bir tür ailesini tanımlar. Böylece, Ptr{Float64}
ve Ptr{Int64}
farklı türlerdir, her ne kadar aynı temsillere sahip olsalar da. Ve elbette, tüm belirli işaretçi türleri, şemsiye Ptr
türünün alt türleridir:
julia> Ptr{Float64} <: Ptr
true
julia> Ptr{Int64} <: Ptr
true
UnionAll Types
Biz, Ptr
gibi bir parametreli türün tüm örneklerinin süper türü olarak davrandığını söyledik (Ptr{Int64}
vb.). Bu nasıl çalışıyor? Ptr
kendisi normal bir veri türü olamaz, çünkü referans verilen verinin türünü bilmeden bu türün bellek işlemleri için açıkça kullanılamaz. Cevap, Ptr
(veya Array
gibi diğer parametreli türler) UnionAll
türü olarak adlandırılan farklı bir türdür. Böyle bir tür, bazı parametrelerin tüm değerleri için türlerin tekrarlanan birleşimini ifade eder.
UnionAll
türleri genellikle where
anahtar kelimesi kullanılarak yazılır. Örneğin, Ptr
daha doğru bir şekilde Ptr{T} where T
olarak yazılabilir; bu, T
için bazı değerler olan tüm Ptr{T}
türündeki değerleri ifade eder. Bu bağlamda, T
parametresi genellikle "tip değişkeni" olarak da adlandırılır, çünkü bu, türler üzerinde değişen bir değişken gibidir. Her where
, tek bir tip değişkeni tanıtır, bu nedenle bu ifadeler, birden fazla parametreye sahip türler için iç içe geçmiş durumdadır; örneğin Array{T,N} where N where T
.
A{B,C}
tür uygulama sözdizimi, A
'nın bir UnionAll
türü olmasını gerektirir ve önce B
'yi A
'daki en dıştaki tür değişkeninin yerine koyar. Sonuç, C
'nin daha sonra yerleştirileceği başka bir UnionAll
türü olmalıdır. Bu nedenle A{B,C}
, A{B}{C}
ile eşdeğerdir. Bu, Array{Float64}
gibi bir türü kısmen başlatmanın mümkün olmasının nedenini açıklar: ilk parametre değeri sabitlenmiştir, ancak ikincisi hala tüm olası değerler üzerinde değişir. Açık where
sözdizimini kullanarak, parametrelerin herhangi bir alt kümesi sabitlenebilir. Örneğin, tüm 1 boyutlu dizilerin türü Array{T,1} where T
olarak yazılabilir.
Tip değişkenleri alt tür ilişkileri ile kısıtlanabilir. Array{T} where T<:Integer
, eleman türü Integer
olan tüm dizilere atıfta bulunur. Array{<:Integer}
sözdizimi, Array{T} where T<:Integer
için pratik bir kısayoldur. Tip değişkenleri hem alt hem de üst sınırları olabilir. Array{T} where Int<:T<:Number
, Int
içerebilen Number
dizilerinin tümüne atıfta bulunur (çünkü T
, en az Int
kadar büyük olmalıdır). where T>:Int
sözdizimi, bir tip değişkeninin yalnızca alt sınırını belirtmek için de çalışır ve Array{>:Int}
, Array{T} where T>:Int
ile eşdeğerdir.
where
ifadeleri iç içe geçtiğinden, tür değişkeni sınırları dıştaki tür değişkenlerine atıfta bulunabilir. Örneğin Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real
, ilk elemanı bazı Real
olan 2-tuple'ları ve ikinci elemanı ilk tuple elemanının türünü içeren herhangi bir türde dizinin Array
'ini ifade eder.
where
anahtar kelimesi, daha karmaşık bir bildirimin içinde iç içe yer alabilir. Örneğin, aşağıdaki bildirimlerle oluşturulan iki türü düşünün:
julia> const T1 = Array{Array{T, 1} where T, 1}
Vector{Vector} (alias for Array{Array{T, 1} where T, 1})
julia> const T2 = Array{Array{T, 1}, 1} where T
Array{Vector{T}, 1} where T
T1
türü, 1 boyutlu dizilerden oluşan 1 boyutlu bir dizi tanımlar; iç dizilerin her biri aynı türdeki nesnelerden oluşur, ancak bu tür bir iç diziden diğerine değişebilir. Öte yandan, T2
türü, iç dizilerin hepsinin aynı türde olması gereken 1 boyutlu dizilerden oluşan 1 boyutlu bir dizi tanımlar. T2
soyut bir türdür; örneğin, Array{Array{Int,1},1} <: T2
iken, T1
somut bir türdür. Sonuç olarak, T1
sıfır argümanlı bir yapıcı ile oluşturulabilir a=T1()
ancak T2
oluşturulamaz.
Böyle türleri adlandırmak için, işlev tanımı sözdiziminin kısa biçimine benzer bir sözdizimi vardır:
Vector{T} = Array{T, 1}
Bu, const Vector = Array{T,1} where T
ile eşdeğerdir. Vector{Float64}
yazmak, Array{Float64,1}
yazmakla eşdeğerdir ve şemsiye türü Vector
, ikinci parametrenin – dizi boyutlarının sayısının – 1 olduğu tüm Array
nesnelerini içerir, eleman türü ne olursa olsun. Parametrik türlerin her zaman tam olarak belirtilmesi gereken dillerde bu pek yardımcı değildir, ancak Julia'da bu, herhangi bir eleman türündeki tüm bir boyutlu yoğun dizileri içeren soyut tür için sadece Vector
yazmanıza olanak tanır.
Singleton types
Değiştirilemez bileşik türler, alanı olmayan singleton olarak adlandırılır. Resmi olarak, eğer
T
, değiştirilemez bir bileşik türdür (yanistruct
ile tanımlanmıştır),a T ise && b T ise
demektira === b
,
o zaman T
bir singleton türüdür.[2] Base.issingletontype
bir türün singleton türü olup olmadığını kontrol etmek için kullanılabilir. Abstract types yapısı gereği singleton türler olamaz.
Tanım gereği, bu türlerin yalnızca bir örneği olabileceği sonucuna varılır:
julia> struct NoFields
end
julia> NoFields() === NoFields()
true
julia> Base.issingletontype(NoFields)
true
===
fonksiyonu, oluşturulan NoFields
örneklerinin aslında bir ve aynı olduğunu doğrular.
Parametrik türler yukarıdaki koşul sağlandığında tekil türler olabilir. Örneğin,
julia> struct NoFieldsParam{T}
end
julia> Base.issingletontype(NoFieldsParam) # Can't be a singleton type ...
false
julia> NoFieldsParam{Int}() isa NoFieldsParam # ... because it has ...
true
julia> NoFieldsParam{Bool}() isa NoFieldsParam # ... multiple instances.
true
julia> Base.issingletontype(NoFieldsParam{Int}) # Parametrized, it is a singleton.
true
julia> NoFieldsParam{Int}() === NoFieldsParam{Int}()
true
Types of functions
Her fonksiyonun kendi türü vardır, bu da Function
'ın bir alt türüdür.
julia> foo41(x) = x + 1
foo41 (generic function with 1 method)
julia> typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)
typeof(foo41)
ifadesinin kendisi olarak yazdırıldığını not edin. Bu, yalnızca bir yazdırma konvansiyonudur, çünkü bu, diğer herhangi bir değer gibi kullanılabilen birinci sınıf bir nesnedir:
julia> T = typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)
julia> T <: Function
true
Üst düzeyde tanımlanan fonksiyon türleri tekil nesnelerdir. Gerekirse, bunları ===
ile karşılaştırabilirsiniz.
Closures ayrıca genellikle #<sayı>
ile biten adlarla yazdırılan kendi türlerine sahiptir. Farklı konumlarda tanımlanan işlevler için adlar ve türler ayrıdır, ancak oturumlar arasında aynı şekilde yazdırılacağı garanti edilmez.
julia> typeof(x -> x + 1)
var"#9#10"
Kapatma türleri mutlaka tekil değildir.
julia> addy(y) = x -> x + y
addy (generic function with 1 method)
julia> typeof(addy(1)) === typeof(addy(2))
true
julia> addy(1) === addy(2)
false
julia> Base.issingletontype(typeof(addy(1)))
false
Type{T}
type selectors
Her tür T
için, Type{T}
yalnızca T
nesnesinin bir örneği olan soyut bir parametrik türdür. Parametric Methods ve conversions konularını tartışana kadar, bu yapının faydasını açıklamak zordur, ancak kısaca, belirli türler üzerinde değerler olarak işlev davranışını özelleştirmeye olanak tanır. Bu, bir türün, argümanlarından birinin türüyle ima edilmek yerine, açık bir argüman olarak verildiği durumlarda (özellikle parametrik olanlar) yöntemler yazmak için yararlıdır.
Tanım biraz zor anlaşıldığı için, bazı örneklere bakalım:
julia> isa(Float64, Type{Float64})
true
julia> isa(Real, Type{Float64})
false
julia> isa(Real, Type{Real})
true
julia> isa(Float64, Type{Real})
false
Başka bir deyişle, isa(A, Type{B})
yalnızca A
ve B
aynı nesne olduğunda ve o nesne bir tür olduğunda doğrudur.
Özellikle, parametreli türler invariant olduğundan, şunları elde ediyoruz:
julia> struct TypeParamExample{T}
x::T
end
julia> TypeParamExample isa Type{TypeParamExample}
true
julia> TypeParamExample{Int} isa Type{TypeParamExample}
false
julia> TypeParamExample{Int} isa Type{TypeParamExample{Int}}
true
Parametresiz, Type
basitçe tüm tür nesnelerinin örnekleri olan soyut bir türdür:
julia> isa(Type{Float64}, Type)
true
julia> isa(Float64, Type)
true
julia> isa(Real, Type)
true
Herhangi bir nesne bir tür değilse, Type
'ın bir örneği değildir:
julia> isa(1, Type)
false
julia> isa("foo", Type)
false
While Type
is part of Julia's type hierarchy like any other abstract parametric type, it is not commonly used outside method signatures except in some special cases. Another important use case for Type
is sharpening field types which would otherwise be captured less precisely, e.g. as DataType
in the example below where the default constructor could lead to performance problems in code relying on the precise wrapped type (similarly to abstract type parameters).
julia> struct WrapType{T}
value::T
end
julia> WrapType(Float64) # default constructor, note DataType
WrapType{DataType}(Float64)
julia> WrapType(::Type{T}) where T = WrapType{Type{T}}(T)
WrapType
julia> WrapType(Float64) # sharpened constructor, note more precise Type{Float64}
WrapType{Type{Float64}}(Float64)
Type Aliases
Bazen zaten ifade edilebilir bir tür için yeni bir ad tanıtmak kullanışlıdır. Bu, basit bir atama ifadesi ile yapılabilir. Örneğin, UInt
ya UInt32
ya da UInt64
olarak, sistemdeki işaretçilerin boyutuna uygun olarak takma ad verilmiştir:
# 32-bit system:
julia> UInt
UInt32
# 64-bit system:
julia> UInt
UInt64
Bu, base/boot.jl
dosyasındaki aşağıdaki kod aracılığıyla gerçekleştirilir:
if Int === Int64
const UInt = UInt64
else
const UInt = UInt32
end
Elbette, bu Int
'in hangi takma adla tanımlandığına bağlıdır - ancak bu, doğru tür olarak önceden tanımlanmıştır - ya Int32
ya da Int64
.
(Not edin Int
gibi, Float
belirli bir boyut için bir tür takma adı olarak mevcut değildir AbstractFloat
. Tam sayı kayıtları ile, Int
'in boyutu o makinedeki yerel bir işaretçinin boyutunu yansıtırken, kayan nokta kayıt boyutları IEEE-754 standardı tarafından belirtilmiştir.)
Tip adı takma adları parametreli olabilir:
julia> const Family{T} = Set{T}
Set
julia> Family{Char} === Set{Char}
true
Operations on Types
Julia'daki türler nesne oldukları için, sıradan fonksiyonlar bunlar üzerinde işlem yapabilir. Türlerle çalışmak veya onları keşfetmek için özellikle yararlı olan bazı fonksiyonlar zaten tanıtılmıştır; bunlardan biri, sol operatörünün sağ operatörünün bir alt türü olup olmadığını gösteren <:
operatörüdür.
isa
fonksiyonu bir nesnenin belirli bir türde olup olmadığını test eder ve true veya false döner:
julia> isa(1, Int)
true
julia> isa(1, AbstractFloat)
false
typeof
fonksiyonu, kılavuzda örneklerde zaten kullanılmıştır, argümanının türünü döndürür. Yukarıda belirtildiği gibi, türler nesne olduğundan, onların da türleri vardır ve biz de bu türlerin ne olduğunu sorabiliriz:
julia> typeof(Rational{Int})
DataType
julia> typeof(Union{Real,String})
Union
Ne olursa olsun süreci tekrar edersek? Bir türün türü nedir? Olması gerektiği gibi, türler tümüyle bileşik değerlerdir ve bu nedenle hepsinin DataType
türü vardır:
julia> typeof(DataType)
DataType
julia> typeof(Union)
DataType
DataType
kendi türüdür.
Başka bir işlem, bazı türlere uygulanan supertype
olup, bir türün süper türünü ortaya çıkarır. Sadece beyan edilen türler (DataType
) belirsiz olmayan süper türlere sahiptir:
julia> supertype(Float64)
AbstractFloat
julia> supertype(Number)
Any
julia> supertype(AbstractString)
Any
julia> supertype(Any)
Any
Eğer supertype
'yı diğer tür nesnelere (veya tür olmayan nesnelere) uygularsanız, bir MethodError
hatası oluşur:
julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
The function `supertype` exists, but no method is defined for this combination of argument types.
Closest candidates are:
[...]
Custom pretty-printing
Sıklıkla, bir türün örneklerinin nasıl görüntüleneceğini özelleştirmek istenir. Bu, show
fonksiyonunu aşırı yükleyerek gerçekleştirilir. Örneğin, karmaşık sayıları kutupsal formda temsil etmek için bir tür tanımlarsak:
julia> struct Polar{T<:Real} <: Number
r::T
Θ::T
end
julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar
Burada, farklı Real
türünde argümanlar alabilmesi için özel bir yapıcı fonksiyonu ekledik ve bunları ortak bir türe yükselttik (bkz. Constructors ve Conversion and Promotion). (Elbette, bunun bir Number
gibi davranabilmesi için birçok başka yöntemi de tanımlamamız gerekecek, örneğin +
, *
, one
, zero
, yükseltme kuralları vb.) Varsayılan olarak, bu türün örnekleri oldukça basit bir şekilde, tür adı ve alan değerleri hakkında bilgi gösterir, örneğin Polar{Float64}(3.0,4.0)
.
Eğer bunu 3.0 * exp(4.0im)
olarak görüntülemek istiyorsak, bir nesneyi belirli bir çıktı nesnesine io
(bir dosya, terminal, tampon vb. temsil eden; bkz. Networking and Streams) yazdırmak için aşağıdaki yöntemi tanımlamalıyız:
julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")
Polar
nesnelerinin görüntülenmesi üzerinde daha ayrıntılı kontrol mümkündür. Özellikle, bazen REPL ve diğer etkileşimli ortamlarda tek bir nesneyi görüntülemek için kullanılan ayrıntılı çok satırlı yazdırma formatına ve ayrıca print
için veya nesneyi başka bir nesnenin parçası olarak (örneğin bir dizide) görüntülemek için kullanılan daha kompakt tek satırlı formata ihtiyaç duyulabilir. Varsayılan olarak show(io, z)
fonksiyonu her iki durumda da çağrılır, ancak text/plain
MIME türünü ikinci argüman olarak alan show
fonksiyonunun üç argümanlı bir formunu aşırı yükleyerek bir nesneyi görüntülemek için farklı bir çok satırlı format tanımlayabilirsiniz (bkz. Multimedia I/O), örneğin:
julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
print(io, "Polar{$T} complex number:\n ", z)
(Burada print(..., z)
ifadesi, 2-argümanlı show(io, z)
metodunu çağıracaktır.) Bu sonuçta:
julia> Polar(3, 4.0)
Polar{Float64} complex number:
3.0 * exp(4.0im)
julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Vector{Polar{Float64}}:
3.0 * exp(4.0im)
4.0 * exp(5.3im)
tek satırlık show(io, z)
biçiminin Polar
değerlerinin bir dizisi için hala kullanıldığı yer. Teknik olarak, REPL bir satırın sonucunu görüntülemek için display(z)
çağrısını yapar; bu, varsayılan olarak show(stdout, MIME("text/plain"), z)
'ye, bu da varsayılan olarak show(stdout, z)
'ye döner, ancak yeni display
yöntemleri tanımlamamalısınız, aksi takdirde yeni bir çoklu ortam görüntüleme işleyici tanımlıyorsanız (bkz. Multimedia I/O).
Ayrıca, bu tür ortamların (örneğin IJulia) desteklediği nesnelerin daha zengin bir şekilde görüntülenmesini sağlamak için diğer MIME türleri için de show
yöntemleri tanımlayabilirsiniz (HTML, resimler vb.). Örneğin, Polar
nesnelerinin üst simgeler ve italik ile biçimlendirilmiş HTML görüntülemesini şu şekilde tanımlayabiliriz:
julia> Base.show(io::IO, ::MIME"text/html", z::Polar{T}) where {T} =
println(io, "<code>Polar{$T}</code> complex number: ",
z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")
Bir Polar
nesnesi, HTML görüntülemeyi destekleyen bir ortamda otomatik olarak görüntülenecektir, ancak isterseniz HTML çıktısı almak için show
fonksiyonunu manuel olarak çağırabilirsiniz:
julia> show(stdout, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>
An HTML renderer would display this as: Polar{Float64}
complex number: 3.0 e4.0 i
Bir kural olarak, tek satırlık show
yöntemi, gösterilen nesneyi oluşturmak için geçerli bir Julia ifadesi yazdırmalıdır. Bu show
yöntemi, yukarıdaki Polar
için tek satırlık show
yöntemimizdeki çarpma operatörü (*
) gibi infiks operatörleri içeriyorsa, başka bir nesnenin parçası olarak yazdırıldığında doğru bir şekilde ayrıştırılmayabilir. Bunu görmek için, belirli bir Polar
türü örneğinin karesini alan ifade nesnesini düşünün (bkz. Program representation):
julia> a = Polar(3, 4.0)
Polar{Float64} complex number:
3.0 * exp(4.0im)
julia> print(:($a^2))
3.0 * exp(4.0im) ^ 2
Çünkü ^
operatörünün önceliği *
operatöründen daha yüksektir (bkz. Operator Precedence and Associativity), bu çıktı a ^ 2
ifadesini doğru bir şekilde temsil etmez; bu ifade (3.0 * exp(4.0im)) ^ 2
ile eşit olmalıdır. Bu sorunu çözmek için, yazdırma sırasında ifade nesnesi tarafından dahili olarak çağrılan Base.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int)
için özel bir yöntem oluşturmalıyız:
julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int)
if Base.operator_precedence(:*) <= precedence
print(io, "(")
show(io, z)
print(io, ")")
else
show(io, z)
end
end
julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)
Yukarıda tanımlanan yöntem, çağırma operatörünün önceliği çarpmanın önceliği ile aynı veya daha yüksek olduğunda show
çağrısının etrafına parantez ekler. Bu kontrol, parantezsiz doğru bir şekilde ayrıştırılan ifadelerin (örneğin :($a + 2)
ve :($a == 2)
) yazdırıldığında parantezleri atlamasına olanak tanır:
julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)
julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)
Bazen, show
yöntemlerinin davranışını bağlamına göre ayarlamak faydalı olabilir. Bu, bağlamsal özelliklerin bir sarmalanmış IO akışı ile birlikte geçilmesine olanak tanıyan IOContext
türü aracılığıyla gerçekleştirilebilir. Örneğin, :compact
özelliği true
olarak ayarlandığında show
yöntemimizde daha kısa bir temsil oluşturabiliriz; eğer özellik false
veya yoksa uzun temsile geri döneriz:
julia> function Base.show(io::IO, z::Polar)
if get(io, :compact, false)::Bool
print(io, z.r, "ℯ", z.Θ, "im")
else
print(io, z.r, " * exp(", z.Θ, "im)")
end
end
Bu yeni kompakt temsil, geçirilen IO akışının :compact
özelliği ayarlanmış bir IOContext
nesnesi olduğu durumlarda kullanılacaktır. Özellikle, yatay alanın sınırlı olduğu çoklu sütunlara sahip dizileri yazdırırken bu durum geçerlidir:
julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im
julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Matrix{Polar{Float64}}:
3.0ℯ4.0im 4.0ℯ5.3im
IOContext
belgesine bakarak yazdırma ayarlarını düzenlemek için kullanılabilecek yaygın özelliklerin bir listesini bulabilirsiniz.
"Value types"
Julia'da, true
veya false
gibi bir değer üzerinde dispatch yapamazsınız. Ancak, parametreli türler üzerinde dispatch yapabilirsiniz ve Julia, "sade bit" değerlerini (Türler, Semboller, Tam sayılar, kayan nokta sayıları, demetler vb.) tür parametreleri olarak dahil etmenize izin verir. Yaygın bir örnek, Array{T,N}
içindeki boyut parametresidir; burada T
bir türdür (örneğin, Float64
), ancak N
sadece bir Int
'dir.
Kendi özel türlerinizi oluşturabilir ve bunları parametre olarak değerler alacak şekilde kullanarak özel türlerin dağıtımını kontrol edebilirsiniz. Bu fikri açıklamak için, parametreli tür Val{x}
ve onun yapıcısı Val(x) = Val{x}()
, daha karmaşık bir hiyerarşiye ihtiyaç duymadığınız durumlar için bu tekniği kullanmanın alışılmış bir yolu olarak tanıtalım.
Val
olarak tanımlanmıştır:
julia> struct Val{x}
end
julia> Val(x) = Val{x}()
Val
Val
uygulamasasında daha fazlası yoktur. Julia'nın standart kütüphanesindeki bazı fonksiyonlar Val
örneklerini argüman olarak kabul eder ve kendi fonksiyonlarınızı yazmak için de bunu kullanabilirsiniz. Örneğin:
julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)
julia> firstlast(::Val{false}) = "Last"
firstlast (generic function with 2 methods)
julia> firstlast(Val(true))
"First"
julia> firstlast(Val(false))
"Last"
Julia'da tutarlılık için, çağrı noktasının her zaman bir Val
örneği geçirmesi gerekir, yani foo(Val(:bar))
kullanın, foo(Val{:bar})
yerine.
Parametrik "değer" türlerini, özellikle de Val
'ı yanlış kullanmanın son derece kolay olduğunu belirtmek gerekir; olumsuz durumlarda, kodunuzun performansını çok kötü hale getirebilirsiniz. Özellikle, yukarıda gösterildiği gibi gerçek kod yazmak istemezsiniz. Val
'ın doğru (ve yanlış) kullanımları hakkında daha fazla bilgi için lütfen the more extensive discussion in the performance tips okuyun.