More about types

إذا كنت قد استخدمت جوليا لفترة، فأنت تفهم الدور الأساسي الذي تلعبه الأنواع. هنا نحاول التعمق في التفاصيل، مع التركيز بشكل خاص على Parametric Types.

Types and sets (and Any and Union{}/Bottom)

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

نوع خرساني T يصف مجموعة القيم التي يتم إرجاع علامةها المباشرة، كما هو موضح بواسطة دالة typeof، هي T. نوع مجرد يصف مجموعة من القيم التي قد تكون أكبر.

Any يصف الكون بأكمله من القيم الممكنة. Integer هو مجموعة فرعية من Any التي تشمل Int، Int8، وأنواع ملموسة أخرى. داخليًا، تستخدم جوليا أيضًا نوعًا آخر معروفًا باسم Bottom، والذي يمكن أيضًا كتابته كـ Union{}. هذا يتوافق مع المجموعة الفارغة.

تدعم أنواع جوليا العمليات القياسية في نظرية المجموعات: يمكنك أن تسأل ما إذا كانت T1 هي "مجموعة فرعية" (نوع فرعي) من T2 باستخدام T1 <: T2. وبالمثل، يمكنك تقاطع نوعين باستخدام typeintersect، وأخذ اتحادهم باستخدام Union، وحساب نوع يحتوي على اتحادهم باستخدام typejoin:

julia> typeintersect(Int, Float64)
Union{}

julia> Union{Int, Float64}
Union{Float64, Int64}

julia> typejoin(Int, Float64)
Real

julia> typeintersect(Signed, Union{UInt8, Int8})
Int8

julia> Union{Signed, Union{UInt8, Int8}}
Union{UInt8, Signed}

julia> typejoin(Signed, Union{UInt8, Int8})
Integer

julia> typeintersect(Tuple{Integer, Float64}, Tuple{Int, Real})
Tuple{Int64, Float64}

julia> Union{Tuple{Integer, Float64}, Tuple{Int, Real}}
Union{Tuple{Int64, Real}, Tuple{Integer, Float64}}

julia> typejoin(Tuple{Integer, Float64}, Tuple{Int, Real})
Tuple{Integer, Real}

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

UnionAll types

يمكن لنظام أنواع جوليا أيضًا التعبير عن اتحاد متكرر للأنواع: اتحاد للأنواع على جميع قيم متغير ما. هذا ضروري لوصف الأنواع المعاملية حيث لا تُعرف قيم بعض المعاملات.

على سبيل المثال، Array يحتوي على معلمتين كما في Array{Int,2}. إذا لم نكن نعرف نوع العنصر، يمكننا كتابة Array{T,2} where T، وهو اتحاد Array{T,2} لجميع قيم T: Union{Array{Int8,2}, Array{Int16,2}, ...}.

يمثل هذا النوع كائن UnionAll، الذي يحتوي على متغير (T في هذا المثال، من نوع TypeVar)، ونوع مغلف (Array{T,2} في هذا المثال).

اعتبر الطرق التالية:

f1(A::Array) = 1
f2(A::Array{Int}) = 2
f3(A::Array{T}) where {T<:Any} = 3
f4(A::Array{Any}) = 4

التوقيع - كما هو موصوف في Function calls - لـ f3 هو نوع UnionAll يلتف حول نوع التوابل: Tuple{typeof(f3), Array{T}} where T. يمكن استدعاء جميع الدوال باستثناء f4 مع a = [1,2]؛ يمكن استدعاء جميع الدوال باستثناء f2 مع b = Any[1,2].

دعونا نلقي نظرة على هذه الأنواع عن كثب:

julia> dump(Array)
UnionAll
  var: TypeVar
    name: Symbol T
    lb: Union{}
    ub: Any
  body: UnionAll
    var: TypeVar
      name: Symbol N
      lb: Union{}
      ub: Any
    body: Array{T, N} <: DenseArray{T, N}
      ref::MemoryRef{T}
      size::NTuple{N, Int64}

