Functions

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

julia> function f(x, y)
           x + y
       end
f (generic function with 1 method)

تقبل هذه الدالة وسيطين x و y وتعيد قيمة آخر تعبير تم تقييمه، وهو x + y.

هناك صيغة ثانية أكثر اختصارًا لتعريف دالة في جوليا. صيغة إعلان الدالة التقليدية الموضحة أعلاه تعادل الشكل "التعييني" المدمج التالي:

julia> f(x, y) = x + y
f (generic function with 1 method)

In the assignment form, the body of the function must be a single expression, although it can be a compound expression (see Compound Expressions). Short, simple function definitions are common in Julia. The short function syntax is accordingly quite idiomatic, considerably reducing both typing and visual noise.

يتم استدعاء الدالة باستخدام بناء الجملة التقليدي للأقواس:

julia> f(2, 3)
5

بدون أقواس، تشير العبارة f إلى كائن الدالة، ويمكن تمريرها مثل أي قيمة أخرى:

julia> g = f;

julia> g(2, 3)
5

كما هو الحال مع المتغيرات، يمكن أيضًا استخدام Unicode لأسماء الدوال:

julia> ∑(x, y) = x + y
∑ (generic function with 1 method)

julia> ∑(2, 3)
5

Argument Passing Behavior

تتبع معلمات دالة جوليا تقليدًا يُسمى أحيانًا "التمرير بالمشاركة"، مما يعني أنه لا يتم نسخ القيم عند تمريرها إلى الدوال. تعمل معلمات الدالة نفسها كـ ربط متغيرات جديدة (أسماء جديدة يمكن أن تشير إلى القيم)، تمامًا مثل assignments argument_name = argument_value، بحيث تكون الكائنات التي تشير إليها متطابقة مع القيم الممررة. ستكون التعديلات على القيم القابلة للتغيير (مثل Arrays) التي تم إجراؤها داخل دالة مرئية للمتصل. (هذا هو نفس السلوك الموجود في Scheme، ومعظم لغات Lisp، وPython، وRuby، وPerl، من بين لغات ديناميكية أخرى.)

على سبيل المثال، في الدالة

function f(x, y)
    x[1] = 42    # mutates x
    y = 7 + y    # new binding for y, no mutation
    return y
end

العبارة x[1] = 42 تعدل الكائن x، وبالتالي ستكون هذه التغييرات مرئية في المصفوفة المرسلة من قبل المتصل لهذه الحجة. من ناحية أخرى، فإن التعيين y = 7 + y يغير الربط ("الاسم") y للإشارة إلى قيمة جديدة 7 + y، بدلاً من تعديل الكائن الأصلي المشار إليه بواسطة y، وبالتالي لا تغير الحجة المقابلة المرسلة من قبل المتصل. يمكن رؤية ذلك إذا قمنا باستدعاء f(x, y):

julia> a = [4, 5, 6]
3-element Vector{Int64}:
 4
 5
 6

julia> b = 3
3

julia> f(a, b) # returns 7 + b == 10
10

julia> a  # a[1] is changed to 42 by f
3-element Vector{Int64}:
 42
  5
  6

julia> b  # not changed
3

كقاعدة شائعة في جوليا (ليست متطلبًا نحويًا)، فإن مثل هذه الدالة ستكون typically be named f!(x, y) بدلاً من f(x, y)، كتذكير بصري في موقع الاستدعاء بأن على الأقل واحدًا من المعاملات (غالبًا الأول) يتم تغييره.

Shared memory between arguments

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

Argument-type declarations

يمكنك إعلان أنواع وسائط الدالة عن طريق إضافة ::TypeName إلى اسم الوسيط، كما هو معتاد لـ Type Declarations في جوليا. على سبيل المثال، تقوم الدالة التالية بحساب Fibonacci numbers بشكل متكرر:

fib(n::Integer) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)

والمواصفة ::Integer تعني أنه سيكون من الممكن استدعاؤها فقط عندما يكون n من نوع فرعي من نوع abstract Integer.

إعلانات نوع المعاملات عادةً ليس لها تأثير على الأداء: بغض النظر عن أنواع المعاملات (إن وجدت) المعلنة، يقوم جوليا بترجمة نسخة متخصصة من الدالة لأنواع المعاملات الفعلية التي يمررها المتصل. على سبيل المثال، استدعاء fib(1) سيؤدي إلى تجميع نسخة متخصصة من fib محسّنة خصيصًا لمعاملات Int، والتي يتم إعادة استخدامها إذا تم استدعاء fib(7) أو fib(15). (هناك استثناءات نادرة عندما يمكن أن تؤدي إعلانات نوع المعاملات إلى تحفيز تخصصات إضافية للمترجم؛ انظر: Be aware of when Julia avoids specializing.) الأسباب الأكثر شيوعًا لإعلان أنواع المعاملات في جوليا هي، بدلاً من ذلك:

  • Dispatch: كما هو موضح في Methods، يمكنك أن تمتلك إصدارات مختلفة ("طرق") لدالة لأنواع مختلفة من المعاملات، وفي هذه الحالة تُستخدم أنواع المعاملات لتحديد أي تنفيذ يتم استدعاؤه لأي معاملات. على سبيل المثال، قد تقوم بتنفيذ خوارزمية مختلفة تمامًا fib(x::Number) = ... تعمل مع أي نوع من Number عن طريق استخدام Binet's formula لتوسيعها إلى قيم غير صحيحة.
  • الصحة: يمكن أن تكون إعلانات الأنواع مفيدة إذا كانت دالتك تعيد نتائج صحيحة فقط لأنواع معينة من الوسائط. على سبيل المثال، إذا قمنا بإغفال أنواع الوسائط وكتبنا fib(n) = n ≤ 2 ? one(n) : fib(n-1) + fib(n-2)، فإن fib(1.5) ستعطينا بهدوء الإجابة غير المنطقية 1.0.
  • الوضوح: يمكن أن تعمل إعلانات الأنواع كنوع من الوثائق حول المعاملات المتوقعة.

