لقاءات

لقاءات

تمرين تعلّمي

مقدمة

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

julia> f(x) = x^2
f (generic function with 1 method)

لماذا "generic function with 1 method"؟

لنفترض أننا نريد أن نكون أكثر تحديدًا بشأن أنواع الوسائط.

julia> f(x::Integer) = x^2
f (generic function with 2 methods)

julia> f(x::Real) = x^2
f (generic function with 3 methods)

أو يمكننا تغيير عدد الوسائط:

julia> f(x, y) = x * y
f (generic function with 4 methods)

يبدو أن التعريفات الجديدة للدالة نفسها تُسمّى «طرقًا». وتُضاف الطرق إلى قائمة بدلًا من أن تستبدل التعريف القديم.

يمكننا سرد الطرق بسهولة:

julia> methods(f)  # output edited for brevity
# 4 methods for generic function "f" from Main:
 [1] f(x, y)
 [2] f(x::Integer)
 [3] f(x::Real)
 [4] f(x)

إذا استدعينا الدالة f، تعرف Julia بطريقة ما أي طريقة ينبغي استخدامها:

julia> f(2.5)  # Float64
6.25

julia> f(2)  # Int64
4

julia> f(2, 3)  # Int64, Int64
6

ما هي الدالة؟

وفقًا للدليل، الدالة كائن يربط صفًّا من الوسائط بقيمة إرجاع.

نفكّر عادةً في وسائط الدالة في f(2, 3) على أنها عنصران منفصلان، لكن المترجم يراها صفًّا واحدًا. وهذا صحيح حتى مع وسيط واحد (وإن كنا نحتاج إلى إضافة فاصلة لتوضيح ذلك):

julia> typeof( (2, 3) )
Tuple{Int64, Int64}

julia> typeof( (2,) )
Tuple{Int64}

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

كيف تتعامل الدوال مع اختلاف أعداد الوسائط وأنواعها؟

يُشار إلى هذا عادةً باسم «تعدد الأشكال».

كان هذا موضوعًا كبيرًا في علوم الحاسوب على مدى عدة عقود، واعتُمدت مقاربات مختلفة.

مقاربة Julia غير معتادة، وإن لم تكن فريدة.

  • الدوال «عامة»، أي يمكن أن تُرتبط بها عدة طرق.
  • الدوال مجرد أسماء، إلى أن تُرتبط بها طرق.
  • يمكن إضافة الطرق حسب الحاجة، وبأعداد كبيرة أحيانًا (فعامل + دالة لها 161 طريقة حاليًا).
  • عند استدعاء دالة، تفحص Julia توقيع الاستدعاء: عدد الوسائط والنوع الحالي لكل منها.
  • تُختار الطريقة الأقرب مطابقةً لتوقيع الاستدعاء.
    • بشكل عام، يجب أن يتطابق عدد الوسائط تمامًا (وإن كان عامل النشر/الالتقاط ... قادرًا على تجاوز ذلك جزئيًا).
    • وتُفضَّل الطرق التي تطابق أنواعها توقيع الاستدعاء تمامًا.
    • وإذا لزم الأمر، تنظر Julia في الأنواع الأعلى لكل وسيط في توقيع الاستدعاء لإيجاد الطريقة الأقرب مطابقة والأكثر تحديدًا.
  • تُرسَل الطريقة المختارة (dispatched) مع الوسائط المعطاة، وتُرجع نتيجة.
  • وإذا لم تُوجد طريقة مناسبة، يُرفع استثناء MethodError.

لاحظ أن Julia ذات أنواع ديناميكية، لذا يحدث إرسال الطرق في وقت التشغيل.

الإرسال المتعدد: مثال

كان في القسم السابق كلام كثير، فلنترجم تلك الأفكار إلى كود. وللتبسيط، للدوال أدناه وسيط واحد فقط، لكن ليس غريبًا أن نجد دوال بعشرات الوسائط (كما في حزم الرسم البياني أو تعلّم الآلة).

ابدأ بتعريف بعض الأنواع المخصصة لتمثيل أشكال هندسية.

julia> abstract type Shape end

julia> struct Circle <: Shape
           radius::Float64
       end

julia> struct Square <: Shape
           side::Float64
       end

julia> struct Rectangle <: Shape
           length::Float64
           width::Float64
       end

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

julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)

julia> area(s::Square) = s.side^2
area (generic function with 2 methods)

julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)

الآن يمكننا إنشاء بعض الأشكال وحساب مساحاتها، معتمدين على آلية الإرسال في Julia لاستخدام الصيغة الصحيحة لكل شكل.

julia> circle = Circle(2.3)
Circle(2.3)

julia> area(circle)  # call signature Tuple{Circle}
16.619025137490002

