المسارات
/
Julia
Julia
/
التمارين
/
خبير التصحيح
خبير التصحيح

خبير التصحيح

تمرين تعلّمي

مقدمة

حتى الآن، لم يقل المنهج الكثير عن الأنواع، لكنها موجودة بوضوح في 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 الكود (كله)، ويرى كيف يُستخدم المتغير، ثم يستنتج نوعًا افتراضيًا مناسبًا يتوافق مع ذلك الاستخدام.

إسناد الأنواع

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

في معظم المعالجات الحديثة، يكون العدد الصحيح افتراضيًا من النوع 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.

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

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

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

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

لا تمتلك Julia أصنافًا.

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

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

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

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

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

كل عنصر في الشجرة هو 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.

التعليمات

في هذا التمرين، ستقوم بقليل من هندسة البيانات عبر المعالجة المسبقة لبعض بيانات الدرجات. تحاول مدرسة تضم عددًا كبيرًا جدًا (بل مفرط؟) من الطلاب إجراء بعض تحليل البيانات على الدرجات التي حصل عليها الطلاب، وتسعى إلى إنجاز ذلك بكفاءة، لذا فالفكرة هي تقليل البيانات وتنظيمها.

عادةً ما يكون سلّم الدرجات من 0 إلى 10، لذا فالأكثر كفاءة هو استخدام نوع بيانات لا يتجاوز ثماني بتات لكل درجة.

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

بعد أن تنفّذ الدالة التي تحوّل الدرجات، ستحتاج إلى كتابة دالة أخرى للتعامل مع المجموعات التي تُحفظ فيها، وذلك بخفض الدرجات وإرجاعها بترتيب تنازلي.

  • يستخدم بعض الأساتذة مصفوفات من النوع Vector مرتبة ترتيبًا تصاعديًا لتخزين درجاتهم، لأنهم دقيقون في بياناتهم.
  • ويستخدم أساتذة آخرون مجموعات Set غير مرتبة لتخزين درجاتهم، لأنهم أكثر تكاسلًا قليلًا.

في كلتا الدالتين، ستحتاج إلى التعامل مع المُدخلات غير الصالحة عن طريق رمي MethodError.

Note

ستُغطّى معالجة الاستثناءات في مفهوم لاحق، لذا، ولأغراض هذا التمرين، يمكنك استخدام الصيغة التالية:

throw(MethodError(f, args))

حيث f هي الدالة، وargs هو صف يحتوي على الوسائط المُدخلة إلى الدالة. وفي حالة الدالة demote(n)، فإن f هي demote وargs هو (n,):

throw(MethodError(demote, (n,)))

1. خفّض الدرجات

نفّذ الطريقة demote(n).

  • مع مُدخل من النوع Float64، ينبغي أن تقرّب إلى أقرب عدد صحيح وتُرجع قيمة من نوع البيانات UInt8.
  • ومع أي عدد صحيح Integer، ينبغي أن تُرجع العدد الصحيح نفسه من نوع البيانات Int8.
  • أما بقية المُدخلات فينبغي أن ترمي MethodError.
julia> demote(4.2)::UInt8
5

julia> demote(4)::Int8
4

julia> demote("hi")
MethodError: no method matching demote(::String)     #output truncated

2. عالج البيانات مسبقًا

نفّذ الطريقة preprocess(coll).

  • مع مُدخل من النوع Vector، ينبغي أن تخفّض جميع الأعداد وتعكس ترتيب المصفوفة.
  • ومع مُدخل من النوع Set، ينبغي أن تخفّض جميع الأعداد وتُرجع مصفوفة مرتبة ترتيبًا تنازليًا.
  • أما بقية المُدخلات فينبغي أن ترمي MethodError.
julia> preprocess([1, 2, 3])
3-element Vector{Int8}:
 3
 2
 1

julia> preprocess(Set([2.2, 5.8, 3.4]))
3-element Vector{UInt8}:
 6
 4
 3

julia> preprocess(42)
MethodError: no method matching preprocess(::Int64)     #output truncated
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
Julia Exercism

مستعد لبدء خبير التصحيح؟

سجّل في Exercism لتتعلّم وتتقن Julia عبر 35 مفهومًا128 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.