ومع ذلك، فإنها خطأ شائع أن تقيد أنواع المعاملات بشكل مفرط، مما يمكن أن يحد بشكل غير ضروري من قابلية استخدام الدالة ويمنع إعادة استخدامها في ظروف لم تتوقعها. على سبيل المثال، تعمل دالة fib(n::Integer) أعلاه بشكل جيد على حد سواء مع معاملات Int (الأعداد الصحيحة الآلية) و BigInt (الأعداد الصحيحة ذات الدقة العشوائية) (انظر BigFloats and BigInts)، وهو أمر مفيد بشكل خاص لأن أعداد فيبوناتشي تنمو بسرعة كبيرة وستتجاوز بسرعة أي نوع ثابت الدقة مثل Int (انظر Overflow behavior). إذا كنا قد أعلنّا عن دالتنا كـ fib(n::Int)، فإن التطبيق على BigInt كان سيمنع دون سبب. بشكل عام، يجب عليك استخدام أكثر الأنواع المجردة العامة القابلة للتطبيق للمعاملات، و عند الشك، قم بإغفال أنواع المعاملات. يمكنك دائمًا إضافة مواصفات نوع المعاملات لاحقًا إذا أصبحت ضرورية، ولن تضحي بالأداء أو الوظائف من خلال إغفالها.

The return Keyword

القيمة التي تُرجعها الدالة هي قيمة آخر تعبير تم تقييمه، والذي، بشكل افتراضي، هو آخر تعبير في جسم تعريف الدالة. في دالة المثال، f، من القسم السابق، هذه هي قيمة تعبير x + y. كبديل، كما هو الحال في العديد من اللغات الأخرى، فإن الكلمة الرئيسية return تجعل الدالة تُرجع على الفور، موفرة تعبيرًا تُرجع قيمته:

function g(x, y)
    return x * y
    x + y
end

نظرًا لأن تعريفات الدوال يمكن إدخالها في الجلسات التفاعلية، فمن السهل مقارنة هذه التعريفات:

julia> f(x, y) = x + y
f (generic function with 1 method)

julia> function g(x, y)
           return x * y
           x + y
       end
g (generic function with 1 method)

julia> f(2, 3)
5

julia> g(2, 3)
6

بالطبع، في جسم دالة خطية بحتة مثل g، فإن استخدام return غير ذي جدوى حيث إن التعبير x + y لا يتم تقييمه أبداً ويمكننا ببساطة جعل x * y هو التعبير الأخير في الدالة وتجاهل return. ومع ذلك، في conjunction مع تدفق التحكم الآخر، فإن return له استخدام حقيقي. هنا، على سبيل المثال، دالة تحسب طول الوتر مثلث قائم الزاوية بأضلاع بطول x و y، متجنبة overflow:

julia> function hypot(x, y)
           x = abs(x)
           y = abs(y)
           if x > y
               r = y/x
               return x*sqrt(1 + r*r)
           end
           if y == 0
               return zero(x)
           end
           r = x/y
           return y*sqrt(1 + r*r)
       end
hypot (generic function with 1 method)

julia> hypot(3, 4)
5.0

هناك ثلاث نقاط محتملة للعودة من هذه الدالة، تعيد قيم ثلاث تعبيرات مختلفة، اعتمادًا على قيم x و y. يمكن حذف return في السطر الأخير لأنه هو التعبير الأخير.

Return type

يمكن تحديد نوع الإرجاع في إعلان الدالة باستخدام عامل ::. هذا يحول قيمة الإرجاع إلى النوع المحدد.

julia> function g(x, y)::Int8
           return x * y
       end;

julia> typeof(g(1, 2))
Int8

سترجع هذه الدالة دائمًا Int8 بغض النظر عن أنواع x و y. راجع Type Declarations لمزيد من المعلومات حول أنواع الإرجاع.

إعلانات نوع الإرجاع نادرة الاستخدام في جوليا: بشكل عام، يجب عليك بدلاً من ذلك كتابة دوال "مستقرة من حيث النوع" حيث يمكن لمترجم جوليا استنتاج نوع الإرجاع تلقائيًا. لمزيد من المعلومات، راجع الفصل Performance Tips.

Returning nothing

بالنسبة للدوال التي لا تحتاج إلى إرجاع قيمة (الدوال المستخدمة فقط لبعض الآثار الجانبية)، فإن قاعدة جوليا هي إرجاع القيمة nothing:

function printx(x)
    println("x = $x")
    return nothing
end

هذا هو الاتفاق بمعنى أن nothing ليست كلمة مفتاحية في جوليا ولكنها مجرد كائن فردي من نوع Nothing. أيضًا، قد تلاحظ أن مثال دالة printx أعلاه مصطنع، لأن println بالفعل تعيد nothing، لذا فإن سطر return غير ضروري.

هناك شكلان مختصران ممكنان لتعبير return nothing. من ناحية، فإن الكلمة الرئيسية return تعيد بشكل ضمني nothing، لذا يمكن استخدامها بمفردها. من ناحية أخرى، بما أن الدوال تعيد بشكل ضمني آخر تعبير تم تقييمه، يمكن استخدام nothing بمفردها عندما تكون آخر تعبير. تفضيل التعبير return nothing بدلاً من return أو nothing بمفردها هو مسألة أسلوب برمجة.

Operators Are Functions

