Methods

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

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

اختيار الطريقة التي يجب تنفيذها عند تطبيق دالة يسمى التوزيع. يسمح جوليا لعملية التوزيع باختيار أي من طرق الدالة يجب استدعاؤها بناءً على عدد المعطيات المقدمة، وعلى أنواع جميع معطيات الدالة. هذا يختلف عن لغات البرمجة الكائنية التقليدية، حيث يحدث التوزيع بناءً فقط على المعطى الأول، الذي غالبًا ما يحتوي على بناء جملة خاص، وأحيانًا يكون ضمنيًا بدلاً من أن يُكتب صراحة كمعطى. [1] استخدام جميع معطيات الدالة لاختيار الطريقة التي يجب استدعاؤها، بدلاً من مجرد المعطى الأول، يُعرف باسم multiple dispatch. التوزيع المتعدد مفيد بشكل خاص للشفرة الرياضية، حيث لا معنى لتصنيف العمليات بشكل مصطنع على أنها "تنتمي" إلى معطى واحد أكثر من غيره: هل تنتمي عملية الجمع في x + y إلى x أكثر مما تنتمي إلى y؟ تعتمد تنفيذ مشغل رياضي عمومًا على أنواع جميع معطياته. حتى خارج العمليات الرياضية، ومع ذلك، ينتهي الأمر بالتوزيع المتعدد ليكون نموذجًا قويًا ومناسبًا لهيكلة وتنظيم البرامج.

Note

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

Defining Methods

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

عند تعريف دالة، يمكن للمرء اختيارياً تقييد أنواع المعلمات التي تنطبق عليها، باستخدام عامل تأكيد النوع ::، الذي تم تقديمه في القسم الخاص بـ Composite Types:

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

تنطبق تعريف هذه الدالة فقط على الاستدعاءات حيث تكون x و y كلاهما قيم من نوع Float64:

julia> f(2.0, 3.0)
7.0

تطبيقه على أي نوع آخر من الحجج سيؤدي إلى MethodError:

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

Closest candidates are:
  f(::Float64, !Matched::Float64)
   @ Main none:1

Stacktrace:
[...]

julia> f(Float32(2.0), 3.0)
ERROR: MethodError: no method matching f(::Float32, ::Float64)
The function `f` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  f(!Matched::Float64, ::Float64)
   @ Main none:1

Stacktrace:
[...]

julia> f(2.0, "3.0")
ERROR: MethodError: no method matching f(::Float64, ::String)
The function `f` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  f(::Float64, !Matched::Float64)
   @ Main none:1

Stacktrace:
[...]

julia> f("2.0", "3.0")
ERROR: MethodError: no method matching f(::String, ::String)
The function `f` exists, but no method is defined for this combination of argument types.

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

julia> f(x::Number, y::Number) = 2x - y
f (generic function with 2 methods)

julia> f(2.0, 3)
1.0

تعريف هذه الطريقة ينطبق على أي زوج من المعاملات التي هي مثيلات لـ Number. لا يلزم أن تكون من نفس النوع، طالما أنها كل منها قيم عددية. يتم تفويض مشكلة التعامل مع الأنواع العددية المختلفة إلى العمليات الحسابية في التعبير 2x - y.

لتعريف دالة بطرق متعددة، يقوم المرء ببساطة بتعريف الدالة عدة مرات، مع أعداد وأنواع مختلفة من المعاملات. تعريف الطريقة الأولى لدالة ما ينشئ كائن الدالة، وتعريفات الطرق اللاحقة تضيف طرقًا جديدة إلى كائن الدالة الموجود. سيتم تنفيذ تعريف الطريقة الأكثر تحديدًا الذي يتطابق مع عدد وأنواع المعاملات عند تطبيق الدالة. وبالتالي، فإن تعريفات الطريقتين أعلاه، مجتمعتين، تحدد السلوك لـ f على جميع أزواج من مثيلات النوع المجرد Number - ولكن مع سلوك مختلف محدد لأزواج من قيم Float64. إذا كانت إحدى المعاملات عبارة عن عدد عشري 64 بت ولكن الأخرى ليست كذلك، فلا يمكن استدعاء طريقة f(Float64,Float64) ويجب استخدام الطريقة الأكثر عمومية f(Number,Number):

julia> f(2.0, 3.0)
7.0

julia> f(2, 3.0)
1.0

julia> f(2.0, 3)
1.0

julia> f(2, 3)
1

تعريف 2x + y يُستخدم فقط في الحالة الأولى، بينما تعريف 2x - y يُستخدم في الحالات الأخرى. لا يتم إجراء أي تحويل تلقائي أو تحويل لوسائط الدالة: جميع التحويلات في جوليا غير سحرية تمامًا وصريحة بالكامل. Conversion and Promotion، مع ذلك، يُظهر كيف يمكن أن يكون التطبيق الذكي لتكنولوجيا متقدمة بما فيه الكفاية غير قابل للتمييز عن السحر. [Clarke61]

بالنسبة للقيم غير الرقمية، ولعدد أقل أو أكثر من حجتين، تظل الدالة f غير معرفة، وتطبيقها سيؤدي إلى MethodError:

julia> f("foo", 3)
ERROR: MethodError: no method matching f(::String, ::Int64)
The function `f` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  f(!Matched::Number, ::Number)
   @ Main none:1
  f(!Matched::Float64, !Matched::Float64)
   @ Main none:1

Stacktrace:
[...]

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

Closest candidates are:
  f(!Matched::Float64, !Matched::Float64)
   @ Main none:1
  f(!Matched::Number, !Matched::Number)
   @ Main none:1

Stacktrace:
[...]

يمكنك بسهولة رؤية الطرق الموجودة لوظيفة ما عن طريق إدخال كائن الوظيفة نفسه في جلسة تفاعلية:

julia> f
f (generic function with 2 methods)

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

julia> methods(f)
# 2 methods for generic function "f" from Main:
 [1] f(x::Float64, y::Float64)
     @ none:1
 [2] f(x::Number, y::Number)
     @ none:1

الذي يظهر أن f لديه طريقتان، واحدة تأخذ وسيطين من نوع Float64 والأخرى تأخذ وسائط من نوع Number. كما يشير إلى الملف ورقم السطر حيث تم تعريف الطرق: لأنه تم تعريف هذه الطرق في REPL، نحصل على رقم السطر الظاهر none:1.

في غياب إعلان نوع مع ::، يكون نوع معلمة الطريقة هو Any بشكل افتراضي، مما يعني أنه غير مقيد حيث أن جميع القيم في جوليا هي حالات من النوع المجرد Any. وبالتالي، يمكننا تعريف طريقة شاملة لـ f على النحو التالي:

julia> f(x,y) = println("Whoa there, Nelly.")
f (generic function with 3 methods)

julia> methods(f)
# 3 methods for generic function "f" from Main:
 [1] f(x::Float64, y::Float64)
     @ none:1
 [2] f(x::Number, y::Number)
     @ none:1
 [3] f(x, y)
     @ none:1

julia> f("foo", 1)
Whoa there, Nelly.

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

لاحظ أنه في توقيع الطريقة الثالثة، لا يوجد نوع محدد للوسائط x و y. هذه طريقة مختصرة للتعبير عن f(x::Any, y::Any).

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

julia> methods(+)
# 180 methods for generic function "+":
[1] +(x::Bool, z::Complex{Bool}) in Base at complex.jl:227
[2] +(x::Bool, y::Bool) in Base at bool.jl:89
[3] +(x::Bool) in Base at bool.jl:86
[4] +(x::Bool, y::T) where T<:AbstractFloat in Base at bool.jl:96
[5] +(x::Bool, z::Complex) in Base at complex.jl:234
[6] +(a::Float16, b::Float16) in Base at float.jl:373
[7] +(x::Float32, y::Float32) in Base at float.jl:375
[8] +(x::Float64, y::Float64) in Base at float.jl:376
[9] +(z::Complex{Bool}, x::Bool) in Base at complex.jl:228
[10] +(z::Complex{Bool}, x::Real) in Base at complex.jl:242
[11] +(x::Char, y::Integer) in Base at char.jl:40
[12] +(c::BigInt, x::BigFloat) in Base.MPFR at mpfr.jl:307
[13] +(a::BigInt, b::BigInt, c::BigInt, d::BigInt, e::BigInt) in Base.GMP at gmp.jl:392
[14] +(a::BigInt, b::BigInt, c::BigInt, d::BigInt) in Base.GMP at gmp.jl:391
[15] +(a::BigInt, b::BigInt, c::BigInt) in Base.GMP at gmp.jl:390
[16] +(x::BigInt, y::BigInt) in Base.GMP at gmp.jl:361
[17] +(x::BigInt, c::Union{UInt16, UInt32, UInt64, UInt8}) in Base.GMP at gmp.jl:398
...
[180] +(a, b, c, xs...) in Base at operators.jl:424

تتيح تقنية التوزيع المتعدد مع نظام النوع المعلمي المرن لجوليا القدرة على التعبير بشكل تجريدي عن الخوارزميات عالية المستوى مفصولة عن تفاصيل التنفيذ.

Method specializations

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

هناك نوع آخر من التخصص يحدث دون تدخل المبرمج: يمكن لمترجم جوليا أن يتخصص تلقائيًا في الطريقة لأنواع الوسائط المحددة المستخدمة. مثل هذه التخصصات لا تُدرج بواسطة methods، حيث أن هذا لا ينشئ Methods جديدة، ولكن أدوات مثل @code_typed تتيح لك فحص مثل هذه التخصصات.

على سبيل المثال، إذا قمت بإنشاء طريقة

mysum(x::Real, y::Real) = x + y

لقد أعطيت الدالة mysum طريقة جديدة واحدة (ربما تكون الطريقة الوحيدة لها)، وهذه الطريقة تأخذ أي زوج من مدخلات الأعداد Real. ولكن إذا قمت بعد ذلك بتنفيذ

julia> mysum(1, 2)
3

julia> mysum(1.0, 2.0)
3.0

