Style Guide

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

Indentation

استخدم 4 مسافات لكل مستوى من مستويات التداخل.

Write functions, not just scripts

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

من المهم أيضًا التأكيد على أن الدوال يجب أن تأخذ معطيات، بدلاً من العمل مباشرة على المتغيرات العالمية (باستثناء الثوابت مثل pi).

Avoid writing overly-specific types

يجب أن يكون الكود عامًا قدر الإمكان. بدلاً من كتابة:

Complex{Float64}(x)

من الأفضل استخدام الدوال العامة المتاحة:

complex(float(x))

ستقوم النسخة الثانية بتحويل x إلى نوع مناسب، بدلاً من أن يكون دائمًا من نفس النوع.

هذه النقطة المتعلقة بالأسلوب ذات صلة خاصة بوسائط الدالة. على سبيل المثال، لا تعلن عن وسيط ليكون من النوع Int أو Int32 إذا كان يمكن أن يكون أي عدد صحيح، معبرًا عنه بالنوع المجرد Integer. في الواقع، في العديد من الحالات يمكنك حذف نوع الوسيط تمامًا، ما لم يكن مطلوبًا لتفريقه عن تعريفات طرق أخرى، نظرًا لأن MethodError سيتم طرحه على أي حال إذا تم تمرير نوع لا يدعم أي من العمليات المطلوبة. (هذا معروف باسم duck typing.)

على سبيل المثال، اعتبر التعريفات التالية لدالة addone التي تعيد واحدًا زائدًا على وسيطها:

addone(x::Int) = x + 1                 # works only for Int
addone(x::Integer) = x + oneunit(x)    # any integer type
addone(x::Number) = x + oneunit(x)     # any numeric type
addone(x) = x + oneunit(x)             # any type supporting + and oneunit

التعريف الأخير لـ addone يتعامل مع أي نوع يدعم oneunit (الذي يُرجع 1 بنفس نوع x، مما يتجنب الترويج غير المرغوب فيه للنوع) ودالة + مع تلك المعطيات. الشيء الرئيسي الذي يجب أن تدركه هو أنه لا يوجد أي عقوبة في الأداء لتعريف فقط addone(x) = x + oneunit(x)، لأن جوليا ستقوم تلقائيًا بترجمة نسخ متخصصة حسب الحاجة. على سبيل المثال، في المرة الأولى التي تستدعي فيها addone(12)، ستقوم جوليا تلقائيًا بترجمة دالة addone المتخصصة لمعطيات x::Int، مع استبدال الاستدعاء لـ oneunit بقيمته المدمجة 1. لذلك، فإن التعريفات الثلاثة الأولى لـ addone أعلاه هي زائدة تمامًا عن التعريف الرابع.

Handle excess argument diversity in the caller

بدلاً من:

function foo(x, y)
    x = Int(x); y = Int(y)
    ...
end
foo(x, y)

استخدم:

function foo(x::Int, y::Int)
    ...
end
foo(Int(x), Int(y))

هذا أسلوب أفضل لأن foo لا يقبل حقًا الأرقام من جميع الأنواع؛ بل يحتاج حقًا إلى Int s.

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

Append ! to names of functions that modify their arguments

بدلاً من:

function double(a::AbstractArray{<:Number})
    for i in eachindex(a)
        a[i] *= 2
    end
    return a
end

استخدم:

function double!(a::AbstractArray{<:Number})
    for i in eachindex(a)
        a[i] *= 2
    end
    return a
end

تستخدم قاعدة جوليا هذا التقليد في جميع أنحاءها وتحتوي على أمثلة لوظائف بأشكال نسخ وتعديل (على سبيل المثال، sort و sort!)، وأخرى تعدل فقط (على سبيل المثال، push!، pop!، splice!). من المعتاد أن تعيد مثل هذه الوظائف أيضًا المصفوفة المعدلة لسهولة الاستخدام.