في جوليا، معظم العوامل هي مجرد دوال تدعم بناءً خاصًا. (الاستثناءات هي العوامل التي لها دلالات تقييم خاصة مثل && و ||. لا يمكن أن تكون هذه العوامل دوال لأن Short-Circuit Evaluation يتطلب أن لا يتم تقييم عملياتها قبل تقييم العامل.) وبناءً عليه، يمكنك أيضًا تطبيقها باستخدام قوائم وسائط محاطة بأقواس، تمامًا كما تفعل مع أي دالة أخرى:

julia> 1 + 2 + 3
6

julia> +(1, 2, 3)
6

الصيغة الداخلية تعادل تمامًا صيغة تطبيق الدالة - في الواقع، يتم تحليل الأولى لإنتاج استدعاء الدالة داخليًا. وهذا يعني أيضًا أنه يمكنك تعيين وتمرير المشغلين مثل + و * تمامًا كما تفعل مع قيم الدالة الأخرى:

julia> f = +;

julia> f(1, 2, 3)
6

تحت الاسم f، لا تدعم الدالة التدوين الوسيط، ومع ذلك.

Operators With Special Names

بعض التعبيرات الخاصة تتوافق مع استدعاءات لدوال بأسماء غير واضحة. هذه هي:

ExpressionCalls
[A B C ...]hcat
[A; B; C; ...]vcat
[A B; C D; ...]hvcat
[A; B;; C; D;; ...]hvncat
A'adjoint
A[i]getindex
A[i] = xsetindex!
A.ngetproperty
A.n = xsetproperty!

لاحظ أن التعبيرات المشابهة لـ [A; B;; C; D;; ...] ولكن مع أكثر من نقطتين متتاليتين ; تت correspond أيضًا إلى استدعاءات hvncat.

Anonymous Functions

تعمل الدوال في جوليا على first-class objects: يمكن تعيينها إلى متغيرات، واستدعاؤها باستخدام بناء جملة استدعاء الدالة القياسي من المتغير الذي تم تعيينها له. يمكن استخدامها كوسائط، ويمكن إرجاعها كقيم. يمكن أيضًا إنشاؤها بشكل مجهول، دون إعطائها اسمًا، باستخدام أي من هذه البنى:

julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)

julia> function (x)
           x^2 + 2x - 1
       end
#3 (generic function with 1 method)

كل عبارة تنشئ دالة تأخذ وسيطًا واحدًا x وتعيد قيمة كثير الحدود x^2 + 2x - 1 عند تلك القيمة. لاحظ أن النتيجة هي دالة عامة، ولكن باسم تم إنشاؤه بواسطة المترجم بناءً على الترقيم المتتالي.

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

julia> map(round, [1.2, 3.5, 1.7])
3-element Vector{Float64}:
 1.0
 4.0
 2.0

هذا جيد إذا كانت هناك دالة مسماة تؤثر على التحويل موجودة بالفعل لتمريرها كأول وسيط إلى map. ومع ذلك، غالبًا ما لا توجد دالة مسماة جاهزة للاستخدام. في هذه الحالات، يسمح بناء الدالة المجهولة بإنشاء كائن دالة للاستخدام لمرة واحدة بسهولة دون الحاجة إلى اسم:

julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Vector{Int64}:
  2
 14
 -2

يمكن كتابة دالة مجهولة تقبل عدة معطيات باستخدام الصيغة (x,y,z)->2x+y-z.

تعمل إعلانات نوع المعاملات للدوال المجهولة كما هو الحال بالنسبة للدوال المسماة، على سبيل المثال x::Integer->2x. لا يمكن تحديد نوع الإرجاع لدالة مجهولة.

يمكن كتابة دالة مجهولة بدون وسائط على أنها ()->2+2. قد تبدو فكرة الدالة بدون وسائط غريبة، لكنها مفيدة في الحالات التي لا يمكن (أو لا ينبغي) حساب نتيجة مسبقًا. على سبيل المثال، تحتوي جوليا على دالة بدون وسائط time التي تعيد الوقت الحالي بالثواني، وبالتالي seconds = ()->round(Int, time()) هي دالة مجهولة تعيد هذا الوقت مقربًا إلى أقرب عدد صحيح يتم تعيينه للمتغير seconds. في كل مرة يتم فيها استدعاء هذه الدالة المجهولة كـ seconds()، سيتم حساب الوقت الحالي وإعادته.

Tuples

تحتوي جوليا على بنية بيانات مدمجة تُسمى الزوج والتي ترتبط ارتباطًا وثيقًا بمعاملات الدوال وقيم الإرجاع. الزوج هو حاوية بطول ثابت يمكن أن تحتوي على أي قيم، ولكن لا يمكن تعديلها (إنها غير قابلة للتغيير). يتم بناء الأزواج باستخدام الفواصل والأقواس، ويمكن الوصول إليها عبر الفهرسة:

julia> (1, 1+1)
(1, 2)

julia> (1,)
(1,)

julia> x = (0.0, "hello", 6*7)
(0.0, "hello", 42)

julia> x[2]
"hello"

لاحظ أن التوكن ذو الطول 1 يجب أن يُكتب مع فاصلة، (1,)، لأن (1) سيكون مجرد قيمة محاطة بأقواس. () تمثل التوكن الفارغ (ذو الطول 0).

Named Tuples

يمكن أن تكون مكونات التوابل مسماة اختيارياً، وفي هذه الحالة يتم إنشاء توبل مسمى:

julia> x = (a=2, b=1+2)
(a = 2, b = 3)

julia> x[1]
2

julia> x.a
2

يمكن الوصول إلى حقول التوائم المسماة بالاسم باستخدام بناء جملة النقطة (x.a) بالإضافة إلى بناء جملة الفهرسة العادية (x[1] أو x[:a]).

Destructuring Assignment and Multiple Return Values