هذا يشير إلى أن Array في الواقع يسمي نوع UnionAll. هناك نوع UnionAll واحد لكل معلمة، متداخلة. الصيغة Array{Int,2} تعادل Array{Int}{2}؛ داخليًا، يتم إنشاء كل UnionAll بقيمة متغيرة معينة، واحدة في كل مرة، من الخارج إلى الداخل. هذا يعطي معنى طبيعي لترك معلمات النوع اللاحقة؛ Array{Int} يعطي نوعًا يعادل Array{Int,N} حيث N.

TypeVar ليست نوعًا بحد ذاتها، بل يجب اعتبارها جزءًا من بنية نوع UnionAll. تحتوي متغيرات النوع على حدود دنيا وعليا لقيمها (في الحقول lb و ub). الرمز name هو تجميلي بحت. داخليًا، يتم مقارنة TypeVars حسب العنوان، لذا يتم تعريفها كأنواع قابلة للتغيير لضمان إمكانية تمييز "متغيرات النوع المختلفة". ومع ذلك، يجب عدم تغييرها وفقًا للعرف.

يمكن للمرء إنشاء TypeVars يدويًا:

julia> TypeVar(:V, Signed, Real)
Signed<:V<:Real

هناك إصدارات مريحة تتيح لك حذف أي من هذه المعاملات باستثناء رمز name.

يتم تقليل الصيغة Array{T} where T<:Integer إلى

let T = TypeVar(:T,Integer)
    UnionAll(T, Array{T})
end

لذا نادرًا ما يكون من الضروري إنشاء TypeVar يدويًا (في الواقع، يجب تجنب ذلك).

Free variables

مفهوم المتغير من النوع المجاني مهم للغاية في نظام الأنواع. نقول إن المتغير V مجاني في النوع T إذا كان T لا يحتوي على UnionAll الذي يقدم المتغير V. على سبيل المثال، النوع Array{Array{V} where V<:Integer} ليس لديه متغيرات مجانية، لكن الجزء Array{V} بداخله يحتوي على متغير مجاني، وهو V.

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

لهذا السبب، فإن الدالة jl_has_free_typevars في واجهة برمجة التطبيقات C مهمة جدًا. الأنواع التي تعيد true لها لن تعطي إجابات ذات مغزى في التوريث وغيرها من وظائف النوع.

TypeNames

النوعان التاليان Array متساويان وظيفيًا، ومع ذلك يطبعا بشكل مختلف:

julia> TV, NV = TypeVar(:T), TypeVar(:N)
(T, N)

julia> Array
Array

julia> Array{TV, NV}
Array{T, N}

يمكن تمييز هذه من خلال فحص حقل name من النوع، وهو كائن من نوع TypeName:

julia> dump(Array{Int,1}.name)
TypeName
  name: Symbol Array
  module: Module Core
  names: empty SimpleVector
  wrapper: UnionAll
    var: TypeVar
      name: Symbol T
      lb: Union{}
      ub: Any
    body: UnionAll
      var: TypeVar
        name: Symbol N
        lb: Union{}
        ub: Any
      body: Array{T, N} <: DenseArray{T, N}
  cache: SimpleVector
    ...

  linearcache: SimpleVector
    ...

  hash: Int64 -7900426068641098781
  mt: MethodTable
    name: Symbol Array
    defs: Nothing nothing
    cache: Nothing nothing
    max_args: Int64 0
    module: Module Core
    : Int64 0
    : Int64 0

في هذه الحالة، الحقل المعني هو wrapper، الذي يحتفظ بإشارة إلى النوع العلوي المستخدم لإنشاء أنواع Array جديدة.

julia> pointer_from_objref(Array)
Ptr{Cvoid} @0x00007fcc7de64850

julia> pointer_from_objref(Array.body.body.name.wrapper)
Ptr{Cvoid} @0x00007fcc7de64850

