Types

تاريخ أنظمة الأنواع تقليديًا إلى معسكرين مختلفين تمامًا: أنظمة الأنواع الثابتة، حيث يجب أن يكون لكل تعبير برمجي نوع يمكن حسابه قبل تنفيذ البرنامج، وأنظمة الأنواع الديناميكية، حيث لا يُعرف شيء عن الأنواع حتى وقت التشغيل، عندما تكون القيم الفعلية التي يتم التعامل معها بواسطة البرنامج متاحة. يسمح التوجه الكائني ببعض المرونة في اللغات ذات الأنواع الثابتة من خلال السماح بكتابة الشيفرة دون معرفة الأنواع الدقيقة للقيم في وقت الترجمة. تُسمى القدرة على كتابة الشيفرة التي يمكن أن تعمل على أنواع مختلفة بالتعددية الشكلية. كل الشيفرات في اللغات الكلاسيكية ذات الأنواع الديناميكية هي متعددة الأشكال: فقط من خلال التحقق صراحة من الأنواع، أو عندما تفشل الكائنات في دعم العمليات في وقت التشغيل، يتم تقييد أنواع أي قيم على الإطلاق.

نظام نوع جوليا ديناميكي، لكنه يكتسب بعض مزايا أنظمة النوع الثابتة من خلال جعل من الممكن الإشارة إلى أن بعض القيم هي من أنواع محددة. يمكن أن يكون هذا مفيدًا جدًا في توليد كود فعال، ولكن بشكل أكثر أهمية، فإنه يسمح بتكامل عميق لتوجيه الأساليب بناءً على أنواع وسائط الدالة مع اللغة. يتم استكشاف توجيه الأساليب بالتفصيل في Methods، لكنه متجذر في نظام النوع المقدم هنا.

السلوك الافتراضي في جوليا عند حذف الأنواع هو السماح للقيم بأن تكون من أي نوع. وبالتالي، يمكن للمرء كتابة العديد من دوال جوليا المفيدة دون الحاجة إلى استخدام الأنواع بشكل صريح. ومع ذلك، عندما تكون هناك حاجة إلى مزيد من التعبير، فإنه من السهل إدخال تعليقات نوعية صريحة تدريجياً في الشيفرة "غير المخصصة" سابقاً. تخدم إضافة التعليقات ثلاثة أغراض رئيسية: الاستفادة من آلية التوزيع المتعدد القوية في جوليا، تحسين قابلية القراءة البشرية، والتقاط أخطاء المبرمجين.

وصف جوليا بلغة type systems هو: ديناميكية، اسمية ومعاملية. يمكن أن تكون الأنواع العامة مُعلمة، والعلاقات الهرمية بين الأنواع هي explicitly declared، بدلاً من implied by compatible structure. إحدى الميزات المميزة بشكل خاص في نظام أنواع جوليا هي أن الأنواع الملموسة قد لا تكون من نوع فرعي لبعضها البعض: جميع الأنواع الملموسة نهائية ولا يمكن أن يكون لها سوى أنواع مجردة كأنواع علوية. بينما قد يبدو هذا في البداية مقيدًا بشكل مفرط، إلا أن له العديد من العواقب المفيدة مع عدد قليل من العيوب بشكل مدهش. يتضح أن القدرة على وراثة السلوك أكثر أهمية بكثير من القدرة على وراثة الهيكل، وأن وراثة كلاهما تسبب صعوبات كبيرة في لغات البرمجة الكائنية التقليدية. الجوانب الأخرى عالية المستوى في نظام أنواع جوليا التي يجب ذكرها من البداية هي:

  • لا يوجد تقسيم بين القيم الكائنية وغير الكائنية: جميع القيم في جوليا هي كائنات حقيقية لها نوع ينتمي إلى رسم بياني نوعي واحد متصل بالكامل، حيث تكون جميع العقد فيه من الدرجة الأولى كأنواع.
  • لا يوجد مفهوم ذو معنى لـ "نوع وقت الترجمة": النوع الوحيد الذي يمتلكه القيمة هو نوعها الفعلي عندما يكون البرنامج قيد التشغيل. يُطلق على هذا اسم "نوع وقت التشغيل" في لغات البرمجة الكائنية حيث يجعل الجمع بين الترجمة الثابتة مع تعدد الأشكال هذا التمييز مهمًا.
  • فقط القيم، وليس المتغيرات، لها أنواع - المتغيرات هي ببساطة أسماء مرتبطة بالقيم، على الرغم من أنه من البساطة أن نقول "نوع المتغير" كاختصار لـ "نوع القيمة التي يشير إليها المتغير".
  • يمكن أن تكون الأنواع المجردة والملموسة معلمة بأنواع أخرى. يمكن أيضًا أن تكون معلمة بالرموز، وبقيم من أي نوع تعيد isbits القيمة true (بشكل أساسي، أشياء مثل الأرقام والقيم المنطقية المخزنة مثل أنواع C أو structs بدون مؤشرات إلى كائنات أخرى)، وأيضًا بالثنائيات منها. يمكن حذف معلمات النوع عندما لا تحتاج إلى الإشارة إليها أو تقييدها.

نظام أنواع جوليا مصمم ليكون قويًا ومعبرًا، ومع ذلك واضحًا وبديهيًا وغير متطفل. قد لا يشعر العديد من مبرمجي جوليا أبدًا بالحاجة إلى كتابة كود يستخدم الأنواع بشكل صريح. ومع ذلك، تصبح بعض أنواع البرمجة أكثر وضوحًا وبساطة وسرعة وموثوقية مع الأنواع المعلنة.

Type Declarations

يمكن استخدام العامل :: لإرفاق تعليقات توضيحية للنوع بالتعبيرات والمتغيرات في البرامج. هناك سببان رئيسيان للقيام بذلك:

  1. كإثبات للمساعدة في تأكيد أن برنامجك يعمل بالطريقة التي تتوقعها، و
  2. لتوفير معلومات نوع إضافية للمجمع، والتي يمكن أن تحسن الأداء في بعض الحالات.

عند إضافته إلى تعبير يحسب قيمة، يتم قراءة عامل :: على أنه "هو مثيل لـ". يمكن استخدامه في أي مكان لتأكيد أن قيمة التعبير على اليسار هي مثيل لنوع على اليمين. عندما يكون النوع على اليمين ملموسًا، يجب أن تكون القيمة على اليسار لها هذا النوع كتطبيق – تذكر أن جميع الأنواع الملموسة نهائية، لذا لا يكون أي تطبيق من نوع فرعي لأي نوع آخر. عندما يكون النوع تجريديًا، يكفي أن يتم تنفيذ القيمة بواسطة نوع ملموس يكون من نوع فرعي من النوع التجريدي. إذا لم يكن تأكيد النوع صحيحًا، يتم طرح استثناء، وإلا يتم إرجاع القيمة الموجودة على اليسار:

julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64

julia> (1+2)::Int
3

هذا يسمح بإرفاق تأكيد نوع بأي تعبير في المكان.

عند إضافته إلى متغير على الجانب الأيسر من تعبير التعيين، أو كجزء من إعلان local، فإن عامل التشغيل :: يعني شيئًا مختلفًا قليلاً: إنه يعلن عن المتغير ليكون له دائمًا النوع المحدد، مثل إعلان النوع في لغة ذات نوع ثابت مثل C. سيتم تحويل كل قيمة يتم تعيينها للمتغير إلى النوع المعلن باستخدام convert:

julia> function foo()
           x::Int8 = 100
           x
       end
foo (generic function with 1 method)

julia> x = foo()
100

julia> typeof(x)
Int8

تُعتبر هذه الميزة مفيدة لتجنب "المفاجآت" المتعلقة بالأداء التي قد تحدث إذا تم تغيير نوع أحد التعيينات لمتغير بشكل غير متوقع.

هذا السلوك "الإعلاني" يحدث فقط في سياقات محددة:

local x::Int8  # in a local declaration
x::Int8 = 10   # as the left-hand side of an assignment

وينطبق على النطاق الحالي بالكامل، حتى قبل الإعلان.

اعتبارًا من Julia 1.8، يمكن الآن استخدام إعلانات الأنواع في النطاق العالمي، أي يمكن إضافة تعليقات توضيحية للنوع إلى المتغيرات العالمية لجعل الوصول إليها ثابتًا من حيث النوع.

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)

يمكن أيضًا إرفاق التصريحات بتعريفات الدوال:

function sinc(x)::Float64
    if x == 0
        return 1
    end
    return sin(pi*x)/(pi*x)
end