يمكن أن تظهر قائمة من المتغيرات مفصولة بفواصل (يمكن أن تكون محاطة بأقواس اختيارية) على الجانب الأيسر من عملية الإسناد: يتم تفكيك القيمة على الجانب الأيمن من خلال التكرار والإسناد إلى كل متغير بالتتابع:

julia> (a, b, c) = 1:3
1:3

julia> b
2

يجب أن تكون القيمة على اليمين مكررًا (انظر Iteration interface) على الأقل بطول عدد المتغيرات على اليسار (يتم تجاهل أي عناصر زائدة من المكرر).

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

julia> function foo(a, b)
           a+b, a*b
       end
foo (generic function with 1 method)

إذا قمت باستدعائه في جلسة تفاعلية دون تعيين قيمة الإرجاع في أي مكان، سترى الزوج المعاد:

julia> foo(2, 3)
(5, 6)

تعيين التفكيك يستخرج كل قيمة إلى متغير:

julia> x, y = foo(2, 3)
(5, 6)

julia> x
5

julia> y
6

استخدام شائع آخر هو لتبديل المتغيرات:

julia> y, x = x, y
(5, 6)

julia> x
6

julia> y
5

إذا كان مطلوبًا فقط مجموعة فرعية من عناصر المكرر، فإن قاعدة شائعة هي تعيين العناصر المهملة إلى متغير يتكون فقط من شرطات سفلية _ (وهو اسم متغير غير صالح بخلاف ذلك، انظر Allowed Variable Names):

julia> _, _, _, d = 1:10
1:10

julia> d
4

يمكن استخدام تعبيرات صالحة أخرى على الجانب الأيسر كعناصر في قائمة التعيين، والتي ستستدعي setindex! أو setproperty!، أو تفكيك العناصر الفردية من المكرر بشكل متكرر:

julia> X = zeros(3);

julia> X[1], (a, b) = (1, (2, 3))
(1, (2, 3))

julia> X
3-element Vector{Float64}:
 1.0
 0.0
 0.0

julia> a
2

julia> b
3
Julia 1.6

... مع التعيين يتطلب جوليا 1.6

إذا كان الرمز الأخير في قائمة التعيين ملحقًا بـ ... (المعروف باسم السحب)، فسيتم تعيينه لمجموعة أو مُكرر كسول من العناصر المتبقية من مُكرر الجانب الأيمن:

julia> a, b... = "hello"
"hello"

julia> a
'h': ASCII/Unicode U+0068 (category Ll: Letter, lowercase)

julia> b
"ello"

julia> a, b... = Iterators.map(abs2, 1:4)
Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4)

julia> a
1

julia> b
Base.Iterators.Rest{Base.Generator{UnitRange{Int64}, typeof(abs2)}, Int64}(Base.Generator{UnitRange{Int64}, typeof(abs2)}(abs2, 1:4), 1)

انظر Base.rest للحصول على تفاصيل حول المعالجة الدقيقة والتخصيص لمكررات معينة.

Julia 1.9

... في موضع غير نهائي من تعيين يتطلب جوليا 1.9

يمكن أن يحدث السحب في المهام أيضًا في أي موضع آخر. على عكس سحب نهاية مجموعة، سيكون هذا دائمًا متعجلاً.

julia> a, b..., c = 1:5
1:5

julia> a
1

julia> b
3-element Vector{Int64}:
 2
 3
 4

julia> c
5

julia> front..., tail = "Hi!"
"Hi!"

julia> front
"Hi"

julia> tail
'!': ASCII/Unicode U+0021 (category Po: Punctuation, other)

هذا يتم تنفيذه من حيث الدالة Base.split_rest.

لاحظ أنه بالنسبة لتعريفات الدوال المتغيرة، فإن استخدام السلوبينغ مسموح به فقط في الموضع النهائي. هذا لا ينطبق على single argument destructuring على الرغم من ذلك، حيث أن ذلك لا يؤثر على توجيه الأساليب:

julia> f(x..., y) = x
ERROR: syntax: invalid "..." on non-final argument
Stacktrace:
[...]

julia> f((x..., y)) = x
f (generic function with 1 method)

julia> f((1, 2, 3))
(1, 2)

Property destructuring

بدلاً من تفكيك القيم بناءً على التكرار، يمكن أيضًا تفكيك الجانب الأيمن من التعيينات باستخدام أسماء الخصائص. يتبع هذا بناء الجملة لـ NamedTuples، ويعمل عن طريق تعيين كل متغير على اليسار إلى خاصية من الجانب الأيمن من التعيين بنفس الاسم باستخدام getproperty:

julia> (; b, a) = (a=1, b=2, c=3)
(a = 1, b = 2, c = 3)

julia> a
1

julia> b
2

Argument destructuring

يمكن أيضًا استخدام ميزة التفكيك داخل وسيطة دالة. إذا تم كتابة اسم وسيطة الدالة كزوج (مثل (x, y)) بدلاً من مجرد رمز، فسيتم إدراج تعيين (x, y) = argument لك:

julia> minmax(x, y) = (y < x) ? (y, x) : (x, y)

julia> gap((min, max)) = max - min

julia> gap(minmax(10, 2))
8

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

بالمثل، يمكن أيضًا استخدام تفكيك الخصائص كوسيلة لتمرير المعاملات إلى الدوال:

julia> foo((; x, y)) = x + y
foo (generic function with 1 method)

julia> foo((x=1, y=2))
3

julia> struct A
           x
           y
       end

julia> foo(A(3, 4))
7

بالنسبة للدوال المجهولة، يتطلب تفكيك حجة واحدة فاصلاً إضافياً:

julia> map(((x, y),) -> x + y, [(1, 2), (3, 4)])
2-element Array{Int64,1}:
 3
 7

Varargs Functions