julia> pointer_from_objref(Array{TV,NV})
Ptr{Cvoid} @0x00007fcc80c4d930

julia> pointer_from_objref(Array{TV,NV}.name.wrapper)
Ptr{Cvoid} @0x00007fcc7de64850

حقل wrapper من Array يشير إلى نفسه، ولكن بالنسبة لـ Array{TV,NV} فإنه يشير مرة أخرى إلى التعريف الأصلي لنوع البيانات.

ماذا عن الحقول الأخرى؟ hash يخصص عددًا صحيحًا لكل نوع. لفحص حقل cache، من المفيد اختيار نوع يستخدم أقل من Array. دعنا أولاً ننشئ نوعنا الخاص:

julia> struct MyType{T,N} end

julia> MyType{Int,2}
MyType{Int64, 2}

julia> MyType{Float32, 5}
MyType{Float32, 5}

عند إنشاء نوع بارامتري، يتم حفظ كل نوع ملموس في ذاكرة التخزين المؤقت للنمط (MyType.body.body.name.cache). ومع ذلك، فإن الحالات التي تحتوي على متغيرات نوع حرة لا يتم تخزينها في الذاكرة المؤقتة.

Tuple types

تشكل أنواع التوائم حالة خاصة مثيرة للاهتمام. لكي يعمل التوزيع على إعلانات مثل x::Tuple، يجب أن يكون النوع قادرًا على استيعاب أي توأم. دعنا نتحقق من المعلمات:

julia> Tuple
Tuple

julia> Tuple.parameters
svec(Vararg{Any})

على عكس الأنواع الأخرى، فإن أنواع التوابل متغيرة في معلماتها، لذا فإن هذا التعريف يسمح لـ Tuple بمطابقة أي نوع من أنواع التوابل:

julia> typeintersect(Tuple, Tuple{Int,Float64})
Tuple{Int64, Float64}

julia> typeintersect(Tuple{Vararg{Any}}, Tuple{Int,Float64})
Tuple{Int64, Float64}

ومع ذلك، إذا كان نوع التوابل المتغير (Vararg) يحتوي على متغيرات حرة، فإنه يمكن أن يصف أنواعًا مختلفة من التوابل:

julia> typeintersect(Tuple{Vararg{T} where T}, Tuple{Int,Float64})
Tuple{Int64, Float64}

julia> typeintersect(Tuple{Vararg{T}} where T, Tuple{Int,Float64})
Union{}

لاحظ أنه عندما يكون T حراً بالنسبة لنوع Tuple (أي أن ارتباطه من نوع UnionAll خارج نوع Tuple)، يجب أن تعمل قيمة واحدة فقط من T على النوع بأكمله. لذلك، لا يتطابق التوابل غير المتجانسة.

أخيرًا، من الجدير بالذكر أن Tuple{} مميز:

julia> Tuple{}
Tuple{}

julia> Tuple{}.parameters
svec()

julia> typeintersect(Tuple{}, Tuple{Int})
Union{}

ما هو نوع التوابل "الرئيسي"؟

julia> pointer_from_objref(Tuple)
Ptr{Cvoid} @0x00007f5998a04370

julia> pointer_from_objref(Tuple{})
Ptr{Cvoid} @0x00007f5998a570d0

julia> pointer_from_objref(Tuple.name.wrapper)
Ptr{Cvoid} @0x00007f5998a04370

julia> pointer_from_objref(Tuple{}.name.wrapper)
Ptr{Cvoid} @0x00007f5998a04370

لذا Tuple == Tuple{Vararg{Any}} هو بالفعل النوع الأساسي.

Diagonal types

اعتبر النوع Tuple{T,T} where T. ستبدو طريقة تحمل هذا التوقيع كما يلي:

f(x::T, y::T) where {T} = ...

