في هذه المقالة ، سننظر في إضافة نوع بيانات مخصص إلى برنامج Julia وزيادة التحميل على الوظائف القياسية للعمل المريح مع النوع الجديد.
ما هو نوع مخصص
سر رهيب - الحدود بين أنواع البيانات "المستخدم" و "المضمنة" في جوليا غائبة عمليا. في أي من برامج Julia ، يمكنك تحديد أنواع البيانات الخاصة بك ، والعمل معها لا يختلف تقريبًا عن العمل مع الأنواع المدمجة ، سواء من حيث كتابة التعليمات البرمجية أو من حيث سرعة تنفيذ هذا الرمز (مرحبًا ، Python). يمكن أن تكون أنواع بيانات الإدخال إما بدائية أو مركبة. وتنقسم الأخيرة أيضا إلى أنواع مجردة وملموسة.
كذلك سننظر في مثال على أنواع الخرسانة المركبة ، منذ ذلك الحين الكائنات المحددة في الذاكرة هي أنواع محددة ، والكائنات المجردة مطلوبة فقط لإنشاء تسلسل هرمي.
يتم تعريف الأنواع المركبة من خلال struct
الكلمات الأساسية struct
mutable struct
. في الحالة الأولى ، يتم إنشاء نوع ثابت:
كائنات النوع Point
غير قابلة للتغيير ، أي لا يمكن تغيير قيمة الحقل في كائن تم إنشاؤه مرة واحدة.
julia> p = Point(2, 3) Point(2, 3) julia> px 2 julia> py 3 julia> px = 4 ERROR: setfield! immutable struct of type Point cannot be changed
لكن يمكنك عمل نوع قابل للتغيير:
mutable struct MutablePoint x y end julia> mp = MutablePoint(3, 4) MutablePoint(3, 4) julia> mp.y = 2 2 julia> mp MutablePoint(3, 2)
من أجل الكفاءة ، يمكن إضافة التعليقات التوضيحية إلى حقول البنية والهيكل نفسه. على سبيل المثال:
struct ParametricPoint{T<:Real} x::T y::T end
هذا يعني أن النوع ParametricPoint
حسب النوع T
وأن الحقلين x
و y
يجب أن يكونا الآن قيمتين من نفس النوع T
سيكون السلوك كما يلي:
julia> ParametricPoint(1, 1) ParametricPoint{Int64}(1, 1) julia> ParametricPoint{Float64}(1, 1) ParametricPoint{Float64}(1.0, 1.0) julia> ParametricPoint{Rational}(1, 1) ParametricPoint{Rational}(1//1, 1//1)
بعد ذلك ، دعونا نلقي نظرة على مثال أكثر تعقيدًا قليلاً لنوع الحاوية لمعرفة الأساليب الموجودة في المكتبة القياسية للتعامل مع هذه الأنواع.
طابور في اتجاهين
نقوم بعمل قائمة انتظار ثنائية الاتجاه من عقد من نوع DequeNode
، والتي ستتضمن سجل بيانات وارتباطات بالعناصر السابقة والقادمة من قائمة الانتظار. يجب أن يكون نوع DequeNode
قابلاً للتغيير ، لأن سوف تحتاج إلى إعادة صياغة الروابط.
mutable struct DequeNode{T} data::T prev::DequeNode{T} next::DequeNode{T}
في هذه الحالة ، تكون هناك حاجة إلى مُنشئات داخلية ، لأنك تحتاج افتراضيًا عند إنشاء كائن إلى تمرير قيم جميع الحقول باستخدام الوسائط. نظرًا لأن النوع يتم الرجوع إليه ذاتيًا ، سيكون من المستحيل على المنشئ القياسي إنشاء الكائن الأول من هذا النوع. لذلك ، نحن نصنع المنشئات الخاصة التي تنشئ عقدة قائمة انتظار تشير إلى نفسها.
يمكن تمثيل قائمة الانتظار نفسها على أنها عقدة "إدخال" وهمية لا تحتوي على أي بيانات. سيشير الرابط next
إلى العنصر الأول في قائمة الانتظار ، prev
إلى الأخير. لقائمة انتظار فارغة ، يشير كلا الارتباطين إلى عقدة الإدخال. تعمل مثل هذه المنظمة على تبسيط إجراءات إضافة العقد وإزالتها.
struct Deque{T} entry::DequeNode{T} end Deque{T}() where T = Deque{T}(DequeNode{T}()) Deque() = Deque{Any}()
منشئ داخلي لم يعد مطلوبًا لهيكل قائمة الانتظار نفسها.
إضافة عناصر
تضيف الإجراءات القياسية لقائمة الانتظار عنصرًا إلى البداية والنهاية ، وتستخرج العناصر من البداية والنهاية. لهذه الإجراءات ، تحتوي المكتبة القياسية بالفعل على وظائف لا تحتاج إلا إلى إضافة طرق للعمل مع نوع آخر:
push!(collection, element)
- أضف عنصرًا إلى المجموعة (للحصول على مجموعة مرتبة - في النهاية)pushfirst!(collection, element)
- إضافة عنصر إلى بداية مجموعة مرتبةpop!(collection)
- حذف عنصر من المجموعة (بالنسبة لمجموعة مرتبة - من النهاية)popfirst!(collection)
- قم بإزالة العنصر الأول من مجموعة مرتبة
كل هذه الوظائف موجودة في الوحدة النمطية Base
، وتحتاج إلى إضافة طرق جديدة إليها. بالإضافة إلى ذلك ، سنقوم بكتابة وظيفة للتحقق مما إذا كانت المجموعة غير فارغة - Base.isempty(collection)
.
Base.isempty(deq::Deque) = deq.entry.prev ≡ deq.entry function Base.push!(deq::Deque{T}, elt) where T tail = deq.entry.prev new_item = DequeNode{T}(elt) new_item.prev, new_item.next = tail, deq.entry tail.next = deq.entry.prev = new_item return deq end function Base.pushfirst!(deq::Deque{T}, elt) where T head = deq.entry.next new_item = DequeNode{T}(elt) new_item.prev, new_item.next = deq.entry, head head.prev = deq.entry.next = new_item return deq end function Base.pop!(deq::Deque) !isempty(deq) || throw(ArgumentError("deque must be non-empty")) last = deq.entry.prev last.prev.next = deq.entry deq.entry.prev = last.prev return last.data end function Base.popfirst!(deq::Deque) !isempty(deq) || throw(ArgumentError("deque must be non-empty")) first = deq.entry.next first.next.prev = deq.entry deq.entry.next = first.next return first.data end
تجعل وظيفة push!()
تافهة تقريبًا لكتابة مُنشئ قائمة انتظار استنادًا إلى مجموعة عشوائية:
function Deque(itr) d = Deque{eltype(itr)}() for elt in itr push!(d, elt) end return d end
الآن يمكن استخدام النوع الجديد كقائمة انتظار إذا كنا بحاجة فقط إلى إضافة عمليات وإزالتها ولا نحتاج إلى وظيفة مثل peek()
(انظر العنصر الموجود في رأس قائمة الانتظار أو ذيلها دون إزالتها). للوصول غير المدمر إلى العناصر ، من الملائم تحديد وظيفة التكرار على الحاوية.
تكرير
جوليا تدعم مفهوم التكرارات لاجتياز عناصر الحاوية. للتكرار ، تحتاج إلى تحديد وظيفة واحدة - iterate(container, state)
، حيث تحدد الحالة الحالة الحالية للتكرار. على وجه الخصوص ، فإن بناء for x in collection
هو سكر نحوي لشيء من هذا القبيل:
let nextstate = iterate(collection) while nextstate ≢ nothing (x, state) = nextstate <- > nextstate = iterate(collection, state) end end
تأخذ الدالة iterate()
حاوية و "حالة التكرار" كوسيطة ، وتُرجع إما tuple من عنصر الحاوية وحالة التكرار التالية ، أو nothing
حالة انتهاء الحاوية.
بالنسبة لقائمة الانتظار ، من المنطقي أن تأخذ عقدة قائمة الانتظار ، التي وصل التكرار إليها ، باسم "الحالة":
function Base.iterate(deq::Deque{T}, state::DequeNode{T}=deq.entry) where T nextstate = state.next nextstate ≡ deq.entry ? nothing : (nextstate.data, nextstate) end
الآن يمكن تكرار عناصر قائمة الانتظار مع حلقة:
julia> for x in Deque(1:10) println(x); end 1 2 3 4 5 6 7 8 9 10
ميزة إضافية كبيرة هي أن العديد من وظائف مكتبة جوليا القياسية مكتوبة باستخدام واجهات معممة ، على وجه الخصوص ، التكرار. وبالتالي ، يتم تلقائيًا التحقق مما إذا كان عنصر ينتمي إلى حاوية:
julia> 5 ∈ Deque(3:10) true julia> 100 ∈ Deque(-5:5) false
نجد أيضًا أن الطريقة first()
، التي تُرجع العنصر الأول من المجموعة ، بدأت في العمل. لإكمال الصورة ، نقوم باستكمال الأسلوب last()
للحصول على العنصر الأخير وتكراره بترتيب عكسي:
function Base.iterate(r::Base.Iterators.Reverse{Deque{T}}, state::DequeNode{T}=r.itr.entry) where T nextstate = state.prev nextstate ≡ r.itr.entry ? nothing : (nextstate.data, nextstate) end function Base.last(deq::Deque) isempty(deq) && throw(ArgumentError("deque must be non-empty")) deq.entry.prev.data end julia> for x in Iterators.reverse(Deque(1:10)) println(x); end 10 9 8 7 6 5 4 3 2 1
لذلك ، يتم الآن تعريف واجهة قائمة الانتظار كهيكل بيانات بشكل كامل
هيكل المطبوعات
على الرغم من أن قائمة الانتظار تعمل بشكل كامل كهيكل بيانات ، إلا أنها لا تزال تمثل فوضى بدون شك:
julia> Deque() Deque{Any}(DequeNode{Any}(
إضافة طريقة لطباعة قائمة الانتظار في شكل أكثر إنسانية. يتم التحكم في إخراج الهيكل إلى المحطة بواسطة وظيفة Base.show()
، والتي سنقوم Base.show()
بشكل زائد:
function Base.show(io::IO, deq::Deque{T}) where T print(io, "Deque{", T, "}(") next = iterate(deq) if next ≢ nothing item, state = next show(io, item) next = iterate(deq, state) while next ≢ nothing item, state = next print(io, ", ") show(io, item) next = iterate(deq, state) end end print(io, ")") end
الوصول والتغيير حسب الفهرس
من حيث المبدأ ، يمكن أيضًا استخدام قائمة الانتظار كحاوية يمكن الوصول إليها بواسطة الفهرس. يتم التحكم في استرداد وتغيير قيمة حسب الفهرس بواسطة getindex()
و setindex!()
. نحن ننفذ الطرق اللازمة لقائمة الانتظار.
function Base.getindex(deq::Deque, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else return next[1] end end function Base.setindex!(deq::Deque, val, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else record = next[2] record.data = val return val end end
من المفيد أيضًا إضافة وظيفة إدراج عنصر إلى مكان تعسفي insert!()
وإزالة عنصر من موضع تعسفي:
function Base.insert!(deq::Deque{T}, i::Integer, val) where T i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing push!(deq, val) else record = next[2] new_node = DequeNode{T}(val) new_node.prev, new_node.next = record.prev, record record.prev = record.prev.next = new_node deq end end function Base.deleteat!(deq::Deque, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else record = next[2] record.prev.next, record.next.prev = record.next, record.prev end end
آخر
كما ذكرنا سابقًا ، تتم كتابة عدد من وظائف المكتبة القياسية باستخدام التكرارات ، مما يتيح لها العمل تلقائيًا مع أي نوع يتم تعريف وظيفة iterate()
. على وجه الخصوص ، minimum()
وظائف مثل map()
و collect()
وطرق التخفيض مثل sum()
أو minimum()
.
على سبيل المثال ، يمكن كتابة دالة للعثور على طول قائمة الانتظار مثل هذا:
Base.length(deq::Deque) = sum(x->1, deq) julia> length(Deque(1, 1, 2, 3, 5, 8, 13)) 7
استنتاج
كما ترى ، فإن إنشاء نوع البيانات الخاص بك في جوليا وتنظيم العمل المريح معه أمر بسيط للغاية. نظرًا لاستخدام البرمجة المعممة في المكتبة القياسية ، يكفي عادة تحديد العديد من الوظائف الأساسية ، وسيعمل عدد من الوظائف التابعة تلقائيًا. لذلك ، من خلال تحديد push!(container, element)
لإضافة عنصر واحد ، يمكنك العثور على ذلك push!(container, elements...)
element push!(container, elements...)
سيعمل على إضافة عدد تعسفي من الوسائط. بعد تحديد التكرار ، بشكل عام ، نحصل تلقائيًا على جميع وظائف المكتبة القياسية للعمل مع حاوية مجردة.
القرصنة سعيدة!