julia> square = Square(1.6)
Square(1.6)

julia> area(square)  # call signature Tuple{Square}
2.5600000000000005

julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)

julia> area(rectangle)  # call signature Tuple{Rectangle}
2.94

طرق ذات معاملات نوعية

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

الصياغة مميزة، إذ تتطلب جملة where:

julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)

julia> same_type([1, 2], 3)
true

julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)

يتطلب المثال أعلاه أن يستخدم الوسيطان النوع نفسه T، أيًّا كان.

يمكننا أن نكون أكثر تحديدًا بوضع قيد على T:

julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)

مع هذه الطريقة، لا تُرجع true إلا الأنواع العددية المتطابقة.

ويمكن استخدام عدة متغيرات صورية، مفصولة بفواصل داخل جملة where.

julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)

الخلاصة

الإرسال المتعدد مرن جدًا وعالي الأداء في Julia، لذا يُستخدم على نطاق واسع.

وكما يوحي مثال عامل + ذي الـ161 طريقة، يسعى مبرمجو Julia إلى تعريف طرق بأنواع محددة بدقة. جمع عددين صحيحين يختلف كثيرًا عن جمع عددين عشريين على مستوى الكود الثنائي، كما يضيف جمع عدد صحيح إلى عدد عشري تعقيدًا إضافيًا يتمثل في ترقية الأنواع.

وكلما كان النوع أكثر تحديدًا، زاد ما يستطيع المترجم القيام به من تحسينات: وهذا ذو أهمية هامشية في تمرين صغير على Exercism، لكنه حيوي للأداء في المحاكيات العددية الكبيرة التي صُمِّمت Julia من أجلها.

إذا كنت تعرف المدخلات التي ستتلقاها طريقة ما، فأخبر المترجم بذلك!

التعليمات

في هذا التمرين، ستُنفّذ محاكاةً للقاءات. سيمنحك هذا إلمامًا بأساسيات الإرسال المتعدد، وهو النموذج الرئيسي في Julia.

For this exercise, all function definitions should be a single-line declaration with no if/else logic.

بشكل عام، يتضمّن اللقاء كيانًا a يلتقي كيانًا آخر b ويتفاعل معه.

encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."

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

1. عرّف أنواعًا مخصّصة

أولًا، أضف نوعًا مجرّدًا Pet.

ثم عرّف النوعين Cat و Dog كنوعين فرعيين من Pet. لكلٍّ منهما حقل واحد هو name.

2. احصل على اسم الحيوان الأليف.

نفّذ دالة name() تُرجع اسم الحيوان الأليف.

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. حدّد ما يحدث عندما تلتقي القطط والكلاب

نفّذ طرق meets() للقاءات التالية:

  • عندما يلتقي كلبان، فإنهما sniff بعضهما البعض.
  • عندما يلتقي قطّ بكلب، فإنه hisses في وجه الكلب.
  • عندما يلتقي كلب بقطّ، فإنه chases القطّ.
  • عندما يلتقي قطّان، فإنهما slink.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. عرّف لقاءً بين كيانين.

نفّذ دالة encounter() بطريقة واحدة، لتُبلّغ عمّا يحدث عندما يلتقي كيانان لهما اسمان.

julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."

5. حدّد ردّ فعل احتياطيًا للقاءات بين الحيوانات الأليفة

ماذا يحدث إذا صادفوا حيوانًا أليفًا مختلفًا أثناء نزهتهم، مثل حصان؟

  • إذا التقى حيوان أليف بحيوان أليف آخر لا يعرفه، فإنه is cautious.
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. حدّد ردًّا احتياطيًا إذا صادف حيوان أليف شيئًا لا يعرفه

هناك أشياء أخرى كثيرة قد تصادفها الحيوانات الأليفة وليست حيوانات أليفة: السيارات والبشر والنباتات والكوارث الطبيعية والكويكبات… فماذا يحدث حينها؟

  • إذا التقى حيوان أليف بشيء لم يره من قبل، فإنه runs away.
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. حدّد ردًّا احتياطيًا عامًّا

هناك لقاءات أخرى كثيرة قد تحدث في محاكاتنا. سيارة تلتقي بكلب، وإلكترونان يلتقيان ويتفاعلان…

من المستحيل تغطية كل اللقاءات مسبقًا، لذلك سننفّذ ردًّا احتياطيًا عامًّا.

  • إذا التقى شيئان أو كائنان مجهولان، فإن nothing happens.
julia> γ1 = Photon("γ1")
Photon("γ1")

julia> γ2 = Photon("γ2")
Photon("γ2")

julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens." 
# This is true, photons just pass through each other.
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
Julia Exercism

مستعد لبدء لقاءات؟

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