وفقًا للتفسير المعتاد لنوع UnionAll، فإن هذا T يتراوح بين جميع الأنواع، بما في ذلك Any، لذا يجب أن يكون هذا النوع مكافئًا لـ Tuple{Any,Any}. ومع ذلك، فإن هذا التفسير يسبب بعض المشاكل العملية.

أولاً، يجب أن تكون قيمة T متاحة داخل تعريف الطريقة. بالنسبة لاستدعاء مثل f(1, 1.0)، ليس من الواضح ما يجب أن تكون عليه T. يمكن أن تكون Union{Int,Float64}، أو ربما Real. بشكل بديهي، نتوقع أن تعني الإعلانات x::T أن T === typeof(x). للتأكد من أن هذا الثبات قائم، نحتاج إلى typeof(x) === typeof(y) === T في هذه الطريقة. وهذا يعني أن الطريقة يجب أن تُستدعى فقط للوسائط من نفس النوع بالضبط.

يتضح أن القدرة على التوزيع بناءً على ما إذا كانت قيمتان لهما نفس النوع مفيدة جدًا (يتم استخدام ذلك من قبل نظام الترويج على سبيل المثال)، لذا لدينا عدة أسباب لرغبتنا في تفسير مختلف لـ Tuple{T,T} where T. لجعل هذا يعمل، نضيف القاعدة التالية إلى تصنيف الأنواع: إذا حدثت متغير أكثر من مرة في موضع متزايد، فإنه يُقيد ليشمل فقط الأنواع المحددة. ("الموضع المتزايد" يعني أن أنواع Tuple و Union فقط تحدث بين حدوث متغير ونوع UnionAll الذي يقدمها.) تُسمى هذه المتغيرات "متغيرات قطرية" أو "متغيرات محددة".

لذا على سبيل المثال، Tuple{T,T} where T يمكن أن يُعتبر كـ Union{Tuple{Int8,Int8}, Tuple{Int16,Int16}, ...}، حيث يتراوح T على جميع الأنواع المحددة. وهذا يؤدي إلى بعض النتائج المثيرة للاهتمام في التوريث. على سبيل المثال، Tuple{Real,Real} ليس نوعًا فرعيًا من Tuple{T,T} where T، لأنه يتضمن بعض الأنواع مثل Tuple{Int8,Int16} حيث يكون للعناصر الاثنين أنواع مختلفة. Tuple{Real,Real} و Tuple{T,T} where T لهما تقاطع غير تافه وهو Tuple{T,T} where T<:Real. ومع ذلك، Tuple{Real} هو نوع فرعي من Tuple{T} where T، لأنه في هذه الحالة يحدث T مرة واحدة فقط وبالتالي لا يكون مائلًا.

بعد ذلك، اعتبر توقيعًا مثل ما يلي:

f(a::Array{T}, x::T, y::T) where {T} = ...

في هذه الحالة، يحدث T في موضع ثابت داخل Array{T}. هذا يعني أن أي نوع من المصفوفات يتم تمريره يحدد بشكل غير ملتبس قيمة T - نقول إن T لديه قيود تساوي عليه. لذلك، في هذه الحالة، فإن قاعدة القطر ليست ضرورية حقًا، حيث تحدد المصفوفة T ويمكننا بعد ذلك السماح لـ x و y بأن يكونا من أي نوع فرعي من T. لذا، فإن المتغيرات التي تحدث في موضع ثابت لا تعتبر أبدًا قطرية. هذا الاختيار في السلوك مثير للجدل قليلاً - يشعر البعض أن هذا التعريف يجب أن يُكتب كـ

f(a::Array{T}, x::S, y::S) where {T, S<:T} = ...

لتوضيح ما إذا كان يجب أن يكون لـ x و y نفس النوع. في هذه النسخة من التوقيع سيكونان كذلك، أو يمكننا إدخال متغير ثالث لنوع y إذا كان يمكن أن يكون لـ x و y أنواع مختلفة.

التعقيد التالي هو تفاعل النقابات والمتغيرات القطرية، على سبيل المثال