ستقوم جوليا بترجمة mysum مرتين، مرة لـ x::Int, y::Int ومرة أخرى لـ x::Float64, y::Float64. الهدف من الترجمة مرتين هو الأداء: الطرق التي يتم استدعاؤها لـ + (التي تستخدمها mysum) تختلف اعتمادًا على الأنواع المحددة لـ x و y، ومن خلال تجميع تخصصات مختلفة يمكن لجوليا القيام بكل عمليات البحث عن الطرق مسبقًا. هذا يسمح للبرنامج بالعمل بشكل أسرع بكثير، حيث لا يتعين عليه الانشغال بعمليات البحث عن الطرق أثناء تشغيله. يسمح التخصص التلقائي لجوليا لك بكتابة خوارزميات عامة وتوقع أن يقوم المترجم بإنشاء كود متخصص وفعال للتعامل مع كل حالة تحتاجها.

في الحالات التي قد يكون فيها عدد التخصصات المحتملة غير محدود بشكل فعال، قد تتجنب جوليا هذا التخصص الافتراضي. انظر Be aware of when Julia avoids specializing لمزيد من المعلومات.

Method Ambiguities

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

julia> g(x::Float64, y) = 2x + y
g (generic function with 1 method)

julia> g(x, y::Float64) = x + 2y
g (generic function with 2 methods)

julia> g(2.0, 3)
7.0

julia> g(2, 3.0)
8.0

julia> g(2.0, 3.0)
ERROR: MethodError: g(::Float64, ::Float64) is ambiguous.

Candidates:
  g(x, y::Float64)
    @ Main none:1
  g(x::Float64, y)
    @ Main none:1

Possible fix, define
  g(::Float64, ::Float64)

Stacktrace:
[...]

هنا يمكن التعامل مع الاستدعاء g(2.0, 3.0) بواسطة إما الطريقة g(::Float64, ::Any) أو الطريقة g(::Any, ::Float64). لا تهم ترتيب تعريف الطرق، ولا تعتبر أي منهما أكثر تحديدًا من الأخرى. في مثل هذه الحالات، ترفع جوليا MethodError بدلاً من اختيار طريقة بشكل تعسفي. يمكنك تجنب التباسات الطرق عن طريق تحديد طريقة مناسبة لحالة التقاطع:

julia> g(x::Float64, y::Float64) = 2x + 2y
g (generic function with 3 methods)

julia> g(2.0, 3)
7.0

julia> g(2, 3.0)
8.0

julia> g(2.0, 3.0)
10.0

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

في الحالات الأكثر تعقيدًا، يتضمن حل غموض الأساليب عنصرًا معينًا من التصميم؛ يتم استكشاف هذا الموضوع بشكل أعمق below.

Parametric Methods

يمكن أن تحتوي تعريفات الطرق على معلمات نوع اختيارية تؤهل التوقيع:

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

julia> same_type(x,y) = false
same_type (generic function with 2 methods)

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

julia> same_type(1, 2)
true

julia> same_type(1, 2.0)
false

julia> same_type(1.0, 2.0)
true

julia> same_type("foo", 2.0)
false

julia> same_type("foo", "bar")
true

julia> same_type(Int32(1), Int64(2))
false

تتوافق هذه التعريفات مع الطرق التي تكون توقيعات أنواعها من نوع UnionAll (انظر UnionAll Types).

هذا النوع من تعريف سلوك الدالة عن طريق التوزيع شائع جدًا - حتى أنه يعتبر أسلوبًا شائعًا - في جوليا. لا تقتصر معلمات نوع الطريقة على استخدامها كأنواع للوسائط: يمكن استخدامها في أي مكان حيث تكون القيمة موجودة في توقيع الدالة أو جسم الدالة. إليك مثال حيث يتم استخدام معلمة نوع الطريقة T كمعلمة نوع لنوع بارامتري Vector{T} في توقيع الطريقة:

julia> function myappend(v::Vector{T}, x::T) where {T}
           return [v..., x]
       end
myappend (generic function with 1 method)

معامل النوع T في هذا المثال يضمن أن العنصر المضاف x هو نوع فرعي من نوع العنصر الموجود في المتجه v. الكلمة الرئيسية where تقدم قائمة بتلك القيود بعد تعريف توقيع الطريقة. يعمل هذا بنفس الطريقة لتعريفات السطر الواحد، كما هو موضح أعلاه، ويجب أن تظهر قبل return type declaration، إذا كانت موجودة، كما هو موضح أدناه:

julia> (myappend(v::Vector{T}, x::T)::Vector) where {T} = [v..., x]
myappend (generic function with 1 method)

julia> myappend([1,2,3],4)
4-element Vector{Int64}:
 1
 2
 3
 4

julia> myappend([1,2,3],2.5)
ERROR: MethodError: no method matching myappend(::Vector{Int64}, ::Float64)
The function `myappend` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  myappend(::Vector{T}, !Matched::T) where T
   @ Main none:1

Stacktrace:
[...]

julia> myappend([1.0,2.0,3.0],4.0)
4-element Vector{Float64}:
 1.0
 2.0
 3.0
 4.0

julia> myappend([1.0,2.0,3.0],4)
ERROR: MethodError: no method matching myappend(::Vector{Float64}, ::Int64)
The function `myappend` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  myappend(::Vector{T}, !Matched::T) where T
   @ Main none:1

Stacktrace:
[...]

