ال

الأنواع في Julia

1 تمرين

نبذة عن الأنواع

حتى الآن، لم يقل المنهج الكثير عن الأنواع، لكنها موجودة بوضوح في Julia:

julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')

julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}

لم نحدّد الأنواع قط، لكن Julia أسندتها على أي حال.

  1. تمتلك Julia types، وهي محورية في تصميمها.
  2. تستطيع Julia عادةً أن "تخمّن" النوع، باستخدام Type Inference.

سيتفحّص مُصرِّف JIT الكود (كله)، ويرى كيف يُستخدم المتغير، ثم يستنتج نوعًا افتراضيًا مناسبًا متوافقًا مع هذا الاستخدام.

ترقية الأنواع

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

julia> 2 + 1.3
3.3

أضفنا عددًا صحيحًا (Int64) وعددًا عشريًا (Float64) فحصلنا على نتيجة من نوع Float64. وبالمثل:

julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)

julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}

julia> [nums...]
3-element Vector{Float64}:
 3.0
 4.1
 0.25

في الحالة أعلاه، حافظ الصف على نوع كل عنصر، لكن التحويل إلى متجه غيّرها جميعها إلى Float64 موحّد.

يفهم مُصرِّف Julia أي تحويلات للأنواع ممكنة: من عدد صحيح إلى عدد عشري لا مشكلة، ومن عدد عشري إلى عدد صحيح يفقد الدقة، لذا يُرفع InexactError. تحوّل ترقية الأنواع جميع القيم في التعبير إلى نوع مشترك، مرن بما يكفي ليتوافق مع كل المدخلات.

ويمكن إجراء التحويل نفسه صراحةً باستخدام الدالة promote():

julia> promote(nums...)
(3.0, 4.1, 0.25)

إسناد النوع

الاعتماد على استنتاج الأنواع وترقية الأنواع أمر جيد لحل تمارين تعليمية بسيطة، لكن في البرامج الأكبر يُرجّح أنك ستحتاج إلى تحكم أدق.

على معظم المعالجات الحديثة، يكون النوع الافتراضي للعدد الصحيح هو Int64. رأينا في مفهوم Numbers أنه يمكن تحويل القيمة إلى نوع معيّن غير افتراضي.

julia> x = Int16(42)
42

julia> typeof(x)
Int16

ومع ذلك، لا يزال من الممكن إعادة إسناد المتغير x إلى نوع مختلف:

julia> x = "changed"
"changed"

julia> typeof(x)
String

هذا ما يُعرف بـ type instability، وهو:

  • مريح جدًا في النصوص البرمجية الصغيرة.
  • سيئ للأداء والموثوقية في البرامج الأكبر.

بدلًا من ذلك، يمكننا تحديد نوع x باستخدام العامل :::

julia> y::Int16 = 42
42

julia> typeof(y)
Int16

julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.

الآن y من نوع Int16، وسيبقى كذلك دائمًا. وهكذا يعرف المُصرِّف كم بايت يحجز له، ويستطيع تحسين بقية الكود بالاعتماد على نوع مستقر. ومن هذه الناحية، يشبه المتغير إلى حد ما نظيره في لغة ذات أنواع ساكنة مثل C.

تأكيد النوع

يُستخدم إسناد النوع الموصوف أعلاه في الطرف الأيسر من إسناد المتغير لتقييد نوع ذلك المتغير.

استخدام العامل :: مع قيمة، أو مع شيء يُقيَّم إلى قيمة، هو عمومًا تأكيد على أن القيمة يجب أن تكون من هذا النوع، وإلّا فيجب رفع خطأ.

julia> 42::Number
42

julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String

يُستخدم هذا عادةً مع القيمة المُرجَعة من دالة، كفحص أخير بسيط للتأكد من أن الدالة تصرّفت كما هو متوقع.

لاحظ أن هناك ماكرو @assert لأشكال أخرى من التأكيد.

التسلسل الهرمي للأنواع

Int64، Int16، String، Char: من أين "تأتي" هذه الأنواع؟

في كثير من اللغات كائنية التوجه (OO)، كل نوع هو صنف، والاشتقاق يرتّبها في تسلسل هرمي للأصناف، وطرق الأصناف تُعرّف السلوكيات.

‏Java وRuby مثالان واضحان على هذا النمط، لكن حتى Python مشابهة من الداخل.

ليس لدى Julia أصناف.

السبب الموثّق هو أن ميزات البرمجة كائنية التوجه تتعارض مع مُصرِّف JIT وتضرّ بالأداء وقت التشغيل.

من يدري، ربما قرأوا هم أيضًا هذا الاقتباس:

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