f(x::Union{Nothing,T}, y::T) where {T} = ...

اعتبر ما تعنيه هذه الإعلانات. y له نوع T. ثم يمكن أن يكون x إما من نفس النوع T، أو أن يكون من النوع Nothing. لذا يجب أن تتطابق جميع المكالمات التالية:

f(1, 1)
f("", "")
f(2.0, 2.0)
f(nothing, 1)
f(nothing, "")
f(nothing, 2.0)

هذه الأمثلة تخبرنا بشيء: عندما يكون x هو nothing::Nothing، لا توجد قيود إضافية على y. إنه كما لو كانت توقيع الطريقة يحتوي على y::Any. في الواقع، لدينا المعادلة النوعية التالية:

(Tuple{Union{Nothing,T},T} where T) == Union{Tuple{Nothing,Any}, Tuple{T,T} where T}

القاعدة العامة هي: المتغير الملموس في موضع متغير يتصرف كما لو لم يكن ملموسًا إذا كانت خوارزمية التوريث تستخدمه مرة واحدة فقط. عندما يكون x من النوع Nothing، لا نحتاج إلى استخدام T في Union{Nothing,T}؛ نحن نستخدمه فقط في الفتحة الثانية. ينشأ هذا بشكل طبيعي من الملاحظة أنه في Tuple{T} where T، فإن تقييد T إلى أنواع ملموسة لا يحدث فرقًا؛ النوع يساوي Tuple{Any} في كلتا الحالتين.

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

Tuple{Int,Int8,Vector{Integer}} <: Tuple{T,T,Vector{Union{Integer,T}}} where T

إذا تم تجاهل T داخل Union، فإن T يكون ملموسًا والإجابة هي "خطأ" لأن النوعين الأولين ليسا متطابقين. ولكن اعتبر بدلاً من ذلك

Tuple{Int,Int8,Vector{Any}} <: Tuple{T,T,Vector{Union{Integer,T}}} where T

الآن لا يمكننا تجاهل T في Union (يجب أن يكون لدينا T == Any)، لذا فإن T ليس محددًا والإجابة هي "صحيح". هذا سيجعل تحديد T يعتمد على النوع الآخر، وهو أمر غير مقبول لأن النوع يجب أن يكون له معنى واضح بمفرده. لذلك، يُعتبر ظهور T داخل Vector في كلا الحالتين.

Subtyping diagonal variables

خوارزمية التفرع للمتغيرات القطرية تحتوي على مكونين: (1) تحديد حدوث المتغيرات، و(2) ضمان أن المتغيرات القطرية تتراوح فقط على أنواع ملموسة.

تتمثل المهمة الأولى في الحفاظ على العدادات occurs_inv و occurs_cov (في src/subtype.c) لكل متغير في البيئة، وتتبع عدد الحدوث الثابت والحادث المتغير، على التوالي. يكون المتغير قطريًا عندما يكون occurs_inv == 0 && occurs_cov > 1.

المهمة الثانية تُنجز من خلال فرض شرط على الحد الأدنى لمتغير. بينما يعمل خوارزمية التفرع، تضيق الحدود لكل متغير (رفع الحدود الدنيا وخفض الحدود العليا) لتتبع نطاق قيم المتغيرات التي ستظل فيها علاقة التفرع قائمة. عندما ننتهي من تقييم جسم نوع UnionAll الذي يكون متغيره قطريًا، ننظر إلى القيم النهائية للحدود. نظرًا لأن المتغير يجب أن يكون ملموسًا، يحدث تناقض إذا لم يكن من الممكن أن يكون حده الأدنى نوعًا فرعيًا من نوع ملموس. على سبيل المثال، لا يمكن أن يكون نوع مجرد مثل AbstractArray نوعًا فرعيًا من نوع ملموس، ولكن يمكن أن يكون نوع ملموس مثل Int، ويمكن أن يكون النوع الفارغ Bottom كذلك. إذا فشل حد أدنى في هذا الاختبار، تتوقف الخوارزمية مع الإجابة false.