يعمل العودة من هذه الدالة تمامًا مثل تعيين قيمة لمتغير مع نوع مُعلن: يتم دائمًا تحويل القيمة إلى Float64.

Abstract Types

لا يمكن إنشاء أنواع مجردة، وتعمل فقط كنقاط في رسم النوع، وبالتالي تصف مجموعات من الأنواع الملموسة ذات الصلة: تلك الأنواع الملموسة التي هي من نسلها. نبدأ بالأنواع المجردة على الرغم من عدم وجود إنشاء لها لأنها العمود الفقري لنظام النوع: فهي تشكل التسلسل الهرمي المفاهيمي الذي يجعل نظام نوع جوليا أكثر من مجرد مجموعة من تنفيذات الكائنات.

تذكر أنه في Integers and Floating-Point Numbers، قدمنا مجموعة متنوعة من الأنواع الملموسة للقيم العددية: Int8، UInt8، Int16، UInt16، Int32، UInt32، Int64، UInt64، Int128، UInt128، Float16، Float32، و Float64. على الرغم من أن لديها أحجام تمثيل مختلفة، إلا أن Int8 و Int16 و Int32 و Int64 و Int128 تشترك جميعها في كونها أنواع أعداد صحيحة موقعة. وبالمثل، فإن UInt8 و UInt16 و UInt32 و UInt64 و UInt128 هي جميعها أنواع أعداد صحيحة غير موقعة، بينما Float16 و Float32 و Float64 متميزة كونها أنواع أعداد عائمة بدلاً من الأعداد الصحيحة. من الشائع أن يكون لقطعة من الشيفرة معنى، على سبيل المثال، فقط إذا كانت وسائطها نوعًا ما من الأعداد الصحيحة، ولكن لا تعتمد حقًا على أي نوع معين من الأعداد الصحيحة. على سبيل المثال، يعمل خوارزمية القاسم المشترك الأكبر لجميع أنواع الأعداد الصحيحة، لكنها لن تعمل مع الأعداد العائمة. تسمح الأنواع المجردة بإنشاء تسلسل هرمي من الأنواع، مما يوفر سياقًا يمكن أن تناسبه الأنواع الملموسة. وهذا يسمح لك، على سبيل المثال، بالبرمجة بسهولة لأي نوع هو عدد صحيح، دون تقييد خوارزمية بنوع معين من الأعداد الصحيحة.

تُعلن الأنواع المجردة باستخدام الكلمة الرئيسية abstract type. الصيغ العامة لإعلان نوع مجرد هي:

abstract type «name» end
abstract type «name» <: «supertype» end

تقدم الكلمة المفتاحية abstract type نوعًا مجردًا جديدًا، يُعطى اسمه بـ «name». يمكن أن يتبع هذا الاسم اختياريًا <: ونوع موجود بالفعل، مما يشير إلى أن النوع المجرد المعلن عنه حديثًا هو نوع فرعي من هذا النوع "الأب".

عندما لا يتم إعطاء نوع أعلى، فإن النوع الأعلى الافتراضي هو Any - نوع مجرد محدد مسبقًا تكون جميع الكائنات من مثيلاته وجميع الأنواع من الأنواع العليا له. في نظرية الأنواع، يُطلق على Any عادةً "الأعلى" لأنه في قمة رسم النوع. تمتلك جوليا أيضًا نوعًا مجردًا محددًا مسبقًا يسمى "الأدنى"، في قاع رسم النوع، والذي يُكتب كـ Union{}. إنه العكس تمامًا لـ Any: لا يوجد كائن هو مثيل لـ Union{} وجميع الأنواع هي أنواع عليا لـ Union{}.

دعونا نعتبر بعض الأنواع المجردة التي تشكل تسلسل جولياء العددي:

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 هو نوع طفل مباشر من Any، و Real هو طفله. بدوره، يحتوي Real على طفلين (لديه المزيد، ولكن يتم عرض طفلين فقط هنا؛ سنتناول الآخرين لاحقًا): Integer و AbstractFloat، مما يفصل العالم إلى تمثيلات للأعداد الصحيحة وتمثيلات للأعداد الحقيقية. تشمل تمثيلات الأعداد الحقيقية أنواع النقاط العائمة، ولكنها تشمل أيضًا أنواعًا أخرى، مثل الكسور. يتضمن AbstractFloat فقط تمثيلات النقاط العائمة للأعداد الحقيقية. يتم تقسيم الأعداد الصحيحة إلى أنواع Signed و Unsigned.

يعني العامل <: بشكل عام "هو نوع فرعي من"، وعند استخدامه في التصريحات مثل تلك المذكورة أعلاه، يعلن عن نوع اليد اليمنى ليكون نوعًا فائقًا مباشرًا لنوع التصريح الجديد. يمكن أيضًا استخدامه في التعبيرات كعامل نوع فرعي يعيد true عندما يكون العامل الأيسر نوعًا فرعيًا من العامل الأيمن:

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

استخدام مهم للأنواع المجردة هو توفير تنفيذات افتراضية للأنواع الملموسة. لإعطاء مثال بسيط، اعتبر:

function myplus(x,y)
    x+y
end

الشيء الأول الذي يجب ملاحظته هو أن إعلانات المعاملات أعلاه تعادل x::Any و y::Any. عندما يتم استدعاء هذه الدالة، على سبيل المثال كـ myplus(2,5)، يختار الموزع الطريقة الأكثر تحديدًا المسماة myplus التي تتطابق مع المعاملات المعطاة. (انظر Methods لمزيد من المعلومات حول التوزيع المتعدد.)

بافتراض عدم العثور على طريقة أكثر تحديدًا من المذكورة أعلاه، تقوم جوليا بعد ذلك بتعريف وتجميع طريقة تُسمى myplus خصيصًا لوسيطين من نوع Int استنادًا إلى الدالة العامة المذكورة أعلاه، أي أنها تعرف وتجمع ضمنيًا:

function myplus(x::Int,y::Int)
    x+y
end

وأخيرًا، يستدعي هذه الطريقة المحددة.

وبالتالي، تتيح الأنواع المجردة للمبرمجين كتابة دوال عامة يمكن استخدامها لاحقًا كطريقة افتراضية من قبل العديد من تركيبات الأنواع الملموسة. بفضل التوزيع المتعدد، يمتلك المبرمج السيطرة الكاملة على ما إذا كانت الطريقة الافتراضية أو الطريقة الأكثر تحديدًا هي المستخدمة.

من النقاط المهمة التي يجب ملاحظتها هي أنه لا يوجد فقدان في الأداء إذا اعتمد المبرمج على دالة تكون وسائطها أنواعًا مجردة، لأنها تُعاد ترجمتها لكل مجموعة من أنواع الوسائط الملموسة التي يتم استدعاؤها بها. (قد تكون هناك مشكلة في الأداء، مع ذلك، في حالة وسائط الدالة التي هي حاويات لأنواع مجردة؛ انظر Performance Tips.)

Primitive Types

Warning

من الأفضل دائمًا تقريبًا لفّ نوع بدائي موجود في نوع مركب جديد بدلاً من تعريف نوع بدائي خاص بك.

توجد هذه الوظيفة للسماح لجوليا بتمهيد الأنواع الأولية القياسية التي تدعمها LLVM. بمجرد تعريفها، لا يوجد سبب كبير لتعريف المزيد.

نوع بدائي هو نوع ملموس تتكون بياناته من بتات قديمة بسيطة. من الأمثلة الكلاسيكية على الأنواع البدائية الأعداد الصحيحة والقيم العائمة. على عكس معظم اللغات، يسمح لك جوليا بإعلان أنواعك البدائية الخاصة، بدلاً من توفير مجموعة ثابتة فقط من الأنواع المدمجة. في الواقع، يتم تعريف جميع الأنواع البدائية القياسية في اللغة نفسها:

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

الصيغ العامة لإعلان نوع بدائي هي:

primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end

عدد البتات يشير إلى مقدار التخزين الذي يتطلبه النوع والاسم يمنح النوع الجديد اسمًا. يمكن إعلان نوع بدائي ليكون اختيارياً كنوع فرعي من نوع أعلى. إذا تم حذف نوع أعلى، فإن النوع يت default إلى أن يكون له Any كنوعه الأعلى المباشر. لذلك، فإن إعلان Bool أعلاه يعني أن قيمة بوليانية تتطلب ثمانية بتات للتخزين، ولها Integer كنوعها الأعلى المباشر. حالياً، يتم دعم الأحجام التي هي مضاعفات لـ 8 بتات فقط ومن المحتمل أن تواجه أخطاء LLVM مع أحجام غير تلك المستخدمة أعلاه. لذلك، لا يمكن إعلان القيم البوليانية، على الرغم من أنها تحتاج حقًا فقط إلى بت واحد، أن تكون أصغر من ثمانية بتات.