من الملائم غالبًا أن تكون قادرًا على كتابة دوال تأخذ عددًا غير محدد من الوسائط. تُعرف هذه الدوال تقليديًا باسم "دوال varargs"، وهو اختصار لـ "عدد متغير من الوسائط". يمكنك تعريف دالة varargs من خلال متابعة آخر وسيط موضعي مع ثلاث نقاط:

julia> bar(a, b, x...) = (a, b, x)
bar (generic function with 1 method)

تكون المتغيرات a و b مرتبطة بقيم الوسيطتين الأوليين كما هو معتاد، ويتم ربط المتغير x بمجموعة قابلة للتكرار من القيم التي تم تمريرها إلى bar بعد الوسيطتين الأوليين:

julia> bar(1, 2)
(1, 2, ())

julia> bar(1, 2, 3)
(1, 2, (3,))

julia> bar(1, 2, 3, 4)
(1, 2, (3, 4))

julia> bar(1, 2, 3, 4, 5, 6)
(1, 2, (3, 4, 5, 6))

في جميع هذه الحالات، يتم ربط x بزوج من القيم المتبقية المرسلة إلى bar.

من الممكن تقييد عدد القيم المرسلة كوسيط متغير؛ سيتم مناقشة ذلك لاحقًا في Parametrically-constrained Varargs methods.

من الجانب الآخر، من المفيد غالبًا "تفريغ" القيم الموجودة في مجموعة قابلة للتكرار إلى استدعاء دالة كوسائط فردية. للقيام بذلك، يستخدم المرء أيضًا ... ولكن في استدعاء الدالة بدلاً من ذلك:

julia> x = (3, 4)
(3, 4)

julia> bar(1, 2, x...)
(1, 2, (3, 4))

في هذه الحالة، يتم دمج مجموعة من القيم في استدعاء varargs بالضبط حيث تذهب عدد المتغيرات من الوسائط. ومع ذلك، لا يجب أن تكون هذه هي الحالة:

julia> x = (2, 3, 4)
(2, 3, 4)

julia> bar(1, x...)
(1, 2, (3, 4))

julia> x = (1, 2, 3, 4)
(1, 2, 3, 4)

julia> bar(x...)
(1, 2, (3, 4))

علاوة على ذلك، لا يجب أن يكون الكائن القابل للتكرار الذي يتم تفكيكه في استدعاء الدالة عبارة عن مجموعة:

julia> x = [3, 4]
2-element Vector{Int64}:
 3
 4

julia> bar(1, 2, x...)
(1, 2, (3, 4))

julia> x = [1, 2, 3, 4]
4-element Vector{Int64}:
 1
 2
 3
 4

julia> bar(x...)
(1, 2, (3, 4))

أيضًا، لا تحتاج الدالة التي يتم تمرير المعاملات إليها إلى أن تكون دالة varargs (على الرغم من أنها غالبًا ما تكون كذلك):

julia> baz(a, b) = a + b;

julia> args = [1, 2]
2-element Vector{Int64}:
 1
 2

julia> baz(args...)
3

julia> args = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
The function `baz` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  baz(::Any, ::Any)
   @ Main none:1

Stacktrace:
[...]

كما ترى، إذا كان هناك عدد خاطئ من العناصر في الحاوية الموزعة، فسيفشل استدعاء الدالة، تمامًا كما لو تم إعطاء عدد كبير جدًا من الوسائط بشكل صريح.

Optional Arguments

من الممكن غالبًا توفير قيم افتراضية معقولة لوسائط الدالة. يمكن أن يوفر ذلك على المستخدمين عناء تمرير كل وسيط في كل استدعاء. على سبيل المثال، الدالة Date(y, [m, d]) من وحدة Dates تقوم بإنشاء نوع Date لسنة معينة y، شهر m ويوم d. ومع ذلك، فإن وسائط m و d اختيارية وقيمتها الافتراضية هي 1. يمكن التعبير عن هذا السلوك بإيجاز كما يلي:

julia> using Dates

julia> function date(y::Int64, m::Int64=1, d::Int64=1)
           err = Dates.validargs(Date, y, m, d)
           err === nothing || throw(err)
           return Date(Dates.UTD(Dates.totaldays(y, m, d)))
       end
date (generic function with 3 methods)

لاحظ أن هذا التعريف يستدعي طريقة أخرى من دالة Date التي تأخذ وسيطًا واحدًا من نوع UTInstant{Day}.

مع هذا التعريف، يمكن استدعاء الدالة إما بوسيط واحد أو اثنين أو ثلاثة، و1 يتم تمريره تلقائيًا عندما يتم تحديد واحد أو اثنان فقط من الوسائط:

julia> date(2000, 12, 12)
2000-12-12

julia> date(2000, 12)
2000-12-01

julia> date(2000)
2000-01-01

تعتبر الوسائط الاختيارية في الواقع مجرد صياغة مريحة لكتابة تعريفات متعددة للطرق بأعداد مختلفة من الوسائط (انظر Note on Optional and keyword Arguments). يمكن التحقق من ذلك من خلال مثال دالة date لدينا عن طريق استدعاء دالة methods:

julia> methods(date)
# 3 methods for generic function "date":
[1] date(y::Int64) in Main at REPL[1]:1
[2] date(y::Int64, m::Int64) in Main at REPL[1]:1
[3] date(y::Int64, m::Int64, d::Int64) in Main at REPL[1]:1

Keyword Arguments

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

على سبيل المثال، اعتبر دالة plot التي ترسم خطًا. قد تحتوي هذه الدالة على العديد من الخيارات، للتحكم في نمط الخط، والعرض، واللون، وما إلى ذلك. إذا كانت تقبل معلمات كلمة، فقد يبدو استدعاء محتمل مثل plot(x, y, width=2)، حيث اخترنا تحديد عرض الخط فقط. لاحظ أن هذا يخدم غرضين. الاستدعاء أسهل في القراءة، حيث يمكننا تسمية معلمة بمعناها. كما أنه يصبح من الممكن تمرير أي مجموعة فرعية من عدد كبير من المعلمات، بأي ترتيب.