تعتبر الدوال المتعلقة بإدخال/إخراج البيانات أو التي تستخدم مولدات الأرقام العشوائية (RNG) استثناءات ملحوظة: حيث يجب على هذه الدوال تقريبًا أن تقوم بتغيير حالة الإدخال/الإخراج أو مولد الأرقام العشوائية، وتستخدم الدوال التي تنتهي بـ ! للدلالة على تغيير غير تغيير الإدخال/الإخراج أو تقدم حالة مولد الأرقام العشوائية. على سبيل المثال، rand(x) يغير حالة مولد الأرقام العشوائية، بينما rand!(x) يغير كل من مولد الأرقام العشوائية و x؛ وبالمثل، read(io) يغير io، بينما read!(io, x) يغير كلا المعطيات.

Avoid strange type Unions

أنواع مثل Union{Function,AbstractString} غالبًا ما تكون علامة على أن التصميم يمكن أن يكون أنظف.

Avoid elaborate container types

عادةً ما يكون من غير المفيد بناء مصفوفات مثل ما يلي:

a = Vector{Union{Int,AbstractString,Tuple,Array}}(undef, n)

في هذه الحالة Vector{Any}(undef, n) هو الأفضل. كما أنه أكثر فائدة للمترجم أن يحدد الاستخدامات المحددة (مثل a[i]::Int) بدلاً من محاولة حشر العديد من البدائل في نوع واحد.

Prefer exported methods over direct field access

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

  • يمكن لمطوري الحزم تغيير التنفيذ بحرية دون كسر كود المستخدم.
  • يمكن تمرير الطرق إلى البنى العليا مثل map (على سبيل المثال map(imag, zs)) بدلاً من [z.im for z in zs]).
  • يمكن تعريف الطرق على الأنواع المجردة.
  • يمكن أن تصف الطرق عملية مفاهيمية يمكن مشاركتها عبر أنواع مختلفة (على سبيل المثال، real(z) يعمل على الأعداد المركبة أو الكواتيرنيونات).

نظام الإرسال في جوليا يشجع على هذا النمط لأن play(x::MyType) يحدد فقط طريقة play على هذا النوع المحدد، مما يترك الأنواع الأخرى لتكون لها تنفيذاتها الخاصة.

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

أمثلة مضادة لهذه القاعدة تشمل NamedTuple، RegexMatch، StatStruct.

Use naming conventions consistent with Julia base/

  • تستخدم أسماء الوحدات وأنواعها الحروف الكبيرة وحالة الجمل: module SparseArrays، struct UnitRange.
  • تكون الدوال بحروف صغيرة (maximum, convert) وعندما تكون قابلة للقراءة، تكون الكلمات المتعددة مضغوطة معًا (isequal, haskey). عند الضرورة، استخدم الشرطات السفلية كفواصل بين الكلمات. تُستخدم الشرطات السفلية أيضًا للإشارة إلى مجموعة من المفاهيم (remotecall_fetch كتنفيذ أكثر كفاءة لـ fetch(remotecall(...))) أو كأدوات تعديل.
  • تنهى الدوال التي تغير على الأقل واحدًا من معطياتها بـ !.
  • تُقدَّر الإيجاز، ولكن تجنب الاختصارات (indexin بدلاً من indxin) لأنها تصبح صعبة التذكر فيما إذا كانت الكلمات معينة مختصرة وكيف.

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

Write functions with argument ordering similar to Julia Base