الأنواع Bool، Int8 و UInt8 جميعها لها تمثيلات متطابقة: فهي قطع من الذاكرة بحجم ثمانية بت. ومع ذلك، نظرًا لأن نظام أنواع جوليا هو اسمي، فإنها ليست قابلة للتبادل على الرغم من أن لها هيكل متطابق. الفرق الأساسي بينها هو أن لها أنواع علوية مختلفة: النوع المباشر لـ 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566 هو Integer، والنوع العلوي لـ 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566 هو Signed، والنوع العلوي لـ 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566 هو Unsigned. جميع الاختلافات الأخرى بين 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566، 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566 و 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566 هي مسائل سلوكية - الطريقة التي يتم بها تعريف الوظائف للتصرف عند إعطائها كائنات من هذه الأنواع كوسائط. لهذا السبب، فإن نظام النوع الاسمي ضروري: إذا كان الهيكل يحدد النوع، الذي بدوره يحدد السلوك، فسيكون من المستحيل جعل 4d61726b646f776e2e436f64652822222c2022426f6f6c2229_40726566 يتصرف بشكل مختلف عن 4d61726b646f776e2e436f64652822222c2022496e74382229_40726566 أو 4d61726b646f776e2e436f64652822222c202255496e74382229_40726566.

Composite Types

Composite types تُسمى سجلات، هياكل، أو كائنات في لغات مختلفة. النوع المركب هو مجموعة من الحقول المسماة، يمكن التعامل مع مثيل منها كقيمة واحدة. في العديد من اللغات، الأنواع المركبة هي النوع الوحيد القابل للتعريف من قبل المستخدم، وهي بلا شك النوع الأكثر استخدامًا من قبل المستخدمين في جوليا أيضًا.

في لغات البرمجة الكائنية الرئيسية، مثل C++ وJava وPython وRuby، تحتوي الأنواع المركبة أيضًا على وظائف مسماة مرتبطة بها، ويطلق على هذا المزيج اسم "كائن". في اللغات الكائنية الأكثر نقاءً، مثل Ruby أو Smalltalk، تعتبر جميع القيم كائنات سواء كانت مركبة أم لا. في اللغات الكائنية الأقل نقاءً، بما في ذلك C++ وJava، بعض القيم، مثل الأعداد الصحيحة والقيم العائمة، ليست كائنات، بينما تعتبر مثيلات الأنواع المركبة المعرفة من قبل المستخدم كائنات حقيقية مع طرق مرتبطة بها. في Julia، جميع القيم هي كائنات، لكن الوظائف ليست مرتبطة بالكائنات التي تعمل عليها. هذا ضروري لأن Julia تختار أي طريقة من وظيفة لاستخدامها من خلال التوزيع المتعدد، مما يعني أن أنواع جميع معطيات الوظيفة تؤخذ بعين الاعتبار عند اختيار طريقة، بدلاً من مجرد الأولى (انظر Methods لمزيد من المعلومات حول الطرق والتوزيع). وبالتالي، سيكون من غير المناسب أن "تنتمي" الوظائف فقط إلى معطياتها الأولى. إن تنظيم الطرق في كائنات الوظائف بدلاً من وجود أكياس مسماة من الطرق "داخل" كل كائن ينتهي به الأمر ليكون جانبًا مفيدًا للغاية في تصميم اللغة.

تُقدَّم الأنواع المركبة باستخدام الكلمة الرئيسية struct متبوعةً بكتلة من أسماء الحقول، مع إمكانية إضافة تعليقات توضيحية للأنواع باستخدام عامل التشغيل :::

julia> struct Foo
           bar
           baz::Int
           qux::Float64
       end

تكون الحقول التي لا تحتوي على توضيح نوع افتراضيًا من نوع Any، وبالتالي يمكنها احتواء أي نوع من القيم.

تُنشأ كائنات جديدة من النوع Foo عن طريق تطبيق كائن النوع Foo كدالة على القيم الخاصة بحقولها:

julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)

julia> typeof(foo)
Foo

عندما يتم تطبيق نوع مثل دالة، يُطلق عليه اسم باني. يتم إنشاء بانيين تلقائيًا (يُطلق عليهما البانيان الافتراضيان). أحدهما يقبل أي معطيات ويستدعي convert لتحويلها إلى أنواع الحقول، والآخر يقبل المعطيات التي تتطابق تمامًا مع أنواع الحقول. السبب في إنشاء كلاهما هو أن ذلك يسهل إضافة تعريفات جديدة دون استبدال باني افتراضي عن غير قصد.

نظرًا لأن حقل bar غير مقيد في النوع، فإن أي قيمة ستفي بالغرض. ومع ذلك، يجب أن تكون القيمة لـ baz قابلة للتحويل إلى Int:

julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(23.5)
Stacktrace:
[...]

يمكنك العثور على قائمة بأسماء الحقول باستخدام دالة fieldnames.

julia> fieldnames(Foo)
(:bar, :baz, :qux)

يمكنك الوصول إلى قيم الحقول لكائن مركب باستخدام صيغة foo.bar التقليدية:

julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

تم الإعلان عن الكائنات المركبة باستخدام struct بأنها غير قابلة للتغيير؛ لا يمكن تعديلها بعد الإنشاء. قد يبدو هذا غريبًا في البداية، لكنه يحمل عدة مزايا:

  • يمكن أن يكون أكثر كفاءة. يمكن تعبئة بعض الهياكل بكفاءة في المصفوفات، وفي بعض الحالات يكون المترجم قادرًا على تجنب تخصيص الكائنات الثابتة تمامًا.
  • لا يمكن انتهاك الثوابت التي توفرها منشئات النوع.
  • يمكن أن يكون الكود الذي يستخدم كائنات غير قابلة للتغيير أسهل في التفكير فيه.

قد يحتوي الكائن غير القابل للتغيير على كائنات قابلة للتغيير، مثل المصفوفات، كحقول. ستظل تلك الكائنات المحتواة قابلة للتغيير؛ فقط حقول الكائن غير القابل للتغيير نفسها لا يمكن تغييرها للإشارة إلى كائنات مختلفة.

حيثما كان ذلك مطلوبًا، يمكن إعلان الكائنات المركبة القابلة للتغيير باستخدام الكلمة الرئيسية mutable struct، والتي ستتم مناقشتها في القسم التالي.

إذا كانت جميع حقول هيكل غير قابل للتغيير غير مميزة (===)، فإن قيمتين غير قابلتين للتغيير تحتويان على تلك الحقول تكون أيضًا غير مميزة:

julia> struct X
           a::Int
           b::Float64
       end

julia> X(1, 2) === X(1, 2)
true

هناك الكثير ليقال عن كيفية إنشاء مثيلات من الأنواع المركبة، لكن تلك المناقشة تعتمد على كل من Parametric Types و Methods، وهي مهمة بما يكفي لتكون موضوعًا في قسم خاص بها: Constructors.

لأنواع المستخدم المعرفة X، قد ترغب في تعريف طريقة Base.broadcastable(x::X) = Ref(x) بحيث تعمل مثيلات هذا النوع كـ "سكالار" بعدد أبعاد 0 لـ broadcasting.

Mutable Composite Types

إذا تم إعلان نوع مركب باستخدام mutable struct بدلاً من struct، فيمكن تعديل مثيلاته:

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

يمكن توفير واجهة إضافية بين الحقول والمستخدم من خلال Instance Properties. وهذا يمنح مزيدًا من التحكم فيما يمكن الوصول إليه وتعديله باستخدام صيغة bar.baz.

من أجل دعم التغيير، يتم عادةً تخصيص مثل هذه الكائنات في الذاكرة الديناميكية، ولها عناوين ذاكرة مستقرة. الكائن القابل للتغيير يشبه حاوية صغيرة قد تحتوي على قيم مختلفة مع مرور الوقت، وبالتالي يمكن التعرف عليه بشكل موثوق من خلال عنوانه فقط. بالمقابل، فإن مثيل نوع غير قابل للتغيير مرتبط بقيم حقول محددة - قيم الحقول وحدها تخبرك بكل شيء عن الكائن. عند اتخاذ قرار بشأن جعل نوع ما قابلاً للتغيير، اسأل ما إذا كان سيتم اعتبار مثيلين لهما نفس قيم الحقول متطابقين، أو إذا كان من الممكن أن يحتاجا إلى التغيير بشكل مستقل مع مرور الوقت. إذا كان سيتم اعتبارهما متطابقين، فمن المحتمل أن يكون النوع غير قابل للتغيير.