تُعرَف الدوال التي تحتوي على معلمات مفتاحية باستخدام فاصلة منقوطة في التوقيع:

function plot(x, y; style="solid", width=1, color="black")
    ###
end

عند استدعاء الدالة، فإن الفاصلة المنقوطة اختيارية: يمكن استدعاء plot(x, y, width=2) أو plot(x, y; width=2)، لكن النمط الأول هو الأكثر شيوعًا. الفاصلة المنقوطة الصريحة مطلوبة فقط لتمرير varargs أو الكلمات الرئيسية المحسوبة كما هو موضح أدناه.

تُقيَّم قيم الافتراضية لوسائط الكلمات الرئيسية فقط عند الضرورة (عندما لا يتم تمرير وسيط كلمة رئيسية مطابق)، وبترتيب من اليسار إلى اليمين. لذلك، قد تشير تعبيرات الافتراضية إلى وسائط الكلمات الرئيسية السابقة.

يمكن توضيح أنواع الوسائط الرئيسية على النحو التالي:

function f(; x::Int=1)
    ###
end

يمكن أيضًا استخدام الوسائط الرئيسية في دوال varargs:

function plot(x...; style="solid")
    ###
end

يمكن جمع كلمات المفتاح الإضافية باستخدام ...، كما في دوال varargs:

function f(x; y=0, kwargs...)
    ###
end

داخل f، ستكون kwargs عبارة عن مُكرر غير قابل للتغيير من نوع المفتاح-القيمة على مجموعة مسماة. يمكن تمرير المجموعات المسماة (بالإضافة إلى القواميس التي تحتوي على مفاتيح من نوع Symbol، وغيرها من المُكررات التي تُنتج مجموعات ذات قيمتين مع الرموز كقيم أولى) كوسائط مفتاحية باستخدام فاصلة منقوطة في استدعاء، على سبيل المثال f(x, z=1; kwargs...).

إذا لم يتم تعيين قيمة افتراضية لحجة الكلمة الرئيسية في تعريف الطريقة، فإنها تكون مطلوبة: سيتم طرح استثناء UndefKeywordError إذا لم يقم المتصل بتعيين قيمة لها:

function f(x; y)
    ###
end
f(3, y=5) # ok, y is assigned
f(3)      # throws UndefKeywordError(:y)

يمكن أيضًا تمرير تعبيرات key => value بعد الفاصلة المنقوطة. على سبيل المثال، plot(x, y; :width => 2) يعادل plot(x, y, width=2). هذا مفيد في الحالات التي يتم فيها حساب اسم الكلمة الرئيسية في وقت التشغيل.

عندما يحدث معرف عاري أو تعبير نقطة بعد فاصلة منقوطة، يتم استنتاج اسم وسيط الكلمة الرئيسية من المعرف أو اسم الحقل. على سبيل المثال plot(x, y; width) يعادل plot(x, y; width=width) و plot(x, y; options.width) يعادل plot(x, y; width=options.width).

تجعل طبيعة الوسائط الرئيسية من الممكن تحديد نفس الوسيط أكثر من مرة. على سبيل المثال، في الاستدعاء plot(x, y; options..., width=2) من الممكن أن يحتوي هيكل options أيضًا على قيمة لـ width. في هذه الحالة، تأخذ الظهور الأيمن الأولوية؛ في هذا المثال، من المؤكد أن width سيكون له القيمة 2. ومع ذلك، فإن تحديد نفس الوسيط الرئيسي عدة مرات بشكل صريح، على سبيل المثال plot(x, y, width=2, width=3)، غير مسموح به ويؤدي إلى خطأ في بناء الجملة.

Evaluation Scope of Default Values

عند تقييم تعبيرات القيم الافتراضية للوسائط الاختيارية ووسائط الكلمات الرئيسية، تكون فقط الوسائط السابقة في النطاق. على سبيل المثال، بالنظر إلى هذا التعريف:

function f(x, a=b, b=1)
    ###
end

الـ b في a=b تشير إلى الـ b في نطاق خارجي، وليس إلى الوسيطة اللاحقة b.

Do-Block Syntax for Function Arguments

تمرير الدوال كوسائط لدوال أخرى هو تقنية قوية، لكن الصياغة لذلك ليست دائمًا مريحة. تكون هذه الاستدعاءات غير مريحة بشكل خاص عند الحاجة إلى عدة أسطر كوسيط للدالة. كمثال، اعتبر استدعاء map على دالة تحتوي على عدة حالات:

map(x->begin
           if x < 0 && iseven(x)
               return 0
           elseif x == 0
               return 1
           else
               return x
           end
       end,
    [A, B, C])

تقدم جوليا كلمة محجوزة do لإعادة كتابة هذا الكود بشكل أكثر وضوحًا:

map([A, B, C]) do x
    if x < 0 && iseven(x)
        return 0
    elseif x == 0
        return 1
    else
        return x
    end
end

تقوم صيغة do x بإنشاء دالة مجهولة مع الوسيط x وتقوم بتمرير الدالة المجهولة كأول وسيط إلى "الدالة الخارجية" - map في هذا المثال. بالمثل، ستقوم do a,b بإنشاء دالة مجهولة ذات وسيطين. لاحظ أن do (a,b) ستقوم بإنشاء دالة مجهولة ذات وسيط واحد، يكون وسيطها عبارة عن مجموعة يجب تفكيكها. ستعلن do عادية أن ما يلي هو دالة مجهولة على شكل () -> ....

