Julia Functions
ستشرح هذه الوثيقة كيفية عمل الدوال وتعريفات الطرق وجداول الطرق.
Method Tables
كل دالة في جوليا هي دالة عامة. الدالة العامة هي مفهومياً دالة واحدة، لكنها تتكون من العديد من التعريفات، أو الطرق. يتم تخزين طرق الدالة العامة في جدول الطرق. ترتبط جداول الطرق (نوع MethodTable
) بـ TypeName
s. يصف TypeName
عائلة من الأنواع المعلمة. على سبيل المثال، تشترك Complex{Float32}
و Complex{Float64}
في نفس كائن اسم النوع Complex
.
جميع الكائنات في جوليا قابلة للاستدعاء بشكل محتمل، لأن كل كائن له نوع، والذي بدوره له TypeName
.
Function calls
بالنظر إلى الاستدعاء f(x, y)
، يتم تنفيذ الخطوات التالية: أولاً، يتم الوصول إلى جدول الطرق المستخدم كـ typeof(f).name.mt
. ثانياً، يتم تشكيل نوع مجموعة المعاملات، Tuple{typeof(f), typeof(x), typeof(y)}
. لاحظ أن نوع الدالة نفسها هو العنصر الأول. وذلك لأن النوع قد يحتوي على معلمات، وبالتالي يحتاج إلى المشاركة في التوزيع. يتم البحث عن نوع المجموعة هذا في جدول الطرق.
تتم عملية الإرسال هذه بواسطة jl_apply_generic
، التي تأخذ وسيطين: مؤشر إلى مصفوفة من القيم f
و x
و y
، وعدد القيم (في هذه الحالة 3).
على مدار النظام، هناك نوعان من واجهات برمجة التطبيقات (APIs) التي تتعامل مع الوظائف وقوائم المعاملات: تلك التي تقبل الوظيفة والمعاملات بشكل منفصل، وتلك التي تقبل هيكل معاملة واحد. في النوع الأول من واجهة برمجة التطبيقات، لا تحتوي جزء "المعاملات" على معلومات حول الوظيفة، حيث يتم تمريرها بشكل منفصل. في النوع الثاني من واجهة برمجة التطبيقات، تكون الوظيفة هي العنصر الأول في هيكل المعاملة.
على سبيل المثال، الدالة التالية لأداء استدعاء تقبل فقط مؤشر args
، لذا سيكون العنصر الأول من مصفوفة args هو الدالة التي سيتم استدعاؤها:
jl_value_t *jl_apply(jl_value_t **args, uint32_t nargs)
تقبل هذه النقطة المدخلية نفس الوظيفة بشكل منفصل، لذا فإن مصفوفة args
لا تحتوي على الوظيفة:
jl_value_t *jl_call(jl_function_t *f, jl_value_t **args, int32_t nargs);
Adding methods
نظرًا لعملية الإرسال المذكورة أعلاه، كل ما هو مطلوب بشكل مفاهيمي لإضافة طريقة جديدة هو (1) نوع مجموعة، و (2) كود لجسم الطريقة. يقوم jl_method_def
بتنفيذ هذه العملية. يتم استدعاء jl_method_table_for
لاستخراج جدول الطريقة المعني من ما سيكون نوع الوسيطة الأولى. هذا أكثر تعقيدًا بكثير من الإجراء المقابل أثناء الإرسال، نظرًا لأن نوع مجموعة الوسائط قد يكون مجردًا. على سبيل المثال، يمكننا تعريف:
(::Union{Foo{Int},Foo{Int8}})(x) = 0
الذي يعمل لأن جميع طرق المطابقة الممكنة ستنتمي إلى نفس جدول الطرق.
Creating generic functions
نظرًا لأن كل كائن يمكن استدعاؤه، فلا حاجة لشيء خاص لإنشاء دالة عامة. لذلك، تقوم jl_new_generic_function
ببساطة بإنشاء نوع فرعي جديد (حجم 0) من Function
وتعيد مثاله. يمكن أن تحتوي الدالة على "اسم عرض" mnemonics يُستخدم في معلومات التصحيح وعند طباعة الكائنات. على سبيل المثال، اسم Base.sin
هو sin
. وفقًا للتقاليد، يكون اسم النوع الذي تم إنشاؤه هو نفسه اسم الدالة، مع إضافة #
في البداية. لذا، فإن typeof(sin)
هو Base.#sin
.
Closures
إغلاق هو ببساطة كائن قابل للاستدعاء مع أسماء الحقول التي تتوافق مع المتغيرات الملتقطة. على سبيل المثال، الكود التالي:
function adder(x)
return y->x+y
end
يتم خفضه إلى (تقريبًا):
struct ##1{T}
x::T
end
(_::##1)(y) = _.x + y
function adder(x)
return ##1(x)
end
Constructors
استدعاء المُنشئ هو مجرد استدعاء لنوع. تحتوي جدول الطرق لـ Type
على جميع تعريفات المُنشئ. تشترك جميع الأنواع الفرعية لـ Type
(Type
، UnionAll
، Union
، و DataType
) حاليًا في جدول طرق عبر ترتيب خاص.
Builtins
الدوال "المضمنة"، المعرفة في وحدة Core
، هي:
<: === _abstracttype _apply_iterate _apply_pure _call_in_world
_call_in_world_total _call_latest _compute_sparams _equiv_typedef _expr
_primitivetype _setsuper! _structtype _svec_ref _typebody! _typevar applicable
apply_type compilerbarrier current_scope donotdelete fieldtype finalizer
get_binding_type getfield getglobal ifelse invoke isa isdefined
memoryref_isassigned memoryrefget memoryrefmodify! memoryrefnew memoryrefoffset
memoryrefreplace! memoryrefset! memoryrefsetonce! memoryrefswap! modifyfield!
modifyglobal! nfields replacefield! replaceglobal! set_binding_type! setfield!
setfieldonce! setglobal! setglobalonce! sizeof svec swapfield! swapglobal! throw
tuple typeassert typeof
هذه هي جميع الكائنات الفردية التي تكون أنواعها من الأنواع الفرعية لـ Builtin
، والتي هي نوع فرعي من Function
. الغرض منها هو كشف نقاط الدخول في وقت التشغيل التي تستخدم اتفاقية استدعاء "jlcall":
jl_value_t *(jl_value_t*, jl_value_t**, uint32_t)
جداول الطرق الخاصة بالـ builtins فارغة. بدلاً من ذلك، لديهم إدخال واحد شامل لطريقة التخزين المؤقت (Tuple{Vararg{Any}}
) الذي يشير مؤشر الدالة jlcall fptr
إلى الدالة الصحيحة. هذه نوع من الحيلة لكنها تعمل بشكل معقول.
Keyword arguments
تعمل الوسائط الرئيسية عن طريق إضافة طرق إلى دالة kwcall. عادةً ما تكون هذه الدالة "منظم الوسائط الرئيسية" أو "منظم الكلمات الرئيسية"، والتي تستدعي بعد ذلك جسم الدالة الداخلية (المعرفة بشكل مجهول). كل تعريف في دالة kwsorter له نفس الوسائط مثل بعض التعريفات في جدول الطرق العادية، باستثناء أنه يتم إضافة وسيط NamedTuple
واحد في البداية، والذي يعطي أسماء وقيم الوسائط الرئيسية المرسلة. وظيفة kwsorter هي نقل الوسائط الرئيسية إلى مواقعها القياسية بناءً على الاسم، بالإضافة إلى تقييم واستبدال أي تعبيرات قيمة افتراضية مطلوبة. النتيجة هي قائمة وسائط موضعية عادية، والتي يتم تمريرها بعد ذلك إلى دالة أخرى تم إنشاؤها بواسطة المترجم.
أسهل طريقة لفهم العملية هي النظر إلى كيفية تقليل تعريف طريقة مع وسيط كلمة رئيسية. الكود:
function circle(center, radius; color = black, fill::Bool = true, options...)
# draw
end
في الواقع ينتج ثلاث تعريفات للطرق. الأول هو دالة تقبل جميع الوسائط (بما في ذلك الوسائط الرئيسية) كوسائط موضعية، ويتضمن الشيفرة لجسم الطريقة. لديها اسم تم إنشاؤه تلقائيًا:
function #circle#1(color, fill::Bool, options, circle, center, radius)
# draw
end
الطريقة الثانية هي تعريف عادي لدالة circle
الأصلية، والتي تتعامل مع الحالة التي لا يتم فيها تمرير أي وسائط مفتاحية:
function circle(center, radius)
#circle#1(black, true, pairs(NamedTuple()), circle, center, radius)
end
هذا ببساطة يرسل إلى الطريقة الأولى، مع تمرير القيم الافتراضية. يتم تطبيق pairs
على التوكن المسمى من الوسائط المتبقية لتوفير تكرار زوج المفتاح والقيمة. لاحظ أنه إذا كانت الطريقة لا تقبل وسائط الكلمات الرئيسية المتبقية، فإن هذه الحجة تكون غائبة.
أخيرًا هناك تعريف kwsorter:
function (::Core.kwftype(typeof(circle)))(kws, circle, center, radius)
if haskey(kws, :color)
color = kws.color
else
color = black
end
# etc.
# put remaining kwargs in `options`
options = structdiff(kws, NamedTuple{(:color, :fill)})
# if the method doesn't accept rest keywords, throw an error
# unless `options` is empty
#circle#1(color, fill, pairs(options), circle, center, radius)
end
تقوم الدالة Core.kwftype(t)
بإنشاء الحقل t.name.mt.kwsorter
(إذا لم يتم إنشاؤه بعد)، وتعيد نوع تلك الدالة.
هذا التصميم يتميز بأن مواقع الاستدعاء التي لا تستخدم وسائط الكلمات الرئيسية لا تتطلب أي معالجة خاصة؛ كل شيء يعمل كما لو لم تكن جزءًا من اللغة على الإطلاق. مواقع الاستدعاء التي تستخدم وسائط الكلمات الرئيسية يتم توجيهها مباشرة إلى kwsorter للدالة المستدعاة. على سبيل المثال، الاستدعاء:
circle((0, 0), 1.0, color = red; other...)
يتم خفضه إلى:
kwcall(merge((color = red,), other), circle, (0, 0), 1.0)
kwcall
(أيضًا في Core
) يدل على توقيع kwcall
والتوزيع. عملية تفكيك الكلمات الرئيسية (المكتوبة كـ other...
) تستدعي دالة merge
للـ named tuple. تقوم هذه الدالة بتفكيك كل عنصر من other
، متوقعةً أن يحتوي كل واحد على قيمتين (رمز وقيمة). من الطبيعي أن يكون هناك تنفيذ أكثر كفاءة إذا كانت جميع الوسائط المفككة هي named tuples. لاحظ أن دالة circle
الأصلية تمرر، للتعامل مع الإغلاقات.
Compiler efficiency issues
إن إنشاء نوع جديد لكل دالة له عواقب خطيرة محتملة على استخدام موارد المترجم عند دمجه مع تصميم جوليا "التخصص على جميع المعاملات بشكل افتراضي". في الواقع، عانت التنفيذ الأولي لهذا التصميم من أوقات بناء واختبار أطول بكثير، واستخدام أعلى للذاكرة، وصورة نظام أكبر تقريبًا بمقدار 2x من الأساس. في تنفيذ ساذج، كانت المشكلة سيئة بما يكفي لجعل النظام غير قابل للاستخدام تقريبًا. كانت هناك حاجة إلى العديد من التحسينات الكبيرة لجعل التصميم عمليًا.
المشكلة الأولى هي التخصص المفرط للدوال لقيم مختلفة من المعاملات ذات القيم الوظيفية. العديد من الدوال ببساطة "تمرر" معاملًا إلى مكان آخر، مثل دالة أخرى أو إلى موقع تخزين. مثل هذه الدوال لا تحتاج إلى أن تكون متخصصة لكل إغلاق قد يتم تمريره. لحسن الحظ، من السهل تمييز هذه الحالة من خلال النظر ببساطة في ما إذا كانت الدالة تستدعي أحد معاملاتها (أي أن المعامل يظهر في "موضع الرأس" في مكان ما). الدوال ذات الأهمية الحرجة للأداء مثل map
تستدعي بالتأكيد دالة معاملها، وبالتالي ستظل متخصصة كما هو متوقع. يتم تنفيذ هذا التحسين من خلال تسجيل المعاملات التي يتم استدعاؤها خلال تمرير analyze-variables
في الواجهة الأمامية. عندما ترى cache_method
معاملًا في تسلسل نوع Function
تم تمريره إلى فتحة تم إعلانها كـ Any
أو Function
، فإنها تتصرف كما لو تم تطبيق توضيح @nospecialize
. يبدو أن هذه القاعدة التجريبية فعالة للغاية في الممارسة العملية.
المسألة التالية تتعلق بهيكل جداول تجزئة ذاكرة التخزين المؤقت للطريقة. تظهر الدراسات التجريبية أن الغالبية العظمى من الاستدعاءات الديناميكية تتضمن واحدًا أو اثنين من المعاملات. بدورها، يمكن حل العديد من هذه الحالات من خلال النظر فقط في المعامل الأول. (ملاحظة: لن يتفاجأ مؤيدو الاستدعاء الفردي بذلك على الإطلاق. ومع ذلك، تعني هذه الحجة "أن الاستدعاء المتعدد سهل التحسين في الممارسة العملية"، وأنه يجب علينا بالتالي استخدامه، وليس "يجب علينا استخدام الاستدعاء الفردي"!) لذا تستخدم ذاكرة التخزين المؤقت للطريقة نوع المعامل الأول كمفتاحها الأساسي. لاحظ، مع ذلك، أن هذا يتوافق مع العنصر الثاني من نوع التوابل لاستدعاء الدالة (حيث يكون العنصر الأول هو نوع الدالة نفسها). عادةً، يكون تنوع النوع في موضع الرأس منخفضًا للغاية - في الواقع، تنتمي الغالبية العظمى من الدوال إلى أنواع فردية بدون معاملات. ومع ذلك، فإن هذا ليس هو الحال بالنسبة للبانيين، حيث تحتفظ جدول طرق البانيين بطرق لكل نوع. لذلك، يتم تخصيص جدول طرق Type
لاستخدام العنصر الأول من نوع التوابل بدلاً من الثاني.
الواجهة الأمامية تولد إعلانات الأنواع لجميع الإغلاقات. في البداية، تم تنفيذ ذلك عن طريق توليد إعلانات أنواع عادية. ومع ذلك، أدى ذلك إلى إنتاج عدد كبير جدًا من المُنشئين، جميعها كانت تافهة (تقوم ببساطة بتمرير جميع الوسائط إلى new
). نظرًا لأن الطرق مرتبة جزئيًا، فإن إدراج كل هذه الطرق هو O(n²)، بالإضافة إلى أنه يوجد عدد كبير جدًا منها للاحتفاظ به. تم تحسين ذلك عن طريق توليد تعبيرات struct_type
مباشرة (تجاوز توليد المُنشئ الافتراضي)، واستخدام new
مباشرة لإنشاء مثيلات الإغلاق. ليست أجمل شيء على الإطلاق، لكن عليك أن تفعل ما عليك فعله.
كانت المشكلة التالية هي ماكرو @test
، الذي أنشأ إغلاقًا بدون وسائط لكل حالة اختبار. هذا ليس ضروريًا حقًا، حيث يتم تشغيل كل حالة اختبار مرة واحدة فقط في مكانها. لذلك، تم تعديل @test
ليتوسع إلى كتلة try-catch تسجل نتيجة الاختبار (صحيح، خطأ، أو استثناء تم رفعه) وتستدعي معالج مجموعة الاختبار عليها.