وهو منسوب إلى Edsger Dijkstra، عالم حاسوب لامع لعقود عدة بدءًا من الخمسينيات (وإن لم يُعرف عمومًا بتفاؤله المشرق ولا دبلوماسيته اللطيفة).

ومع ذلك، انظر إلى هذا الكود:

julia> y::Int16 = 42
42

julia> typeof(y)
Int16

julia> supertype(Int16)
Signed

julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)

مع بعض التفاصيل:

  • Int16 نوع، ويمكننا إنشاء متغيرات من هذا النوع.
  • Int16 نوع فرعي من Signed، وتُظهر لنا ذلك الدالة supertype().
  • هناك تسلسل هرمي للأنواع، يصعد عبر Integer وReal وNumber إلى Any في القمة، وستُدرج لنا subtypes() هذا الفرع من التسلسل الهرمي.

تنتهي كل الفروع عند Any، وهو فريد في كونه النوع الأعلى لنفسه.

julia> supertypes(String)
(String, AbstractString, Any)

julia> supertype(Any)
Any

إذًا، ليس لدى Julia تسلسل هرمي للأصناف class، لكن لديها بالفعل تسلسل هرمي للأنواع type. محاولة عرض التسلسل الهرمي كاملًا تُنتج شجرة ضخمة يستحيل رؤيتها فعليًا. والنظر إلى أجزاء منها أمر نوقش على الإنترنت.

في النهاية سنحاول تفكيك كيفية عمل هذا، لكن هناك الكثير مما يجب استكشافه أولًا.

التحقق من الأنواع

يمكننا استخدام typeof() لاختبار التساوي بالطريقة المعتادة.

julia> typeof(11)
Int64

julia> typeof(11) == Int64
true

julia> typeof(11) == Number
false

يجب أن يكون تساوي الأنواع تامًا، فهذا الشكل من المقارنة لا يفهم التسلسل الهرمي للأنواع.

وبمرونة أكبر، ستخبرنا isa ما إذا كانت القيمة من النوع نفسه الذي عليه الطرف المقارَن، أو من نوع فرعي منه. ويمكن استخدامها إما في صورة عامل بيني أو في صورة دالة.

julia> 12 isa Int64
true

julia> 12 isa Number
true

julia> isa(12, Number)
true

julia> 12 isa String
false

لاحظ أن isa تتوقع value على اليسار، لا type.

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

julia> Int64 isa Number  ## Don't do this!
false

julia> Int64 <: Number
true

يمكن استخدام التحقق من الأنواع للتحكم في سير التنفيذ داخل دالة، لكن هذا غير معتاد نسبيًا في أسلوب Julia المتّبع.

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

الأنواع المجرّدة مقابل الأنواع الملموسة

رأينا أن التسلسل الهرمي للأنواع يشكّل بنية شجرية (بالمعنى الحاسوبي، حيث الجذر في القمة).

كل عنصر في الشجرة هو node، ويمكن تقسيم هذه العناصر إلى فئات:

  1. العناصر التي لها أنواع فرعية تُسمى abstract.
  2. العناصر الطرفية، التي ليس لها أنواع فرعية، تُسمى concrete.
julia> subtypes(Integer)  # an abstract type
3-element Vector{Any}:
 Bool
 Signed
 Unsigned

julia> subtypes(Int64)  # a concrete type
Type[]

هذا تمييز مهم، لأن الأنواع الملموسة وحدها يمكن أن تكون instantiated كمتغيرات.

julia> a::Int16 = 42
42

julia> typeof(a)
Int16

julia> b::Integer = 42
42

julia> typeof(b)
Int64

لاحظ أن محاولة استخدام نوع مجرّد لا تعطي رسالة خطأ (في هذه الحالة)، لكن المُصرِّف ينشئ نوعًا ملموسًا مناسبًا: Int64 بدل Integer. على الأقل يمنع إسناد المتغير إلى ما ليس عددًا صحيحًا:

julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.

إذًا فإسناد النوع بنوع مجرّد هو تقييد لنوع المتغير، بحيث يكون أي نوع فرعي من Integer (في الحالة أعلاه). وهذا أمر غير معتاد في عالم لغات البرمجة: أضعف من إسناد النوع في C، وأقوى من تلميح النوع في الإصدارات الحديثة من Python.

تتيح الدالتان isabstracttype() وisconcretetype() الاختبار. لاحظ أن هاتين ليستا مجرد نفي إحداهما للأخرى: سنرى في مفهوم لاحق أن بعض الأنواع قد لا تكون مجرّدة ولا ملموسة.

julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)

julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)

# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)

مع أن Vector ليس نوعًا ملموسًا، فإن عناصره ملموسة. وتستخرج الدالة eltype() (نوع العنصر) هذا النوع:

julia> eltype([1, 2.3])
Float64
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة

تعلّم الأنواع