لتلخيص، خاصيتان أساسيتان تحددان الثبات في جوليا:

  • لا يُسمح بتعديل قيمة نوع غير قابل للتغيير.

    • بالنسبة لأنواع البتات، فهذا يعني أن نمط البت لقيمة معينة بمجرد تعيينه لن يتغير أبدًا وأن تلك القيمة هي هوية نوع البتات.
    • بالنسبة للأنواع المركبة، فهذا يعني أن هوية قيم حقولها لن تتغير أبدًا. عندما تكون الحقول من أنواع بت، فهذا يعني أن بتاتها لن تتغير، أما بالنسبة للحقول التي تكون قيمها من أنواع قابلة للتغيير مثل المصفوفات، فهذا يعني أن الحقول ستشير دائمًا إلى نفس القيمة القابلة للتغيير على الرغم من أن محتوى تلك القيمة القابلة للتغيير قد يتم تعديله.
  • يمكن نسخ كائن من نوع غير قابل للتغيير بحرية بواسطة المترجم نظرًا لأن عدم قابليته للتغيير يجعل من المستحيل تمييزه برمجيًا بين الكائن الأصلي ونسخة منه.

    • بشكل خاص، يعني هذا أن القيم الثابتة الصغيرة مثل الأعداد الصحيحة والأعداد العشرية تُمرر عادةً إلى الدوال في السجلات (أو مخصصة في المكدس).
    • القيم القابلة للتغيير، من ناحية أخرى، يتم تخصيصها في الذاكرة الديناميكية ويتم تمريرها إلى الدوال كمؤشرات إلى القيم المخصصة في الذاكرة الديناميكية، باستثناء الحالات التي يكون فيها المترجم متأكدًا من أنه لا يوجد طريقة لمعرفة أن هذا ليس ما يحدث.

في الحالات التي يُعرف فيها أن حقلًا أو أكثر من هيكل قابل للتغيير بخلاف ذلك غير قابل للتغيير، يمكن للمرء أن يعلن عن هذه الحقول على أنها كذلك باستخدام const كما هو موضح أدناه. هذا يمكّن بعض، ولكن ليس كل، تحسينات الهياكل غير القابلة للتغيير، ويمكن استخدامه لفرض الثوابت على الحقول المحددة التي تم وضع علامة عليها كـ const.

Julia 1.8

const annotating fields of mutable structs يتطلب على الأقل Julia 1.8.

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

الأنواع الثلاثة (المجردة، البدائية، المركبة) التي تم مناقشتها في الأقسام السابقة مرتبطة ببعضها البعض بشكل وثيق. تشترك في نفس الخصائص الرئيسية:

  • تم الإعلان عنها بشكل صريح.
  • لديهم أسماء.
  • لقد أعلنوا صراحة عن الأنواع العليا.
  • قد تحتوي على معلمات.

بسبب هذه الخصائص المشتركة، يتم تمثيل هذه الأنواع داخليًا كحالات من نفس المفهوم، DataType، وهو نوع أي من هذه الأنواع:

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

يمكن أن يكون DataType مجردًا أو ملموسًا. إذا كان ملموسًا، فإنه يحتوي على حجم محدد، وتخطيط تخزين، و(اختياريًا) أسماء الحقول. وبالتالي، فإن النوع البدائي هو DataType بحجم غير صفري، ولكن بدون أسماء حقول. النوع المركب هو DataType يحتوي على أسماء حقول أو يكون فارغًا (حجم صفر).

كل قيمة ملموسة في النظام هي مثيل لنوع بيانات معين DataType.

Type Unions

نوع الاتحاد هو نوع مجرد خاص يتضمن ككائنات جميع حالات أي من أنواع حججه، التي تم إنشاؤها باستخدام الكلمة الرئيسية الخاصة Union:

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

تحتوي المترجمات للعديد من اللغات على بنية اتحاد داخلية للتفكير في الأنواع؛ جوليّا ببساطة تعرضها للمبرمج. يتمكن مترجم جوليّا من توليد كود فعال في وجود أنواع Union مع عدد قليل من الأنواع [1]، من خلال توليد كود متخصص في فروع منفصلة لكل نوع ممكن.

حالة مفيدة بشكل خاص من نوع Union هي Union{T, Nothing}، حيث يمكن أن يكون T أي نوع و Nothing هو نوع الساكن الذي تكون مثاله الوحيد هو الكائن nothing. هذه النمط هو المعادل في جوليا لـ Nullable, Option or Maybe الأنواع في لغات أخرى. يسمح إعلان حجة دالة أو حقل كـ Union{T, Nothing} بتعيينه إما إلى قيمة من نوع T، أو إلى nothing للإشارة إلى عدم وجود قيمة. انظر this FAQ entry لمزيد من المعلومات.

Parametric Types

ميزة مهمة وقوية في نظام أنواع جوليا هي أنها بارامترية: يمكن أن تأخذ الأنواع معلمات، بحيث أن إعلانات الأنواع تقدم في الواقع عائلة كاملة من الأنواع الجديدة - واحدة لكل مجموعة ممكنة من قيم المعلمات. هناك العديد من اللغات التي تدعم بعض النسخ من generic programming، حيث يمكن تحديد الهياكل البيانية والخوارزميات للتلاعب بها دون تحديد الأنواع الدقيقة المعنية. على سبيل المثال، توجد بعض أشكال البرمجة العامة في ML وHaskell وAda وEiffel وC++ وJava وC# وF# وScala، فقط لذكر القليل. تدعم بعض هذه اللغات تعدد الأشكال البارامتري الحقيقي (مثل ML وHaskell وScala)، بينما تدعم أخرى أنماط البرمجة العامة المعتمدة على القوالب (مثل C++ وJava). مع وجود العديد من الأنواع المختلفة من البرمجة العامة والأنواع البارامترية في لغات مختلفة، لن نحاول حتى مقارنة أنواع جوليا البارامترية بلغات أخرى، ولكننا سنركز بدلاً من ذلك على شرح نظام جوليا في حد ذاته. سنلاحظ، مع ذلك، أنه نظرًا لأن جوليا لغة ذات نوع ديناميكي ولا تحتاج إلى اتخاذ جميع قرارات النوع في وقت الترجمة، يمكن التعامل مع العديد من الصعوبات التقليدية التي تواجه أنظمة الأنواع البارامترية الثابتة بسهولة نسبية.

يمكن تهيئة جميع الأنواع المعلنة (من نوع DataType) باستخدام نفس الصيغة في كل حالة. سنناقشها بالترتيب التالي: أولاً، الأنواع المركبة المعاملات، ثم الأنواع المجردة المعاملات، وأخيراً الأنواع الأولية المعاملات.

Parametric Composite Types

تُقدَّم معلمات النوع مباشرةً بعد اسم النوع، محاطةً بأقواس معقوفة:

julia> struct Point{T}
           x::T
           y::T
       end

هذا الإعلان يحدد نوعًا بارامترًا جديدًا، Point{T}، يحمل "إحداثيين" من النوع T. ما هو T، قد يسأل المرء؟ حسنًا، هذه هي بالضبط نقطة الأنواع البارامترية: يمكن أن يكون أي نوع على الإطلاق (أو قيمة من أي نوع بت، في الواقع، على الرغم من أنه هنا يُستخدم بوضوح كنوع). Point{Float64} هو نوع ملموس يعادل النوع المحدد عن طريق استبدال T في تعريف Point بـ Float64. وبالتالي، هذا الإعلان الواحد يعلن في الواقع عن عدد غير محدود من الأنواع: Point{Float64}، Point{AbstractString}، Point{Int64}، إلخ. كل من هذه الأنواع الآن نوع ملموس قابل للاستخدام:

julia> Point{Float64}
Point{Float64}

julia> Point{AbstractString}
Point{AbstractString}

النوع Point{Float64} هو نقطة تكون إحداثياتها قيم عائمة بدقة 64 بت، بينما النوع Point{AbstractString} هو "نقطة" تكون "إحداثياتها" كائنات نصية (انظر Strings).

Point نفسه هو أيضًا كائن نوع صالح، يحتوي على جميع الحالات Point{Float64}، Point{AbstractString}، إلخ كأنواع فرعية:

julia> Point{Float64} <: Point
true

julia> Point{AbstractString} <: Point
true

أنواع أخرى، بالطبع، ليست تحت أنواع منه:

julia> Float64 <: Point
false

julia> AbstractString <: Point
false

أنواع Point الخرسانية بقيم مختلفة من T لا تكون أبدًا أنواع فرعية من بعضها البعض:

julia> Point{Float64} <: Point{Int64}
false

julia> Point{Float64} <: Point{Real}
false
Warning

هذه النقطة الأخيرة مهمة جداً: على الرغم من أن Float64 <: Real إلا أننا لا نملك Point{Float64} <: Point{Real}.

بعبارة أخرى، في مصطلحات نظرية الأنواع، معلمات النوع في جوليا هي ثابتة، بدلاً من أن تكون covariant (or even contravariant). هذا لأسباب عملية: بينما قد يكون أي مثيل من Point{Float64} مفهوميًا مثل مثيل من Point{Real} أيضًا، فإن النوعين لهما تمثيلات مختلفة في الذاكرة:

  • يمكن تمثيل مثيل من Point{Float64} بشكل مضغوط وفعال كزوج فوري من قيمتين 64 بت؛
  • يجب أن تكون مثيل لـ Point{Real} قادرًا على الاحتفاظ بأي زوج من مثيلات Real. نظرًا لأن الكائنات التي هي مثيلات لـ Real يمكن أن تكون بحجم وبنية عشوائية، في الممارسة العملية يجب تمثيل مثيل لـ Point{Real} كزوج من المؤشرات إلى كائنات Real المخصصة بشكل فردي.

الكفاءة المكتسبة من القدرة على تخزين Point{Float64} كائنات بالقيم الفورية تتضخم بشكل هائل في حالة المصفوفات: يمكن تخزين Array{Float64} ككتلة ذاكرة متجاورة من قيم النقاط العائمة 64 بت، في حين يجب أن تكون Array{Real} مصفوفة من المؤشرات إلى Real كائنات تم تخصيصها بشكل فردي – والتي قد تكون أيضًا boxed قيم عائمة 64 بت، ولكن قد تكون أيضًا كائنات كبيرة ومعقدة بشكل تعسفي، والتي تم الإعلان عنها كتنفيذات لنوع Real المجرد.

نظرًا لأن Point{Float64} ليس نوعًا فرعيًا من Point{Real}، لا يمكن تطبيق الطريقة التالية على المعاملات من نوع Point{Float64}:

function norm(p::Point{Real})
    sqrt(p.x^2 + p.y^2)
end

طريقة صحيحة لتعريف دالة تقبل جميع المعاملات من نوع Point{T} حيث T هو نوع فرعي من Real هي:

function norm(p::Point{<:Real})
    sqrt(p.x^2 + p.y^2)
end

(بالمثل، يمكن تعريف function norm(p::Point{T} where T<:Real) أو function norm(p::Point{T}) where T<:Real؛ انظر UnionAll Types.)

سيتم مناقشة المزيد من الأمثلة لاحقًا في Methods.

كيف يمكن للمرء إنشاء كائن Point؟ من الممكن تعريف منشئات مخصصة للأنواع المركبة، والتي ستتم مناقشتها بالتفصيل في Constructors، ولكن في غياب أي إعلانات خاصة للمنشئ، هناك طريقتان افتراضيتان لإنشاء كائنات مركبة جديدة، واحدة يتم فيها إعطاء معلمات النوع بشكل صريح والأخرى يتم فيها استنتاجها من الوسائط إلى منشئ الكائن.

نظرًا لأن النوع Point{Float64} هو نوع ملموس يعادل Point المعلن عنه باستخدام Float64 بدلاً من T، يمكن استخدامه كمنشئ وفقًا لذلك:

julia> p = Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(p)
Point{Float64}

للباني الافتراضي، يجب توفير حجة واحدة بالضبط لكل حقل:

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.
[...]

يتم إنشاء مُنشئ افتراضي واحد فقط لأنواع المعلمات، حيث أنه لا يمكن تجاوز هذا المُنشئ. يقبل هذا المُنشئ أي معلمات ويحولها إلى أنواع الحقول.

في العديد من الحالات، يكون من الزائد تقديم نوع كائن Point الذي ترغب في إنشائه، حيث أن أنواع المعاملات في استدعاء المُنشئ توفر بالفعل معلومات نوعية ضمنية. لهذا السبب، يمكنك أيضًا استخدام Point نفسه كمُنشئ، بشرط أن تكون القيمة الضمنية لنوع المعامل T غير غامضة:

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، يتم استنتاج نوع T بشكل غير غامض إذا وفقط إذا كانت الحجتان لـ Point من نفس النوع. عندما لا يكون هذا هو الحال، سيفشل المُنشئ مع MethodError:

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:
[...]

يمكن تعريف طرق المُنشئ للتعامل بشكل مناسب مع مثل هذه الحالات المختلطة، ولكن لن يتم مناقشة ذلك حتى وقت لاحق في Constructors.

Parametric Abstract Types

إعلانات نوع تجريدي بارامتري تعلن عن مجموعة من الأنواع التجريدية، بنفس الطريقة:

julia> abstract type Pointy{T} end

مع هذا الإعلان، Pointy{T} هو نوع مجرد متميز لكل نوع أو قيمة صحيحة لـ T. كما هو الحال مع الأنواع المركبة المعاملية، فإن كل حالة من هذه الحالات هي نوع فرعي من Pointy:

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

أنواع المعلمات المجردة غير متغيرة، تمامًا كما هي أنواع المركبات المعلمات:

julia> Pointy{Float64} <: Pointy{Real}
false

julia> Pointy{Real} <: Pointy{Float64}
false

يمكن استخدام التدوين Pointy{<:Real} للتعبير عن النظير الجولي للنوع التغاير، بينما Pointy{>:Int} هو النظير لنوع التعاكس، ولكن تقنيًا تمثل هذه الأنواع مجموعات من الأنواع (انظر UnionAll Types).

julia> Pointy{Float64} <: Pointy{<:Real}
true

julia> Pointy{Real} <: Pointy{>:Int}
true

تمامًا كما أن الأنواع المجردة التقليدية تخدم لإنشاء تسلسل هرمي مفيد من الأنواع فوق الأنواع الملموسة، فإن الأنواع المجردة المعاملاتية تخدم نفس الغرض فيما يتعلق بالأنواع المركبة المعاملاتية. يمكننا، على سبيل المثال، أن نعلن أن Point{T} هو نوع فرعي من Pointy{T} كما يلي:

julia> struct Point{T} <: Pointy{T}
           x::T
           y::T
       end

نظرًا لمثل هذا الإعلان، لكل اختيار لـ T، لدينا Point{T} كنوع فرعي من Pointy{T}:

julia> Point{Float64} <: Pointy{Float64}
true

julia> Point{Real} <: Pointy{Real}
true

julia> Point{AbstractString} <: Pointy{AbstractString}
true

هذه العلاقة أيضًا غير متغيرة:

julia> Point{Float64} <: Pointy{Real}
false

julia> Point{Float64} <: Pointy{<:Real}
true

ما الغرض من الأنواع التجريدية المعاملية مثل Pointy؟ اعتبر إذا قمنا بإنشاء تنفيذ يشبه النقطة يتطلب فقط إحداثي واحد لأن النقطة تقع على الخط القطري x = y:

julia> struct DiagPoint{T} <: Pointy{T}
           x::T
       end

الآن كل من Point{Float64} و DiagPoint{Float64} هما تنفيذات للواجهة Pointy{Float64}، وبالمثل لكل خيار آخر ممكن من النوع T. هذا يسمح بالبرمجة على واجهة مشتركة تتشاركها جميع كائنات Pointy، المنفذة لكل من Point و DiagPoint. ومع ذلك، لا يمكن إثبات ذلك بالكامل حتى نقدم الطرق والتوزيع في القسم التالي، Methods.

هناك حالات قد لا يكون من المنطقي أن تتراوح معلمات النوع بحرية على جميع الأنواع الممكنة. في مثل هذه الحالات، يمكن للمرء تقييد نطاق T على النحو التالي:

julia> abstract type Pointy{T<:Real} end

مع مثل هذا الإعلان، من المقبول استخدام أي نوع هو نوع فرعي من Real بدلاً من T، ولكن ليس الأنواع التي ليست أنواع فرعية من Real:

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

يمكن تقييد معلمات النوع لأنواع المركبات المعلمية بنفس الطريقة:

struct Point{T<:Real} <: Pointy{T}
    x::T
    y::T
end

لتقديم مثال من العالم الحقيقي حول كيفية فائدة كل هذه الآلات من نوع المعلمات، إليك التعريف الفعلي لنوع Rational غير القابل للتغيير في جوليا (باستثناء أننا نتجاهل المُنشئ هنا من أجل البساطة)، والذي يمثل نسبة دقيقة من الأعداد الصحيحة:

struct Rational{T<:Integer} <: Real
    num::T
    den::T
end

يقتصر نوع المعامل T على كونه نوعًا فرعيًا من Integer، ويمثل نسبة الأعداد الصحيحة قيمة على خط الأعداد الحقيقية، لذا فإن أي Rational هو مثال على تجريد Real.

Tuple Types

تعتبر التوائم تجسيدًا لوسائط الدالة - دون الدالة نفسها. الجوانب البارزة لوسائط الدالة هي ترتيبها وأنواعها. لذلك، فإن نوع التوائم مشابه لنوع غير قابل للتغيير مُعَلم حيث كل معلم هو نوع حقل واحد. على سبيل المثال، يشبه نوع التوائم المكون من عنصرين النوع غير القابل للتغيير التالي:

struct Tuple2{A,B}
    a::A
    b::B
end

ومع ذلك، هناك ثلاث اختلافات رئيسية:

  • يمكن أن تحتوي أنواع التوابل على أي عدد من المعلمات.
  • أنواع التوابل هي متغايرة في معلماتها: Tuple{Int} هو نوع فرعي من Tuple{Any}. لذلك، يعتبر Tuple{Any} نوعًا تجريديًا، وأنواع التوابل تكون ملموسة فقط إذا كانت معلماتها ملموسة.
  • لا تحتوي التوائم على أسماء الحقول؛ يتم الوصول إلى الحقول فقط بواسطة الفهرس.

تُكتب قيم التوابل باستخدام الأقواس والفواصل. عند إنشاء التوابل، يتم توليد نوع توابل مناسب عند الطلب:

julia> typeof((1,"foo",2.5))
Tuple{Int64, String, Float64}

لاحظ تداعيات التغاير:

julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true

julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false

julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false

بشكل بديهي، يتوافق هذا مع كون نوع معطيات الدالة هو نوع فرعي من توقيع الدالة (عندما يتطابق التوقيع).

Vararg Tuple Types

يمكن أن تكون المعلمة الأخيرة من نوع التوابل القيمة الخاصة Vararg، والتي تشير إلى أي عدد من العناصر المتبقية:

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

علاوة على ذلك، Vararg{T} يتوافق مع صفر أو أكثر من العناصر من النوع T. تُستخدم أنواع التوابل المتغيرة لتمثيل المعاملات المقبولة من قبل طرق المتغيرات (انظر Varargs Functions).

القيمة الخاصة Vararg{T,N} (عند استخدامها كآخر معلمة لنوع التوابل) تتوافق تمامًا مع N عنصر من النوع T. NTuple{N,T} هو اسم مستعار مريح لـ Tuple{Vararg{T,N}}، أي نوع توابل يحتوي بالضبط على N عنصر من النوع T.

Named Tuple Types

تعتبر التوائم المسماة مثيلات من نوع NamedTuple، والذي يحتوي على معلمين: مجموعة من الرموز التي تعطي أسماء الحقول، ونوع مجموعة يعطي أنواع الحقول. لسهولة الاستخدام، يتم طباعة أنواع NamedTuple باستخدام الماكرو @NamedTuple الذي يوفر بناء جملة مشابه لـ struct للإعلان عن هذه الأنواع عبر إعلانات key::Type، حيث يتوافق ::Type المهمل مع ::Any.

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}

تسمح صيغة begin ... end لماكرو @NamedTuple بتقسيم التصريحات عبر عدة أسطر (مشابهة لإعلان الهيكل)، لكنها تعادل ذلك بخلاف ذلك:

julia> @NamedTuple begin
           a::Int
           b::String
       end
@NamedTuple{a::Int64, b::String}

يمكن استخدام نوع NamedTuple كمنشئ، يقبل وسيطًا واحدًا من نوع tuple. يمكن أن يكون نوع NamedTuple المُنشأ إما نوعًا محددًا، مع تحديد كلا المعاملين، أو نوعًا يحدد أسماء الحقول فقط:

julia> @NamedTuple{a::Float32,b::String}((1, ""))
(a = 1.0f0, b = "")

julia> NamedTuple{(:a, :b)}((1, ""))
(a = 1, b = "")

إذا تم تحديد أنواع الحقول، يتم تحويل المعاملات. خلاف ذلك، يتم استخدام أنواع المعاملات مباشرة.

Parametric Primitive Types

يمكن أيضًا إعلان الأنواع البدائية بشكل برامترية. على سبيل المثال، يتم تمثيل المؤشرات كأنواع بدائية والتي سيتم إعلانها في جوليا على النحو التالي:

# 32-bit system:
primitive type Ptr{T} 32 end

# 64-bit system:
primitive type Ptr{T} 64 end

الميزة الغريبة قليلاً في هذه الإعلانات مقارنةً بأنواع المركبات المعلمية النموذجية، هي أن معلمة النوع T لا تُستخدم في تعريف النوع نفسه - إنها مجرد علامة مجردة، تحدد في الأساس عائلة كاملة من الأنواع ذات الهيكل المتطابق، والتي تختلف فقط من خلال معلمة النوع الخاصة بها. وبالتالي، فإن Ptr{Float64} و Ptr{Int64} هما نوعان متميزان، على الرغم من أن لهما تمثيلات متطابقة. وبالطبع، فإن جميع أنواع المؤشرات المحددة هي أنواع فرعية من النوع الشامل Ptr:

julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

UnionAll Types

لقد قلنا إن نوعًا بارامترًا مثل Ptr يعمل كنوع أعلى لجميع مثيلاته (Ptr{Int64} وما إلى ذلك). كيف يعمل هذا؟ لا يمكن أن يكون Ptr نفسه نوع بيانات عادي، لأنه بدون معرفة نوع البيانات المرجعية، لا يمكن استخدام النوع بوضوح لعمليات الذاكرة. الجواب هو أن Ptr (أو أنواع بارامترية أخرى مثل Array) هو نوع مختلف يسمى نوع UnionAll. يعبر هذا النوع عن الاتحاد المتكرر للأنواع لجميع قيم بعض المعامل.

UnionAll الأنواع عادة ما تُكتب باستخدام الكلمة الرئيسية where. على سبيل المثال، يمكن كتابة Ptr بدقة أكبر كـ Ptr{T} where T، مما يعني جميع القيم التي نوعها Ptr{T} لقيمة ما من T. في هذا السياق، يُطلق على المعامل T غالبًا اسم "متغير نوع" لأنه يشبه متغيرًا يتراوح عبر الأنواع. كل where تُدخل متغير نوع واحد، لذا فإن هذه التعبيرات متداخلة للأنواع ذات المعاملات المتعددة، على سبيل المثال Array{T,N} where N where T.

يتطلب بناء جملة تطبيق النوع A{B,C} أن يكون A نوع UnionAll، ويقوم أولاً باستبدال B بأكثر متغير نوع خارجي في A. من المتوقع أن تكون النتيجة نوع UnionAll آخر، يتم بعد ذلك استبدال C فيه. لذا فإن A{B,C} يعادل A{B}{C}. هذا يفسر لماذا من الممكن تهيئة نوع جزئي، كما في Array{Float64}: قيمة المعامل الأول قد تم تثبيتها، لكن الثاني لا يزال يتراوح بين جميع القيم الممكنة. باستخدام بناء جملة where الصريح، يمكن تثبيت أي مجموعة فرعية من المعاملات. على سبيل المثال، يمكن كتابة نوع جميع المصفوفات أحادية البعد كـ Array{T,1} where T.

يمكن تقييد متغيرات النوع بعلاقات النوع الفرعي. Array{T} where T<:Integer تشير إلى جميع المصفوفات التي نوع عناصرها هو نوع من Integer. الصيغة Array{<:Integer} هي اختصار مريح لـ Array{T} where T<:Integer. يمكن أن تحتوي متغيرات النوع على حدود دنيا وعليا. Array{T} where Int<:T<:Number تشير إلى جميع المصفوفات من Numbers التي يمكن أن تحتوي على Ints (حيث يجب أن يكون T على الأقل بحجم Int). تعمل الصيغة where T>:Int أيضًا لتحديد الحد الأدنى فقط لمتغير النوع، و Array{>:Int} تعادل Array{T} where T>:Int.

نظرًا لأن تعبيرات where تتداخل، يمكن أن تشير حدود متغيرات النوع إلى متغيرات النوع الخارجية. على سبيل المثال، Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real تشير إلى أزواج مكونة من عنصرين حيث العنصر الأول هو نوع Real، والعنصر الثاني هو Array من أي نوع من المصفوفات التي تحتوي على نوع عنصر يتضمن نوع العنصر الأول من الزوج.