تعتمد كيفية تهيئة هذه الحجج على "الدالة الخارجية"؛ هنا، map ستقوم بتعيين x بالتتابع إلى A، B، C، مع استدعاء الدالة المجهولة في كل مرة، تمامًا كما يحدث في بناء الجملة map(func, [A, B, C]).

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

open("outfile", "w") do io
    write(io, data)
end

يتم تحقيق ذلك من خلال التعريف التالي:

function open(f::Function, args...)
    io = open(args...)
    try
        f(io)
    finally
        close(io)
    end
end

هنا، open يفتح الملف للكتابة ثم يمرر تدفق الإخراج الناتج إلى الدالة المجهولة التي قمت بتعريفها في كتلة do ... end. بعد خروج دالتك، سيتأكد 4d61726b646f776e2e436f64652822222c20226f70656e2229_40726566 من أن التدفق مغلق بشكل صحيح، بغض النظر عما إذا كانت دالتك قد خرجت بشكل طبيعي أو أثارت استثناء. (سيتم وصف بناء try/finally في Control Flow.)

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

كتلة do، مثل أي دالة داخلية أخرى، يمكن أن "تلتقط" المتغيرات من نطاقها المحيط. على سبيل المثال، المتغير data في المثال أعلاه لـ open...do يتم التقاطه من النطاق الخارجي. يمكن أن تخلق المتغيرات الملتقطة تحديات في الأداء كما تم مناقشته في performance tips.

Function composition and piping

يمكن دمج الدوال في جوليا عن طريق تركيبها أو توصيلها (سلسلتها) معًا.

تكوين الدوال هو عندما تجمع الدوال معًا وتطبق التركيب الناتج على المعطيات. تستخدم عامل تركيب الدوال () لتكوين الدوال، لذا فإن (f ∘ g)(args...; kw...) هو نفسه f(g(args...; kw...)).

يمكنك كتابة عامل التركيب في REPL والمحررات المهيأة بشكل مناسب باستخدام \circ<tab>.

على سبيل المثال، يمكن دمج دالة sqrt و + بهذه الطريقة:

julia> (sqrt ∘ +)(3, 6)
3.0

هذا يضيف الأرقام أولاً، ثم يجد الجذر التربيعي للنتيجة.

المثال التالي يتكون من ثلاث دوال ويقوم بتطبيق النتيجة على مصفوفة من السلاسل النصية:

julia> map(first ∘ reverse ∘ uppercase, split("you can compose functions like this"))
6-element Vector{Char}:
 'U': ASCII/Unicode U+0055 (category Lu: Letter, uppercase)
 'N': ASCII/Unicode U+004E (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

تسلسل الدوال (الذي يُطلق عليه أحيانًا "الأنابيب" أو "استخدام الأنبوب" لإرسال البيانات إلى دالة لاحقة) هو عندما تقوم بتطبيق دالة على ناتج الدالة السابقة:

julia> 1:10 |> sum |> sqrt
7.416198487095663

هنا، يتم تمرير الناتج الكلي من sum إلى دالة sqrt. التركيب المعادل سيكون:

julia> (sqrt ∘ sum)(1:10)
7.416198487095663

يمكن أيضًا استخدام عامل الأنابيب مع البث، كـ .|>, لتوفير مجموعة مفيدة من سلسلة/أنابيب وبناء جملة التوجيه النقطي (الموصوف أدناه).

julia> ["a", "list", "of", "strings"] .|> [uppercase, reverse, titlecase, length]
4-element Vector{Any}:
  "A"
  "tsil"
  "Of"
 7

عند دمج الأنابيب مع الدوال المجهولة، يجب استخدام الأقواس إذا لم يكن من المقرر تحليل الأنابيب اللاحقة كجزء من جسم الدالة المجهولة. قارن:

julia> 1:3 .|> (x -> x^2) |> sum |> sqrt
3.7416573867739413

julia> 1:3 .|> x -> x^2 |> sum |> sqrt
3-element Vector{Float64}:
 1.0
 2.0
 3.0

Dot Syntax for Vectorizing Functions

في لغات الحوسبة التقنية، من الشائع أن يكون هناك نسخ "موجهة" من الدوال، والتي ببساطة تطبق دالة معينة f(x) على كل عنصر من مصفوفة A لتوليد مصفوفة جديدة عبر f(A). هذه الصيغة مريحة لمعالجة البيانات، ولكن في لغات أخرى، غالبًا ما تكون التوجيهات مطلوبة أيضًا من أجل الأداء: إذا كانت الحلقات بطيئة، يمكن أن تستدعي النسخة "الموجهة" من دالة ما كود مكتبة سريع مكتوب بلغة منخفضة المستوى. في جوليا، لا تتطلب الدوال الموجهة الأداء، وفي الواقع، غالبًا ما يكون من المفيد كتابة حلقاتك الخاصة (انظر Performance Tips)، لكنها لا تزال مريحة. لذلك، يمكن تطبيق أي دالة جوليا f على كل عنصر في أي مصفوفة (أو مجموعة أخرى) باستخدام الصيغة f.(A). على سبيل المثال، يمكن تطبيق sin على جميع العناصر في المتجه A على النحو التالي:

julia> A = [1.0, 2.0, 3.0]
3-element Vector{Float64}:
 1.0
 2.0
 3.0

julia> sin.(A)
3-element Vector{Float64}:
 0.8414709848078965
 0.9092974268256817
 0.1411200080598672

بالطبع، يمكنك حذف النقطة إذا كتبت طريقة "متجه" متخصصة لـ f، على سبيل المثال عبر f(A::AbstractArray) = map(f, A)، وهذا بنفس كفاءة f.(A). الميزة في بناء جملة f.(A) هي أنه لا يحتاج كاتب المكتبة إلى تحديد أي الوظائف قابلة للتطبيق على المتجهات مسبقًا.

بشكل عام، f.(args...) يعادل في الواقع broadcast(f, args...)، مما يتيح لك العمل على مصفوفتين متعددتين (حتى بأشكال مختلفة)، أو مزيج من المصفوفات والسكالار (انظر Broadcasting). على سبيل المثال، إذا كان لديك f(x, y) = 3x + 4y، فإن f.(pi, A) ستعيد مصفوفة جديدة تتكون من f(pi,a) لكل a في A، و f.(vector1, vector2) ستعيد متجهًا جديدًا يتكون من f(vector1[i], vector2[i]) لكل فهرس i (مما يؤدي إلى رمي استثناء إذا كانت المتجهات ذات أطوال مختلفة).

julia> f(x, y) = 3x + 4y;

julia> A = [1.0, 2.0, 3.0];

julia> B = [4.0, 5.0, 6.0];

julia> f.(pi, A)
3-element Vector{Float64}:
 13.42477796076938
 17.42477796076938
 21.42477796076938

julia> f.(A, B)
3-element Vector{Float64}:
 19.0
 26.0
 33.0

لا يتم بث الوسائط الرئيسية، بل يتم تمريرها ببساطة إلى كل استدعاء للدالة. على سبيل المثال، round.(x, digits=3) يعادل broadcast(x -> round(x, digits=3), x).

علاوة على ذلك، يتم دمج استدعاءات f.(args...) المتداخلة في حلقة broadcast واحدة. على سبيل المثال، sin.(cos.(X)) تعادل broadcast(x -> sin(cos(x)), X)، مشابهة لـ [sin(cos(x)) for x in X]: هناك حلقة واحدة فقط على X، ويتم تخصيص مصفوفة واحدة فقط للنتيجة. [على النقيض من ذلك، فإن sin(cos(X)) في لغة "موجهة نحو المتجهات" نموذجية ستخصص أولاً مصفوفة مؤقتة واحدة لـ tmp=cos(X)، ثم تحسب sin(tmp) في حلقة منفصلة، مما يؤدي إلى تخصيص مصفوفة ثانية.] هذا الدمج للحلقات ليس تحسينًا من قبل المترجم قد يحدث أو لا يحدث، بل هو ضمان نحوي كلما تم العثور على استدعاءات f.(args...) المتداخلة. تقنيًا، يتوقف الدمج بمجرد مواجهة استدعاء دالة "غير نقطي"؛ على سبيل المثال، في sin.(sort(cos.(X))) لا يمكن دمج حلقات sin و cos بسبب دالة sort المتداخلة.

أخيرًا، يتم تحقيق الكفاءة القصوى عادةً عندما يتم تخصيص مصفوفة الإخراج لعملية متجهة مسبقًا، بحيث لا تؤدي الاستدعاءات المتكررة إلى تخصيص مصفوفات جديدة مرارًا وتكرارًا للنتائج (انظر Pre-allocating outputs). الصياغة المريحة لذلك هي X .= ...، والتي تعادل broadcast!(identity, X, ...) باستثناء أنه، كما هو مذكور أعلاه، يتم دمج حلقة broadcast! مع أي استدعاءات "نقطة" متداخلة. على سبيل المثال، X .= sin.(Y) تعادل broadcast!(sin, X, Y)، مما يكتب فوق X بـ sin.(Y) في المكان. إذا كان الجانب الأيسر تعبيرًا عن فهرسة المصفوفة، مثل X[begin+1:end] .= sin.(Y)، فإنه يترجم إلى broadcast! على view، مثل broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y)، بحيث يتم تحديث الجانب الأيسر في المكان.

نظرًا لأن إضافة النقاط إلى العديد من العمليات واستدعاءات الدوال في تعبير ما يمكن أن تكون مملة وتؤدي إلى كود يصعب قراءته، فإن الماكرو @. متوفر لتحويل كل استدعاء دالة، عملية، وتعيين في تعبير ما إلى النسخة "المبنية على النقاط".

julia> Y = [1.0, 2.0, 3.0, 4.0];

julia> X = similar(Y); # pre-allocate output array

julia> @. X = sin(cos(Y)) # equivalent to X .= sin.(cos.(Y))
4-element Vector{Float64}:
  0.5143952585235492
 -0.4042391538522658
 -0.8360218615377305
 -0.6080830096407656

تُعالج العمليات الثنائية (أو الأحادية) مثل .+ بنفس الآلية: فهي تعادل استدعاءات broadcast وتندمج مع استدعاءات "نقطة" متداخلة أخرى. X .+= Y وما إلى ذلك تعادل X .= X .+ Y وتؤدي إلى تعيين مدمج في المكان؛ انظر أيضًا dot operators.

يمكنك أيضًا دمج عمليات النقاط مع سلسلة الوظائف باستخدام |>، كما في هذا المثال:

julia> 1:5 .|> [x->x^2, inv, x->2*x, -, isodd]
5-element Vector{Real}:
    1
    0.5
    6
   -4
 true

تُستدعى جميع الدوال في البث المدمج دائمًا لكل عنصر من عناصر النتيجة. وبالتالي، X .+ σ .* randn.() ستضيف قناعًا من القيم العشوائية المستقلة والمتطابقة التوزيع إلى كل عنصر من مصفوفة X، ولكن X .+ σ .* randn() ستضيف نفس العينة العشوائية إلى كل عنصر. في الحالات التي يكون فيها الحساب المدمج ثابتًا على محور أو أكثر من محاور تكرار البث، قد يكون من الممكن الاستفادة من مقايضة بين المساحة والوقت وتخصيص قيم وسيطة لتقليل عدد العمليات الحسابية. لمزيد من المعلومات، انظر إلى performance tips.

Further Reading

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