كقاعدة عامة، تستخدم مكتبة Base الترتيب التالي للوسائط في الدوال، حسب الاقتضاء:

  1. حجة الدالة. وضع حجة الدالة أولاً يسمح باستخدام do كتل لتمرير الدوال المجهولة متعددة الأسطر.
  2. تدفق الإدخال/الإخراج. يسمح تحديد كائن IO أولاً بتمرير الدالة إلى دوال مثل sprint، على سبيل المثال sprint(show, x).
  3. إدخال يتم تغييره. على سبيل المثال، في fill!(x, v)، x هو الكائن الذي يتم تغييره ويظهر قبل القيمة التي سيتم إدخالها في x.
  4. النوع. عادةً ما يعني تمرير نوع أن الناتج سيكون له النوع المعطى. في parse(Int, "1")، يأتي النوع قبل السلسلة التي سيتم تحليلها. هناك العديد من الأمثلة مثل هذه حيث يظهر النوع أولاً، ولكن من المفيد ملاحظة أنه في read(io, String)، يظهر وسيط IO قبل النوع، وهو ما يتماشى مع الترتيب الموضح هنا.
  5. لا يتم تغيير الإدخال. في fill!(x, v)، v لا يتم تغييره ويأتي بعد x.
  6. المفتاح. بالنسبة للمجموعات المرتبطة، هذا هو مفتاح زوج القيمة-المفتاح. بالنسبة للمجموعات المفهرسة الأخرى، هذا هو الفهرس.
  7. القيمة. بالنسبة للمجموعات المرتبطة، هذه هي قيمة زوج المفتاح-القيمة. في حالات مثل fill!(x, v)، هذه هي v.
  8. كل شيء آخر. أي حجج أخرى.
  9. Varargs. هذا يشير إلى المعاملات التي يمكن إدراجها بشكل غير محدود في نهاية استدعاء الدالة. على سبيل المثال، في Matrix{T}(undef, dims)، يمكن إعطاء الأبعاد كـ Tuple، على سبيل المثال Matrix{T}(undef, (1,2))، أو كـ Vararg، على سبيل المثال Matrix{T}(undef, 1, 2).
  10. وسائط الكلمات الرئيسية. في جوليا، يجب أن تأتي وسائط الكلمات الرئيسية في النهاية في تعريفات الدوال؛ تم إدراجها هنا من أجل الاكتمال.

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

هناك بالطبع بعض الاستثناءات. على سبيل المثال، في convert، يجب أن يأتي النوع دائمًا أولاً. في setindex!، تأتي القيمة قبل الفهارس بحيث يمكن تقديم الفهارس كـ varargs.

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

Don't overuse try-catch

من الأفضل تجنب الأخطاء بدلاً من الاعتماد على التقاطها.

Don't parenthesize conditions

جوليا لا تتطلب أقواسًا حول الشروط في if و while. اكتب:

if a == b

بدلاً من:

if (a == b)

Don't overuse ...

يمكن أن تكون وظيفة تجميع الوسائط مثيرة للإدمان. بدلاً من [a..., b...]، استخدم ببساطة [a; b]، الذي يقوم بالفعل بدمج المصفوفات. collect(a) أفضل من [a...]، ولكن بما أن a قابل للتكرار بالفعل، غالبًا ما يكون من الأفضل تركه كما هو، وعدم تحويله إلى مصفوفة.

Ensure constructors return an instance of their own type

عندما يتم استدعاء طريقة T(x) على نوع T، يُتوقع عمومًا أن تُرجع قيمة من نوع T. يمكن أن يؤدي تعريف constructor التي تُرجع نوعًا غير متوقع إلى سلوك محير وغير متوقع:

julia> struct Foo{T}
           x::T
       end

julia> Base.Float64(foo::Foo) = Foo(Float64(foo.x))  # Do not define methods like this

julia> Float64(Foo(3))  # Should return `Float64`
Foo{Float64}(3.0)

julia> Foo{Int}(x) = Foo{Float64}(x)  # Do not define methods like this

julia> Foo{Int}(3)  # Should return `Foo{Int}`
Foo{Float64}(3.0)

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

Don't use unnecessary static parameters

توقيع دالة:

foo(x::T) where {T<:Real} = ...

يجب أن تُكتب كالتالي:

foo(x::Real) = ...

بدلاً من ذلك، خاصة إذا لم يتم استخدام T في جسم الدالة. حتى إذا تم استخدام T، يمكن استبداله بـ typeof(x) إذا كان ذلك مريحًا. لا يوجد فرق في الأداء. لاحظ أن هذه ليست تحذيرًا عامًا ضد المعلمات الثابتة، بل ضد الاستخدامات التي لا تكون فيها ضرورية.

لاحظ أيضًا أن أنواع الحاويات، قد تحتاج تحديد نوع المعاملات في استدعاءات الدوال. راجع الأسئلة الشائعة Avoid fields with abstract containers لمزيد من المعلومات.