يمكن أن تكون الكلمة الرئيسية where متداخلة داخل إعلان أكثر تعقيدًا. على سبيل المثال، اعتبر النوعين اللذين تم إنشاؤهما بواسطة الإعلانات التالية:

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 مصفوفة أحادية البعد من مصفوفات أحادية البعد؛ كل من المصفوفات الداخلية تتكون من كائنات من نفس النوع، ولكن هذا النوع قد يختلف من مصفوفة داخلية إلى أخرى. من ناحية أخرى، يعرّف النوع T2 مصفوفة أحادية البعد من مصفوفات أحادية البعد يجب أن تحتوي جميع مصفوفاتها الداخلية على نفس النوع. لاحظ أن T2 هو نوع مجرد، على سبيل المثال، Array{Array{Int,1},1} <: T2، في حين أن T1 هو نوع ملموس. ونتيجة لذلك، يمكن إنشاء T1 باستخدام مُنشئ بدون معلمات a=T1() ولكن لا يمكن ذلك مع T2.

هناك صياغة مريحة لتسمية مثل هذه الأنواع، مشابهة للصيغة القصيرة لتعريف الدوال:

Vector{T} = Array{T, 1}

هذا يعادل const Vector = Array{T,1} where T. كتابة Vector{Float64} تعادل كتابة Array{Float64,1}، ونوع المظلة Vector لديه كحالات جميع كائنات Array حيث المعامل الثاني - عدد أبعاد المصفوفة - هو 1، بغض النظر عن نوع العنصر. في اللغات التي يجب فيها دائمًا تحديد الأنواع البارامترية بالكامل، فإن هذا ليس مفيدًا بشكل خاص، ولكن في جوليا، يسمح لك هذا بكتابة Vector فقط لنوع مجرد يشمل جميع المصفوفات الكثيفة أحادية البعد من أي نوع عنصر.

Singleton types

تسمى الأنواع المركبة غير القابلة للتغيير التي لا تحتوي على حقول أحادية. بشكل رسمي، إذا

  1. T هو نوع مركب غير قابل للتغيير (أي معرف باستخدام struct
  2. a isa T && b isa T يعني a === b,

ثم T هو نوع فردي.[2] Base.issingletontype يمكن استخدامه للتحقق مما إذا كان النوع نوعًا فرديًا. Abstract types لا يمكن أن تكون أنواعًا فردية من حيث البناء.

من التعريف، يتبع أنه لا يمكن أن يكون هناك سوى مثيل واحد من هذه الأنواع:

julia> struct NoFields
       end

julia> NoFields() === NoFields()
true

julia> Base.issingletontype(NoFields)
true

تؤكد دالة === أن الكائنات المُنشأة من NoFields هي في الواقع واحدة ونفس الشيء.

يمكن أن تكون الأنواع البارامترية أنواعًا مفردة عندما يتحقق الشرط أعلاه. على سبيل المثال،

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

كل دالة لها نوعها الخاص، وهو نوع فرعي من Function.

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) يطبع كما هو. هذه مجرد قاعدة لطباعة، حيث إنه كائن من الدرجة الأولى يمكن استخدامه مثل أي قيمة أخرى:

julia> T = typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

julia> T <: Function
true

أنواع الدوال المعرفة على المستوى الأعلى هي كائنات مفردة. عند الضرورة، يمكنك مقارنتها بـ ===.

Closures لديهم أيضًا نوع خاص بهم، والذي يتم طباعته عادةً بأسماء تنتهي بـ #<number>. الأسماء والأنواع للدوال المعرفة في مواقع مختلفة متميزة، ولكن لا يُضمن أن يتم طباعتها بنفس الطريقة عبر الجلسات.

julia> typeof(x -> x + 1)
var"#9#10"

أنواع الإغلاقات ليست بالضرورة كائنات مفردة.

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

لكل نوع T، فإن Type{T} هو نوع بارامتري تجريدي ليس له سوى مثيل واحد وهو الكائن T. حتى نناقش Parametric Methods و conversions، من الصعب شرح فائدة هذا البناء، ولكن باختصار، فإنه يسمح بتخصص سلوك الدالة على أنواع معينة كـ قيم. هذا مفيد لكتابة طرق (خاصة البارامترية منها) يعتمد سلوكها على نوع يتم إعطاؤه كوسيط صريح بدلاً من أن يُفهم من نوع أحد وسائطها.

نظرًا لأن التعريف يصعب فهمه قليلاً، دعنا نلقي نظرة على بعض الأمثلة:

julia> isa(Float64, Type{Float64})
true

julia> isa(Real, Type{Float64})
false

julia> isa(Real, Type{Real})
true

julia> isa(Float64, Type{Real})
false

بعبارة أخرى، isa(A, Type{B}) صحيح إذا وفقط إذا كان A و B نفس الكائن وأن هذا الكائن هو نوع.

بشكل خاص، نظرًا لأن الأنواع البارامترية هي invariant، لدينا

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

بدون المعامل، Type هو ببساطة نوع مجرد يحتوي على جميع كائنات الأنواع كحالات له:

julia> isa(Type{Float64}, Type)
true

julia> isa(Float64, Type)
true

julia> isa(Real, Type)
true

أي كائن ليس نوعًا ليس مثيلًا لـ Type:

julia> isa(1, Type)
false

julia> isa("foo", Type)
false

بينما Type هو جزء من تسلسل أنواع جوليا مثل أي نوع بارامتر آخر مجرد، إلا أنه لا يُستخدم عادةً خارج توقيعات الطرق باستثناء بعض الحالات الخاصة. حالة استخدام مهمة أخرى لـ Type هي تحسين أنواع الحقول التي قد يتم التقاطها بشكل أقل دقة، على سبيل المثال كـ DataType في المثال أدناه حيث يمكن أن يؤدي المُنشئ الافتراضي إلى مشاكل في الأداء في الشيفرة التي تعتمد على النوع المغلف الدقيق (بالمثل لـ 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

في بعض الأحيان يكون من الملائم تقديم اسم جديد لنوع يمكن التعبير عنه بالفعل. يمكن القيام بذلك باستخدام عبارة تعيين بسيطة. على سبيل المثال، يتم تعيين UInt إما إلى UInt32 أو UInt64 حسب حجم المؤشرات في النظام:

# 32-bit system:
julia> UInt
UInt32

# 64-bit system:
julia> UInt
UInt64

يتم تحقيق ذلك من خلال الكود التالي في base/boot.jl:

if Int === Int64
    const UInt = UInt64
else
    const UInt = UInt32
end

بالطبع، يعتمد ذلك على ما يتم الإشارة إليه بـ Int - ولكن تم تعريفه مسبقًا ليكون النوع الصحيح - إما Int32 أو Int64.

(لاحظ أنه على عكس Int، لا يوجد Float كاسم نوع لمقاس محدد AbstractFloat. على عكس سجلات الأعداد الصحيحة، حيث يعكس حجم Int حجم المؤشر الأصلي على تلك الآلة، يتم تحديد أحجام سجلات النقاط العائمة بواسطة معيار IEEE-754.)

يمكن أن تكون أسماء الأنواع معلمة:

julia> const Family{T} = Set{T}
Set

julia> Family{Char} === Set{Char}
true

Operations on Types

نظرًا لأن الأنواع في جوليا هي في حد ذاتها كائنات، يمكن للدوال العادية العمل عليها. تم تقديم بعض الدوال التي تعتبر مفيدة بشكل خاص للعمل مع الأنواع أو استكشافها، مثل عامل <:، الذي يشير إلى ما إذا كان المعامل الأيسر هو نوع فرعي من المعامل الأيمن.

تختبر دالة isa ما إذا كان الكائن من نوع معين وتعيد القيمة صحيحة أو خاطئة:

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

تقوم دالة typeof، التي تم استخدامها بالفعل في جميع أنحاء الدليل في الأمثلة، بإرجاع نوع وسيطها. نظرًا لأن الأنواع، كما هو مذكور أعلاه، هي كائنات، فإن لها أيضًا أنواع، ويمكننا أن نسأل ما هي أنواعها:

julia> typeof(Rational{Int})
DataType

julia> typeof(Union{Real,String})
Union

ماذا لو كررنا العملية؟ ما هو نوع نوع نوع؟ كما يحدث، فإن الأنواع هي جميعها قيم مركبة وبالتالي جميعها لها نوع DataType:

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

DataType هو نوعه الخاص.

عملية أخرى تنطبق على بعض الأنواع هي supertype، والتي تكشف عن النوع الفائق لنوع ما. فقط الأنواع المعلنة (DataType) لها أنواع فائقة غير غامضة:

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

إذا قمت بتطبيق supertype على كائنات من نوع آخر (أو كائنات غير نوعية)، يتم رفع MethodError:

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

غالبًا ما يرغب المرء في تخصيص كيفية عرض مثيلات نوع معين. يتم تحقيق ذلك عن طريق تحميل دالة show. على سبيل المثال، لنفترض أننا نعرف نوعًا لتمثيل الأعداد المركبة في الشكل القطبي:

julia> struct Polar{T<:Real} <: Number
           r::T
           Θ::T
       end

julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar

هنا، أضفنا دالة مُنشئ مخصصة بحيث يمكنها أخذ معطيات من أنواع مختلفة Real وترقيتها إلى نوع مشترك (انظر Constructors و Conversion and Promotion). (بالطبع، سيتعين علينا تعريف الكثير من الطرق الأخرى أيضًا، لجعلها تعمل مثل Number، مثل +، *، one، zero، قواعد الترقية وما إلى ذلك.) بشكل افتراضي، تعرض مثيلات هذا النوع ببساطة، مع معلومات حول اسم النوع وقيم الحقول، كما في Polar{Float64}(3.0,4.0).

إذا أردنا أن يتم عرضه بدلاً من ذلك كـ 3.0 * exp(4.0im)، سنقوم بتعريف الطريقة التالية لطباعة الكائن إلى كائن الإخراج المعطى io (يمثل ملفًا، أو محطة، أو مخزن مؤقت، إلخ؛ انظر Networking and Streams):

julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")

يمكن التحكم بشكل أكثر دقة في عرض كائنات Polar. على وجه الخصوص، أحيانًا يرغب المرء في الحصول على تنسيق طباعة متعدد الأسطر مفصل، يُستخدم لعرض كائن واحد في REPL وبيئات تفاعلية أخرى، وأيضًا تنسيق مضغوط في سطر واحد يُستخدم لـ print أو لعرض الكائن كجزء من كائن آخر (مثل في مصفوفة). على الرغم من أنه بشكل افتراضي يتم استدعاء دالة show(io, z) في كلا الحالتين، يمكنك تعريف تنسيق متعدد الأسطر مختلف لعرض كائن عن طريق تحميل شكل ثلاثي المعاملات من show الذي يأخذ نوع MIME text/plain كمعامل ثانٍ (انظر Multimedia I/O)، على سبيل المثال:

julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
           print(io, "Polar{$T} complex number:\n   ", z)

(لاحظ أن print(..., z) هنا ستستدعي طريقة show(io, z) ذات المعاملين.) وهذا يؤدي إلى:

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)