إذا لم يتطابق نوع العنصر المضاف مع نوع العنصر في المتجه الذي تمت إضافته إليه، يتم رفع MethodError. في المثال التالي، يتم استخدام معلمة نوع الطريقة T كقيمة إرجاع:

julia> mytypeof(x::T) where {T} = T
mytypeof (generic function with 1 method)

julia> mytypeof(1)
Int64

julia> mytypeof(1.0)
Float64

تمامًا كما يمكنك وضع قيود فرعية على معلمات النوع في إعلانات النوع (انظر Parametric Types)، يمكنك أيضًا تقييد معلمات النوع للطرق:

julia> same_type_numeric(x::T, y::T) where {T<:Number} = true
same_type_numeric (generic function with 1 method)

julia> same_type_numeric(x::Number, y::Number) = false
same_type_numeric (generic function with 2 methods)

julia> same_type_numeric(1, 2)
true

julia> same_type_numeric(1, 2.0)
false

julia> same_type_numeric(1.0, 2.0)
true

julia> same_type_numeric("foo", 2.0)
ERROR: MethodError: no method matching same_type_numeric(::String, ::Float64)
The function `same_type_numeric` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  same_type_numeric(!Matched::T, ::T) where T<:Number
   @ Main none:1
  same_type_numeric(!Matched::Number, ::Number)
   @ Main none:1

Stacktrace:
[...]

julia> same_type_numeric("foo", "bar")
ERROR: MethodError: no method matching same_type_numeric(::String, ::String)
The function `same_type_numeric` exists, but no method is defined for this combination of argument types.

julia> same_type_numeric(Int32(1), Int64(2))
false

تعمل دالة same_type_numeric بشكل مشابه لدالة same_type المعرفة أعلاه، لكنها معرفة فقط لزوج من الأرقام.

تسمح الطرق البارامترية بنفس بناء الجملة المستخدم في تعبيرات where لكتابة الأنواع (انظر UnionAll Types). إذا كان هناك معلم واحد فقط، يمكن حذف الأقواس المعقوفة المحيطة (في where {T})، ولكن غالبًا ما يُفضل استخدامها من أجل الوضوح. يمكن فصل المعلمات المتعددة بفواصل، على سبيل المثال where {T, S<:Real}، أو كتابتها باستخدام where متداخلة، على سبيل المثال where S<:Real where T.

Redefining Methods

عند إعادة تعريف طريقة أو إضافة طرق جديدة، من المهم أن ندرك أن هذه التغييرات لا تدخل حيز التنفيذ على الفور. هذه هي النقطة الأساسية في قدرة جوليا على استنتاج الشيفرة بشكل ثابت وتجميعها لتعمل بسرعة، دون الحيل والتكاليف المعتادة في JIT. في الواقع، لن تكون أي تعريفات جديدة للطريقة مرئية لبيئة التشغيل الحالية، بما في ذلك المهام والخيوط (وأي دوال @generated تم تعريفها مسبقًا). دعونا نبدأ بمثال لنرى ماذا يعني هذا:

julia> function tryeval()
           @eval newfun() = 1
           newfun()
       end
tryeval (generic function with 1 method)

julia> tryeval()
ERROR: MethodError: no method matching newfun()
The applicable method may be too new: running in world age xxxx1, while current world is xxxx2.
Closest candidates are:
  newfun() at none:1 (method too new to be called from this world context.)
 in tryeval() at none:1
 ...

julia> newfun()
1

في هذا المثال، لاحظ أن التعريف الجديد لـ newfun قد تم إنشاؤه، لكنه لا يمكن استدعاؤه على الفور. المتغير العالمي الجديد مرئي على الفور لدالة tryeval، لذا يمكنك كتابة return newfun (بدون أقواس). لكن لا يمكنك، ولا يمكن لأي من المتصلين بك، ولا الدوال التي يستدعونها، أو إلخ، استدعاء هذا التعريف الجديد للطريقة!

لكن هناك استثناء: المكالمات المستقبلية إلى newfun من REPL تعمل كما هو متوقع، حيث يمكنها رؤية واستدعاء التعريف الجديد لـ newfun.

ومع ذلك، ستستمر المكالمات المستقبلية إلى tryeval في رؤية تعريف newfun كما كان في العبارة السابقة في REPL، وبالتالي قبل تلك المكالمة إلى tryeval.

قد ترغب في تجربة ذلك بنفسك لترى كيف يعمل.

تنفيذ هذا السلوك هو "عداد عمر العالم". هذه القيمة المتزايدة بشكل أحادي تتعقب كل عملية تعريف طريقة. وهذا يسمح بوصف "مجموعة تعريفات الطرق المرئية لبيئة التشغيل المعطاة" كرقم واحد، أو "عمر العالم". كما يسمح بمقارنة الطرق المتاحة في عالمين فقط من خلال مقارنة قيمتها الترتيبية. في المثال أعلاه، نرى أن "العالم الحالي" (الذي توجد فيه الطريقة newfun) هو أكبر بواحد من "عالم التشغيل المحلي" الذي تم تثبيته عندما بدأت عملية تنفيذ tryeval.

أحيانًا يكون من الضروري تجاوز ذلك (على سبيل المثال، إذا كنت تقوم بتنفيذ REPL المذكور أعلاه). لحسن الحظ، هناك حل سهل: استدعاء الدالة باستخدام Base.invokelatest:

julia> function tryeval2()
           @eval newfun2() = 2
           Base.invokelatest(newfun2)
       end
tryeval2 (generic function with 1 method)

julia> tryeval2()
2

أخيرًا، دعونا نلقي نظرة على بعض الأمثلة الأكثر تعقيدًا حيث تدخل هذه القاعدة حيز التنفيذ. عرّف دالة f(x)، التي تحتوي في البداية على طريقة واحدة:

julia> f(x) = "original definition"
f (generic function with 1 method)

ابدأ بعض العمليات الأخرى التي تستخدم f(x):

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

julia> t = @async f(wait()); yield();

الآن نضيف بعض الطرق الجديدة إلى f(x):

julia> f(x::Int) = "definition for Int"
f (generic function with 2 methods)

julia> f(x::Type{Int}) = "definition for Type{Int}"
f (generic function with 3 methods)

قارن كيف تختلف هذه النتائج:

julia> f(1)
"definition for Int"

julia> g(1)
"definition for Int"

julia> fetch(schedule(t, 1))
"original definition"

julia> t = @async f(wait()); yield();

julia> fetch(schedule(t, 1))
"definition for Int"

Design Patterns with Parametric Methods

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

Extracting the type parameter from a super-type

هنا قالب كود صحيح لإرجاع نوع العنصر T لأي نوع فرعي عشوائي من AbstractArray الذي له نوع عنصر محدد جيدًا:

abstract type AbstractArray{T, N} end
eltype(::Type{<:AbstractArray{T}}) where {T} = T

باستخدام ما يُعرف بتوجيه ثلاثي الأبعاد. لاحظ أن أنواع UnionAll، على سبيل المثال eltype(AbstractArray{T} where T <: Integer)، لا تتطابق مع الطريقة المذكورة أعلاه. تضيف تنفيذ eltype في Base طريقة احتياطية لـ Any في مثل هذه الحالات.

خطأ شائع هو محاولة الحصول على نوع العنصر باستخدام الاستكشاف:

eltype_wrong(::Type{A}) where {A<:AbstractArray} = A.parameters[1]

ومع ذلك، ليس من الصعب بناء حالات حيث سيفشل هذا:

struct BitVector <: AbstractArray{Bool, 1}; end

هنا قمنا بإنشاء نوع BitVector الذي ليس له معلمات، ولكن حيث يتم تحديد نوع العنصر بالكامل، مع T يساوي Bool!

خطأ آخر هو محاولة السير في تسلسل الأنواع باستخدام supertype:

eltype_wrong(::Type{AbstractArray{T}}) where {T} = T
eltype_wrong(::Type{AbstractArray{T, N}}) where {T, N} = T
eltype_wrong(::Type{A}) where {A<:AbstractArray} = eltype_wrong(supertype(A))

بينما يعمل هذا مع الأنواع المعلنة، فإنه يفشل مع الأنواع التي لا تحتوي على أنواع علوية:

julia> eltype_wrong(Union{AbstractArray{Int}, AbstractArray{Float64}})
ERROR: MethodError: no method matching supertype(::Type{Union{AbstractArray{Float64,N} where N, AbstractArray{Int64,N} where N}})
Closest candidates are:
  supertype(::DataType) at operators.jl:43
  supertype(::UnionAll) at operators.jl:48

Building a similar type with a different type parameter

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

تقوم الأنواع الفرعية من AbstractArray عادةً بتنفيذ طريقتين لتحقيق ذلك: طريقة لتحويل مصفوفة الإدخال إلى نوع فرعي من نوع AbstractArray{T, N} المجرد المحدد؛ وطريقة لإنشاء مصفوفة جديدة غير مهيأة بنوع عنصر محدد. يمكن العثور على أمثلة تنفيذية لهذه في قاعدة جوليا. إليك مثال أساسي على استخدامهما، يضمن أن input و output من نفس النوع:

input = convert(AbstractArray{Eltype}, input)
output = similar(input, Eltype)

كامتداد لذلك، في الحالات التي يحتاج فيها الخوارزم إلى نسخة من مصفوفة الإدخال، convert غير كافية حيث قد يكون قيمة الإرجاع تتداخل مع الإدخال الأصلي. الجمع بين similar (لإنشاء مصفوفة الإخراج) و copyto! (لملئها ببيانات الإدخال) هو طريقة عامة للتعبير عن متطلبات نسخة قابلة للتغيير من وسيط الإدخال:

copy_with_eltype(input, Eltype) = copyto!(similar(input, Eltype), input)

Iterated dispatch

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

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

# First dispatch selects the map algorithm for element-wise summation.
+(a::Matrix, b::Matrix) = map(+, a, b)
# Then dispatch handles each element and selects the appropriate
# common element type for the computation.
+(a, b) = +(promote(a, b)...)
# Once the elements have the same type, they can be added.
# For example, via primitive operations exposed by the processor.
+(a::Float64, b::Float64) = Core.add(a, b)

Trait-based dispatch

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

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