على سبيل المثال، في المشكلة Tuple{Int,String} <: Tuple{T,T} where T، نستنتج أن هذا سيكون صحيحًا إذا كان T نوعًا فوقيًا لـ Union{Int,String}. ومع ذلك، فإن Union{Int,String} هو نوع تجريدي، لذا فإن العلاقة لا تنطبق.

يتم إجراء اختبار التحديد هذا بواسطة الدالة is_leaf_bound. لاحظ أن هذا الاختبار يختلف قليلاً عن jl_is_leaf_type، حيث إنه يعيد أيضًا true لـ Bottom. حاليًا، هذه الدالة هي تجريبية، ولا تلتقط جميع الأنواع الملموسة الممكنة. تكمن الصعوبة في أن ما إذا كان الحد الأدنى ملموسًا قد يعتمد على قيم حدود متغيرات النوع الأخرى. على سبيل المثال، Vector{T} يعادل النوع الملموس Vector{Int} فقط إذا كانت كل من الحدود العليا والسفلى لـ T تساوي Int. لم نعمل بعد على خوارزمية كاملة لذلك.

Introduction to the internal machinery

توجد معظم العمليات المتعلقة بالأنواع في الملفات jltypes.c و subtype.c. طريقة جيدة للبدء هي مشاهدة التحتية أثناء العمل. قم ببناء جوليا باستخدام make debug وابدأ جوليا داخل مصحح الأخطاء. gdb debugging tips يحتوي على بعض النصائح التي قد تكون مفيدة.

لأن كود التفرع الفرعي يُستخدم بشكل مكثف في REPL نفسه - وبالتالي يتم تفعيل نقاط التوقف في هذا الكود بشكل متكرر - سيكون من الأسهل إذا قمت بعمل التعريف التالي:

julia> function mysubtype(a,b)
           ccall(:jl_breakpoint, Cvoid, (Any,), nothing)
           a <: b
       end

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

كتمهيد، جرب ما يلي:

mysubtype(Tuple{Int, Float64}, Tuple{Integer, Real})

يمكننا جعلها أكثر إثارة من خلال تجربة حالة أكثر تعقيدًا:

mysubtype(Tuple{Array{Int,2}, Int8}, Tuple{Array{T}, T} where T)

Subtyping and method sorting

تُستخدم دوال type_morespecific لفرض ترتيب جزئي على الدوال في جداول الطرق (من الأكثر تحديدًا إلى الأقل تحديدًا). التحديد صارم؛ إذا كانت a أكثر تحديدًا من b، فإن a لا تساوي b و b ليست أكثر تحديدًا من a.

إذا كان a هو نوع فرعي صارم من b، فإنه يعتبر تلقائيًا أكثر تحديدًا. من هناك، يستخدم type_morespecific بعض القواعد الأقل رسمية. على سبيل المثال، subtype حساس لعدد المعاملات، لكن type_morespecific قد لا يكون كذلك. على وجه الخصوص، Tuple{Int,AbstractFloat} أكثر تحديدًا من Tuple{Integer}، على الرغم من أنه ليس نوعًا فرعيًا. (من Tuple{Int,AbstractFloat} و Tuple{Integer,Float64}، لا يعتبر أي منهما أكثر تحديدًا من الآخر.) وبالمثل، Tuple{Int,Vararg{Int}} ليس نوعًا فرعيًا من Tuple{Integer}، لكنه يعتبر أكثر تحديدًا. ومع ذلك، يحصل morespecific على ميزة للطول: على وجه الخصوص، Tuple{Int,Int} أكثر تحديدًا من Tuple{Int,Vararg{Int}}.

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

type_morespecific(a, b) = ccall(:jl_type_morespecific, Cint, (Any,Any), a, b)

الذي يتيح لك اختبار ما إذا كان نوع التوابل a أكثر تحديدًا من نوع التوابل b.