حيث لا يزال يتم استخدام الشكل ذو السطر الواحد show(io, z) لمصفوفة من قيم Polar. تقنيًا، يستدعي REPL display(z) لعرض نتيجة تنفيذ سطر، والذي يتضمن افتراضيًا show(stdout, MIME("text/plain"), z)، والذي بدوره يتضمن افتراضيًا show(stdout, z)، ولكن يجب عليك عدم تعريف طرق جديدة display ما لم تكن تقوم بتعريف معالج عرض وسائط متعددة جديد (انظر Multimedia I/O).

علاوة على ذلك، يمكنك أيضًا تعريف طرق show لأنواع MIME الأخرى من أجل تمكين عرض أغنى (HTML، صور، إلخ) للكائنات في البيئات التي تدعم ذلك (مثل IJulia). على سبيل المثال، يمكننا تعريف عرض HTML المنسق لكائنات Polar، مع الأسس والمائل، عبر:

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>")

سيتم بعد ذلك عرض كائن Polar تلقائيًا باستخدام HTML في بيئة تدعم عرض HTML، ولكن يمكنك استدعاء show يدويًا للحصول على مخرجات HTML إذا كنت ترغب في ذلك:

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

كقاعدة عامة، يجب أن تقوم طريقة show ذات السطر الواحد بطباعة تعبير جوليا صالح لإنشاء الكائن المعروض. عندما تحتوي هذه الطريقة show على عوامل تشغيل داخلية، مثل عامل الضرب (*) في طريقة show ذات السطر الواحد لـ Polar أعلاه، فقد لا يتم تحليلها بشكل صحيح عند طباعتها كجزء من كائن آخر. لرؤية ذلك، اعتبر كائن التعبير (انظر Program representation) الذي يأخذ مربع حالة معينة من نوع Polar لدينا:

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

لأن العامل ^ له أسبقية أعلى من * (انظر Operator Precedence and Associativity)، فإن هذه النتيجة لا تمثل بدقة التعبير a ^ 2 الذي يجب أن يكون مساوياً لـ (3.0 * exp(4.0im)) ^ 2. لحل هذه المشكلة، يجب علينا إنشاء طريقة مخصصة لـ Base.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int)، والتي يتم استدعاؤها داخليًا بواسطة كائن التعبير عند الطباعة:

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)

تضيف الطريقة المحددة أعلاه أقواسًا حول الاستدعاء لـ show عندما تكون أسبقية عامل الاستدعاء أعلى من أو تساوي أسبقية الضرب. تسمح هذه الفحص بالتعبيرات التي يتم تحليلها بشكل صحيح بدون الأقواس (مثل :($a + 2) و :($a == 2)) بتجاهلها عند الطباعة:

julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)

julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)

في بعض الحالات، يكون من المفيد تعديل سلوك طرق show اعتمادًا على السياق. يمكن تحقيق ذلك من خلال النوع IOContext، الذي يسمح بتمرير الخصائص السياقية مع تدفق IO مغلف. على سبيل المثال، يمكننا بناء تمثيل أقصر في طريقة show الخاصة بنا عندما تكون خاصية :compact مضبوطة على true، والعودة إلى التمثيل الطويل إذا كانت الخاصية false أو غائبة:

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

سيتم استخدام هذا التمثيل المدمج الجديد عندما يكون تدفق الإدخال/الإخراج الممرر هو كائن IOContext مع خاصية :compact مضبوطة. على وجه الخصوص، هذه هي الحالة عند طباعة المصفوفات ذات الأعمدة المتعددة (حيث تكون المساحة الأفقية محدودة):

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 للحصول على قائمة بالخصائص الشائعة التي يمكن استخدامها لضبط الطباعة.

"Value types"

في جوليا، لا يمكنك تنفيذ التوزيع بناءً على قيمة مثل true أو false. ومع ذلك، يمكنك تنفيذ التوزيع بناءً على الأنواع المعلمية، وتسمح جوليا لك بتضمين قيم "عادية" (أنواع، رموز، أعداد صحيحة، أعداد عائمة، مجموعات، إلخ) كمعلمات نوع. مثال شائع هو معلمة الأبعاد في Array{T,N}، حيث T هو نوع (على سبيل المثال، Float64) ولكن N هو مجرد Int.

يمكنك إنشاء أنواع مخصصة خاصة بك تأخذ القيم كمعلمات، واستخدامها للتحكم في إرسال الأنواع المخصصة. لتوضيح هذه الفكرة، دعنا نقدم النوع البرامترى Val{x}، وبانيه Val(x) = Val{x}(), الذي يعمل كطريقة تقليدية لاستغلال هذه التقنية في الحالات التي لا تحتاج فيها إلى تسلسل هرمي أكثر تعقيدًا.

Val يُعرَّف على أنه:

julia> struct Val{x}
       end

julia> Val(x) = Val{x}()
Val

لا يوجد المزيد في تنفيذ Val من هذا. بعض الدوال في مكتبة جوليا القياسية تقبل مثيلات Val كوسائط، ويمكنك أيضًا استخدامها لكتابة دوالك الخاصة. على سبيل المثال:

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"

لضمان الاتساق عبر جوليا، يجب على موقع الاستدعاء دائمًا تمرير مثيل Val بدلاً من استخدام نوع، أي استخدم foo(Val(:bar)) بدلاً من foo(Val{:bar}).

من الجدير بالذكر أنه من السهل للغاية إساءة استخدام أنواع "القيمة" المعاملية، بما في ذلك Val؛ في الحالات غير المواتية، يمكنك بسهولة أن تنتهي إلى جعل أداء الكود الخاص بك أسوأ بكثير. على وجه الخصوص، لن ترغب أبدًا في كتابة كود فعلي كما هو موضح أعلاه. لمزيد من المعلومات حول الاستخدامات الصحيحة (وغير الصحيحة) لـ Val، يرجى قراءة the more extensive discussion in the performance tips.

  • 1"Small" is defined by the max_union_splitting configuration, which currently defaults to 4.
  • 2A few popular languages have singleton types, including Haskell, Scala and Ruby.