المثال في القسم السابق تجاهل تفاصيل التنفيذ لـ map و promote، واللذان يعملان من حيث هذه الصفات. عند التكرار على مصفوفة، مثلما في تنفيذ map، فإن سؤالًا مهمًا هو أي ترتيب يجب استخدامه لتجاوز البيانات. عندما تقوم الأنواع الفرعية لـ AbstractArray بتنفيذ صفة Base.IndexStyle، يمكن لوظائف أخرى مثل map أن تعتمد على هذه المعلومات لاختيار أفضل خوارزمية (انظر Abstract Array Interface). هذا يعني أن كل نوع فرعي لا يحتاج إلى تنفيذ نسخة مخصصة من map، حيث ستتيح التعريفات العامة + فئات الصفات للنظام اختيار النسخة الأسرع. هنا تنفيذ تجريبي لـ map يوضح التوزيع القائم على الصفات:

map(f, a::AbstractArray, b::AbstractArray) = map(Base.IndexStyle(a, b), f, a, b)
# generic implementation:
map(::Base.IndexCartesian, f, a::AbstractArray, b::AbstractArray) = ...
# linear-indexing implementation (faster)
map(::Base.IndexLinear, f, a::AbstractArray, b::AbstractArray) = ...

توجد هذه المقاربة المعتمدة على السمات أيضًا في آلية promote المستخدمة بواسطة السكالار +. تستخدم promote_type، والتي تعيد النوع المشترك الأمثل لحساب العملية بالنظر إلى نوعي المعاملات. وهذا يجعل من الممكن تقليل مشكلة تنفيذ كل دالة لكل زوج من أنواع المعاملات الممكنة، إلى المشكلة الأصغر بكثير المتمثلة في تنفيذ عملية تحويل من كل نوع إلى نوع مشترك، بالإضافة إلى جدول لقواعد الترويج المفضلة بين الأزواج.

Output-type computation

تقدم مناقشة الترويج القائم على الصفات انتقالًا إلى نمط التصميم التالي لدينا: حساب نوع عنصر الإخراج لعملية المصفوفة.

لتنفيذ العمليات الأولية، مثل الجمع، نستخدم دالة promote_type لحساب نوع الإخراج المطلوب. (كما في السابق، رأينا هذا يعمل في استدعاء promote في الاستدعاء لـ +).

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

  1. Write a small function op that expresses the set of operations performed by the kernel of the algorithm.
  2. احسب نوع العنصر R لمصفوفة النتيجة كـ promote_op(op, argument_types...)، حيث يتم حساب argument_types من خلال تطبيق eltype على كل مصفوفة إدخال.
  3. قم ببناء مصفوفة الإخراج كـ similar(R, dims)، حيث dims هي الأبعاد المرغوبة لمصفوفة الإخراج.

كمثال أكثر تحديدًا، قد يبدو رمز pseudo-code لضرب مصفوفة مربعة عامة كما يلي:

function matmul(a::AbstractMatrix, b::AbstractMatrix)
    op = (ai, bi) -> ai * bi + ai * bi

    ## this is insufficient because it assumes `one(eltype(a))` is constructable:
    # R = typeof(op(one(eltype(a)), one(eltype(b))))

    ## this fails because it assumes `a[1]` exists and is representative of all elements of the array
    # R = typeof(op(a[1], b[1]))

    ## this is incorrect because it assumes that `+` calls `promote_type`
    ## but this is not true for some types, such as Bool:
    # R = promote_type(ai, bi)

    # this is wrong, since depending on the return value
    # of type-inference is very brittle (as well as not being optimizable):
    # R = Base.return_types(op, (eltype(a), eltype(b)))

    ## but, finally, this works:
    R = promote_op(op, eltype(a), eltype(b))
    ## although sometimes it may give a larger type than desired
    ## it will always give a correct type

    output = similar(b, R, (size(a, 1), size(b, 2)))
    if size(a, 2) > 0
        for j in 1:size(b, 2)
            for i in 1:size(a, 1)
                ## here we don't use `ab = zero(R)`,
                ## since `R` might be `Any` and `zero(Any)` is not defined
                ## we also must declare `ab::R` to make the type of `ab` constant in the loop,
                ## since it is possible that typeof(a * b) != typeof(a * b + a * b) == R
                ab::R = a[i, 1] * b[1, j]
                for k in 2:size(a, 2)
                    ab += a[i, k] * b[k, j]
                end
                output[i, j] = ab
            end
        end
    end
    return output
end

Separate convert and kernel logic

طريقة واحدة لتقليل أوقات التجميع وتعقيد الاختبار بشكل كبير هي عزل المنطق الخاص بالتحويل إلى النوع المطلوب والحساب. هذا يسمح للمجمع بالتخصص وإدراج منطق التحويل بشكل مستقل عن بقية جسم النواة الأكبر.

هذا نمط شائع يُرى عند التحويل من فئة أكبر من الأنواع إلى نوع الوسيطة المحددة الواحدة التي يدعمها الخوارزم:

complexfunction(arg::Int) = ...
complexfunction(arg::Any) = complexfunction(convert(Int, arg))

matmul(a::T, b::T) = ...
matmul(a, b) = matmul(promote(a, b)...)

Parametrically-constrained Varargs methods