Avoid confusion about whether something is an instance or a type

مجموعات التعريفات مثل ما يلي تكون مربكة:

foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)

قرر ما إذا كان المفهوم المعني سيكتب كـ MyType أو MyType()، والتزم بذلك.

الأسلوب المفضل هو استخدام الحالات بشكل افتراضي، وإضافة الطرق التي تتعلق بـ Type{MyType} لاحقًا فقط إذا أصبحت ضرورية لحل بعض المشكلات.

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

Don't overuse macros

كن واعيًا عندما يمكن أن تكون الماكرو في الواقع دالة بدلاً من ذلك.

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

Don't expose unsafe operations at the interface level

إذا كان لديك نوع يستخدم مؤشرًا أصليًا:

mutable struct NativeType
    p::Ptr{UInt8}
    ...
end

لا تكتب تعريفات مثل التالية:

getindex(x::NativeType, i) = unsafe_load(x.p, i)

المشكلة هي أن مستخدمي هذا النوع يمكنهم كتابة x[i] دون إدراك أن العملية غير آمنة، ومن ثم يكونون عرضة لأخطاء الذاكرة.

يجب أن تتحقق هذه الدالة من العملية للتأكد من أنها آمنة، أو أن تحتوي على كلمة unsafe في اسمها لتنبيه المتصلين.

Don't overload methods of base container types

من الممكن كتابة تعريفات مثل ما يلي:

show(io::IO, v::Vector{MyType}) = ...

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

Avoid type piracy

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

كمثال، افترض أنك تريد تعريف الضرب على الرموز في وحدة:

module A
import Base.*
*(x::Symbol, y::Symbol) = Symbol(x,y)
end

المشكلة هي أنه الآن أي وحدة أخرى تستخدم Base.* سترى أيضًا هذا التعريف. نظرًا لأن Symbol معرف في Base ويستخدمه وحدات أخرى، يمكن أن يغير هذا سلوك الشيفرة غير المرتبطة بشكل غير متوقع. هناك العديد من البدائل هنا، بما في ذلك استخدام اسم دالة مختلف، أو تغليف Symbols في نوع آخر تقوم بتعريفه.

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

Be careful with type equality

تريد عمومًا استخدام isa و <: لاختبار الأنواع، وليس ==. عادةً ما يكون التحقق من الأنواع من أجل المساواة الدقيقة منطقيًا فقط عند المقارنة بنوع ملموس معروف (مثل T == Float64)، أو إذا كنت حقًا، حقًا تعرف ما تفعله.

Don't write a trivial anonymous function x->f(x) for a named function f

نظرًا لأن الدوال من الرتبة العليا غالبًا ما تُستدعى باستخدام دوال مجهولة، فمن السهل أن نستنتج أن هذا مرغوب فيه أو حتى ضروري. لكن يمكن تمرير أي دالة مباشرة، دون الحاجة إلى "تغليفها" في دالة مجهولة. بدلاً من كتابة map(x->f(x), a)، اكتب map(f, a).

Avoid using floats for numeric literals in generic code when possible

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

على سبيل المثال،

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

julia> f(1//2)
1.0

julia> f(1/2)
1.0

julia> f(1)
2.0

بينما

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

julia> g(1//2)
1//1

julia> g(1/2)
1.0

julia> g(1)
2

كما ترى، النسخة الثانية، حيث استخدمنا حرف Int، حافظت على نوع وسيط الإدخال، بينما الأولى لم تفعل. وذلك لأن، على سبيل المثال، promote_type(Int, Float64) == Float64، وتحدث الترقية مع الضرب. وبالمثل، فإن الحروف Rational أقل تدميراً لنوع البيانات من الحروف Float64، لكنها أكثر تدميراً من Ints:

julia> h(x) = 2//1 * x
h (generic function with 1 method)

julia> h(1//2)
1//1

julia> h(1/2)
1.0

julia> h(1)
2//1

لذا، استخدم القيم الأدبية Int عند الإمكان، مع Rational{Int} للأرقام غير الصحيحة الأدبية، لتسهيل استخدام كودك.