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 Union
s
أنواع مثل 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 الترتيب التالي للوسائط في الدوال، حسب الاقتضاء:
- حجة الدالة. وضع حجة الدالة أولاً يسمح باستخدام
do
كتل لتمرير الدوال المجهولة متعددة الأسطر. - تدفق الإدخال/الإخراج. يسمح تحديد كائن
IO
أولاً بتمرير الدالة إلى دوال مثلsprint
، على سبيل المثالsprint(show, x)
. - إدخال يتم تغييره. على سبيل المثال، في
fill!(x, v)
،x
هو الكائن الذي يتم تغييره ويظهر قبل القيمة التي سيتم إدخالها فيx
. - النوع. عادةً ما يعني تمرير نوع أن الناتج سيكون له النوع المعطى. في
parse(Int, "1")
، يأتي النوع قبل السلسلة التي سيتم تحليلها. هناك العديد من الأمثلة مثل هذه حيث يظهر النوع أولاً، ولكن من المفيد ملاحظة أنه فيread(io, String)
، يظهر وسيطIO
قبل النوع، وهو ما يتماشى مع الترتيب الموضح هنا. - لا يتم تغيير الإدخال. في
fill!(x, v)
،v
لا يتم تغييره ويأتي بعدx
. - المفتاح. بالنسبة للمجموعات المرتبطة، هذا هو مفتاح زوج القيمة-المفتاح. بالنسبة للمجموعات المفهرسة الأخرى، هذا هو الفهرس.
- القيمة. بالنسبة للمجموعات المرتبطة، هذه هي قيمة زوج المفتاح-القيمة. في حالات مثل
fill!(x, v)
، هذه هيv
. - كل شيء آخر. أي حجج أخرى.
- Varargs. هذا يشير إلى المعاملات التي يمكن إدراجها بشكل غير محدود في نهاية استدعاء الدالة. على سبيل المثال، في
Matrix{T}(undef, dims)
، يمكن إعطاء الأبعاد كـTuple
، على سبيل المثالMatrix{T}(undef, (1,2))
، أو كـVararg
، على سبيل المثالMatrix{T}(undef, 1, 2)
. - وسائط الكلمات الرئيسية. في جوليا، يجب أن تأتي وسائط الكلمات الرئيسية في النهاية في تعريفات الدوال؛ تم إدراجها هنا من أجل الاكتمال.
الغالبية العظمى من الدوال لن تأخذ كل نوع من المعاملات المذكورة أعلاه؛ الأرقام تشير فقط إلى الأولوية التي يجب استخدامها لأي معاملات قابلة للتطبيق على دالة.
هناك بالطبع بعض الاستثناءات. على سبيل المثال، في 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 ويستخدمه وحدات أخرى، يمكن أن يغير هذا سلوك الشيفرة غير المرتبطة بشكل غير متوقع. هناك العديد من البدائل هنا، بما في ذلك استخدام اسم دالة مختلف، أو تغليف Symbol
s في نوع آخر تقوم بتعريفه.
أحيانًا، قد تشارك الحزم المترابطة في سرقة النوع لفصل الميزات عن التعريفات، خاصة عندما تم تصميم الحزم من قبل مؤلفين متعاونين، وعندما تكون التعريفات قابلة لإعادة الاستخدام. على سبيل المثال، قد توفر حزمة ما بعض الأنواع المفيدة للعمل مع الألوان؛ بينما قد تعرف حزمة أخرى طرقًا لتلك الأنواع تمكن من التحويلات بين مساحات الألوان. مثال آخر قد يكون حزمة تعمل كغلاف رقيق لبعض كود 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
، لكنها أكثر تدميراً من Int
s:
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}
للأرقام غير الصحيحة الأدبية، لتسهيل استخدام كودك.