يمكن أيضًا استخدام معلمات الدالة لتقييد عدد الوسائط التي يمكن تقديمها إلى دالة "varargs" (Varargs Functions). يتم استخدام التدوين Vararg{T,N} للإشارة إلى مثل هذا القيد. على سبيل المثال:

julia> bar(a,b,x::Vararg{Any,2}) = (a,b,x)
bar (generic function with 1 method)

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

Closest candidates are:
  bar(::Any, ::Any, ::Any, !Matched::Any)
   @ Main none:1

Stacktrace:
[...]

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

julia> bar(1,2,3,4,5)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64, ::Int64, ::Int64)
The function `bar` exists, but no method is defined for this combination of argument types.

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

Stacktrace:
[...]

بشكل أكثر فائدة، من الممكن تقييد طرق varargs بواسطة معلمة. على سبيل المثال:

function getindex(A::AbstractArray{T,N}, indices::Vararg{Number,N}) where {T,N}

سيتم استدعاؤه فقط عندما يتطابق عدد indices مع أبعاد المصفوفة.

عندما يحتاج فقط نوع المعاملات الموردة إلى التقييد، يمكن كتابة Vararg{T} بشكل مكافئ كـ T.... على سبيل المثال، f(x::Int...) = x هو اختصار لـ f(x::Vararg{Int}) = x.

Note on Optional and keyword Arguments

كما ذُكر بإيجاز في Functions، يتم تنفيذ الوسائط الاختيارية كصيغة لتعريفات متعددة للطرق. على سبيل المثال، هذا التعريف:

f(a=1,b=2) = a+2b

يترجم إلى الطرق الثلاثة التالية:

f(a,b) = a+2b
f(a) = f(a,2)
f() = f(1,2)

هذا يعني أن استدعاء f() يعادل استدعاء f(1,2). في هذه الحالة، النتيجة هي 5، لأن f(1,2) يستدعي الطريقة الأولى من f أعلاه. ومع ذلك، قد لا يكون هذا هو الحال دائمًا. إذا قمت بتعريف طريقة رابعة أكثر تخصصًا للأعداد الصحيحة:

f(a::Int,b::Int) = a-2b

ثم تكون نتيجة كل من f() و f(1,2) هي -3. بعبارة أخرى، يتم ربط الوسائط الاختيارية بدالة، وليس بأي طريقة محددة من تلك الدالة. يعتمد الأمر على أنواع الوسائط الاختيارية التي يتم استدعاء الطريقة بناءً عليها. عندما يتم تعريف الوسائط الاختيارية من حيث متغير عالمي، قد يتغير نوع الوسيط الاختياري حتى في وقت التشغيل.

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

Function-like objects

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

على سبيل المثال، يمكنك تعريف نوع يخزن معاملات كثير الحدود، ولكنه يتصرف مثل دالة تقوم بتقييم كثير الحدود:

julia> struct Polynomial{R}
           coeffs::Vector{R}
       end

julia> function (p::Polynomial)(x)
           v = p.coeffs[end]
           for i = (length(p.coeffs)-1):-1:1
               v = v*x + p.coeffs[i]
           end
           return v
       end

julia> (p::Polynomial)() = p(5)

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

julia> p = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])

julia> p(3)
931

julia> p()
2551

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

Empty generic functions

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

function emptyfunc end

Method design and the avoidance of ambiguities

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

أشير أعلاه إلى أنه يمكن حل الغموض مثل

f(x, y::Int) = 1
f(x::Int, y) = 2

عن طريق تعريف طريقة

f(x::Int, y::Int) = 3

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

أدناه نناقش تحديات معينة وبعض الطرق البديلة لحل هذه القضايا.

Tuple and NTuple arguments

TupleNTuple) المعاملات تقدم تحديات خاصة. على سبيل المثال،

f(x::NTuple{N,Int}) where {N} = 1
f(x::NTuple{N,Float64}) where {N} = 2

غامضة بسبب إمكانية أن يكون N == 0: لا توجد عناصر لتحديد ما إذا كان يجب استدعاء متغير Int أو Float64. لحل الغموض، إحدى الطرق هي تعريف طريقة للزوج الفارغ:

f(x::Tuple{}) = 3

بدلاً من ذلك، بالنسبة لجميع الطرق ما عدا واحدة، يمكنك الإصرار على وجود عنصر واحد على الأقل في المجموعة:

f(x::NTuple{N,Int}) where {N} = 1           # this is the fallback
f(x::Tuple{Float64, Vararg{Float64}}) = 2   # this requires at least one Float64

Orthogonalize your design

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

f(x::A, y::A) = ...
f(x::A, y::B) = ...
f(x::B, y::A) = ...
f(x::B, y::B) = ...

قد تفكر في تعريف

f(x::A, y::A) = ...
f(x, y) = f(g(x), g(y))

حيث يقوم g بتحويل الوسيطة إلى النوع A. هذه مثال محدد جدًا من المبدأ العام الأكثر شمولاً orthogonal design، حيث يتم تخصيص مفاهيم منفصلة لطرق منفصلة. هنا، من المحتمل أن يحتاج g إلى تعريف احتياطي.

g(x::A) = x

تستغل استراتيجية ذات صلة promote لجلب x و y إلى نوع مشترك:

f(x::T, y::T) where {T} = ...
f(x, y) = f(promote(x, y)...)

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

Dispatch on one argument at a time

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

f(x::A, y) = _fA(x, y)
f(x::B, y) = _fB(x, y)

ثم يمكن للطريقتين الداخليتين _fA و _fB أن تتعاملان مع y دون القلق بشأن أي لبس بينهما فيما يتعلق بـ x.

كن على علم بأن هذه الاستراتيجية لها على الأقل عيب رئيسي واحد: في العديد من الحالات، لا يمكن للمستخدمين تخصيص سلوك f بشكل أكبر عن طريق تعريف تخصصات إضافية لدالتك المصدرة f. بدلاً من ذلك، يتعين عليهم تعريف تخصصات لطرقك الداخلية _fA و _fB، وهذا يblur الخطوط بين الطرق المصدرة والداخلية.

Abstract containers and element types

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

-(A::AbstractArray{T}, b::Date) where {T<:Date}

يولد غموضًا لأي شخص يعرف طريقة

-(A::MyArrayType{T}, b::T) where {T}

أفضل نهج هو تجنب تعريف أي من هاتين الطريقتين: بدلاً من ذلك، اعتمد على طريقة عامة -(A::AbstractArray, b) وتأكد من تنفيذ هذه الطريقة باستخدام استدعاءات عامة (مثل similar و -) التي تقوم بالشيء الصحيح لكل نوع حاوية ونوع عنصر بشكل منفصل. هذه مجرد نسخة أكثر تعقيدًا من النصيحة لـ orthogonalize طرقك.

عندما لا يكون هذا النهج ممكنًا، قد يكون من المفيد بدء مناقشة مع مطورين آخرين حول حل الغموض؛ فقط لأن طريقة واحدة تم تعريفها أولاً لا تعني بالضرورة أنه لا يمكن تعديلها أو إلغاؤها. كملاذ أخير، يمكن لمطور واحد تعريف طريقة "الضمادة".

-(A::MyArrayType{T}, b::Date) where {T<:Date} = ...

الذي يحل الغموض بالقوة الغاشمة.

Complex method "cascades" with default arguments

إذا كنت تعرف طريقة "cascade" التي توفر القيم الافتراضية، كن حذرًا بشأن إسقاط أي معلمات تتوافق مع القيم الافتراضية المحتملة. على سبيل المثال، افترض أنك تكتب خوارزمية تصفية رقمية ولديك طريقة تتعامل مع حواف الإشارة عن طريق تطبيق الحشو:

function myfilter(A, kernel, ::Replicate)
    Apadded = replicate_edges(A, size(kernel))
    myfilter(Apadded, kernel)  # now perform the "real" computation
end

سيؤدي هذا إلى تعارض مع طريقة توفر حشوة افتراضية:

myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # replicate the edge by default

معًا، تولد هاتان الطريقتان تكرارًا لا نهائيًا مع A الذي ينمو باستمرار.

سيكون التصميم الأفضل هو تعريف تسلسل استدعاء الوظائف الخاص بك على النحو التالي:

struct NoPad end  # indicate that no padding is desired, or that it's already applied

myfilter(A, kernel) = myfilter(A, kernel, Replicate())  # default boundary conditions

function myfilter(A, kernel, ::Replicate)
    Apadded = replicate_edges(A, size(kernel))
    myfilter(Apadded, kernel, NoPad())  # indicate the new boundary conditions
end

# other padding methods go here

function myfilter(A, kernel, ::NoPad)
    # Here's the "real" implementation of the core computation
end

NoPad يتم تزويده في نفس موضع الوسائط مثل أي نوع آخر من الحشو، لذا فإنه يحافظ على تنظيم تسلسل الاستدعاء بشكل جيد مع تقليل احتمالية الغموض. علاوة على ذلك، فإنه يوسع واجهة myfilter "العامة": يمكن للمستخدم الذي يرغب في التحكم في الحشو بشكل صريح استدعاء النسخة NoPad مباشرة.

Defining methods in local scope

يمكنك تعريف طرق داخل local scope، على سبيل المثال

julia> function f(x)
           g(y::Int) = y + x
           g(y) = y - x
           g
       end
f (generic function with 1 method)

julia> h = f(3);

julia> h(4)
7

julia> h(4.0)
1.0

ومع ذلك، يجب عليك عدم تعريف الطرق المحلية بشكل شرطي أو خاضع لتدفق التحكم، كما في

function f2(inc)
    if inc
        g(x) = x + 1
    else
        g(x) = x - 1
    end
end

function f3()
    function g end
    return g
    g() = 0
end

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

في حالات مثل هذه استخدم الدوال المجهولة بدلاً من ذلك:

function f2(inc)
    g = if inc
        x -> x + 1
    else
        x -> x - 1
    end
end
  • 1In C++ or Java, for example, in a method call like obj.meth(arg1,arg2), the object obj "receives" the method call and is implicitly passed to the method via the this keyword, rather than as an explicit method argument. When the current this object is the receiver of a method call, it can be omitted altogether, writing just meth(arg1,arg2), with this implied as the receiving object.
  • Clarke61Arthur C. Clarke, Profiles of the Future (1961): Clarke's Third Law.