على الرغم من حقيقة أن مفهوم جوليا يفتقر إلى البرمجة "وجوه" الموجهة للكائنات مع الطبقات والأساليب ، توفر اللغة أدوات التجريد ، وهو الدور الرئيسي الذي يلعبه نظام النوع وعناصر البرمجة الوظيفية. دعونا ننظر في النقطة الثانية بمزيد من التفصيل.
ربما يكون مفهوم الوظائف في جوليا هو الأكثر شبهاً باللغات من عائلة ليسب (على نحو أكثر دقة ، فروع ليسب -1) ، ويمكن اعتبار الوظائف على ثلاثة مستويات: برامج فرعية ، كمجردات لسلسلة معينة من الإجراءات ، وكبيانات تمثل هذا التجريد .
المستوى 1. وظائف الروتينية
استمر تخصيص البرامج الثانوية وتعيين أسمائهم الخاصة منذ عصور ما قبل التاريخ ، عندما كان فورتران يعتبر لغة عالية المستوى ، ولم تكن C موجودة بعد.
في هذا المعنى ، منتجات جوليا قياسية. يمكن تسمية "الميزة" بحقيقة أنه لا يوجد تقسيم إلى إجراءات ووظائف. بغض النظر عما إذا كان يتم استدعاء الروتين الفرعي للحصول على بعض القيمة أو لمجرد القيام ببعض الإجراءات على البيانات ، فإنه يطلق عليه دالة.
يبدأ تعريف الوظيفة بوظيفة الكلمة الأساسية ، متبوعة بقائمة من الوسائط ، وتسلسل الأوامر بين قوسين ، وتنتهي
end
الكلمة بالتعريف:
""" sum_all(collection) Sum all elements of a collection and return the result """ function sum_all(collection) sum = 0 for item in collection sum += collection end sum end
يتميز بناء الجملة بالسلوك الموروث من Lisp: بالنسبة لإرجاع "عادي" لقيمة من دالة ، فإن الكلمة
return
ليست ضرورية: قيمة التعبير الأخير المحسوب قبل إرجاع
end
. في المثال أعلاه ، سيتم إرجاع قيمة
sum
المتغير. وبالتالي ، يمكن استخدام
return
كعلامة للسلوك الخاص للدالة:
function safe_division(number, divisor) if divisor == 0 return 0 end number / divisor end
بالنسبة للوظائف ذات التعريف القصير ، يوجد بناء جملة مختص مشابه للترميز الرياضي. لذلك ، يمكن تعريف حساب طول الوتر على طول الساقين على النحو التالي:
hypotenuse(a, b) = sqrt(a^2 + b^2)
يمكن كتابة القسم "الآمن" باستخدام العامل الثلاثي على النحو التالي:
safe_division(number, divisor) = divisor == 0 ? 0 : number / divisor
كما ترى ، ليس من الضروري تحديد أنواع للوسائط الوظيفية. بالنظر إلى كيفية عمل برنامج التحويل البرمجي Julia JIT ، فإن كتابة البط لن يؤدي دائمًا إلى أداء رديء.
كما حاولت التوضيح في
مقال سابق ، يمكن لمترجم جوليا استنتاج نوع نتيجة الإرجاع حسب أنواع وسائط الإدخال. لذلك ، على سبيل المثال ، تتطلب وظيفة
safe_division
الحد الأدنى من التعديل للتشغيل السريع:
function safe_division(number, divisor) if divisor == 0 return zero(number / divisor) end number / divisor end
الآن ، إذا كانت أنواع كلا الوسيطتين معروفة في مرحلة الترجمة ، فسيتم أيضًا عرض نوع النتيجة التي تم إرجاعها بشكل لا لبس فيه ، حيث ترجع الدالة
zero(x)
قيمة صفرية من نفس النوع مثل الوسيطة الخاصة بها (والتقسيم على صفر ، وفقًا
لمعيار IEEE 754 ، له قيمة يمكن تمثيلها تمامًا بتنسيق أرقام الفاصلة العائمة).
يمكن أن تحتوي الدالات على عدد ثابت من الوسائط الموضعية ، والوسائط الموضعية ذات القيم الافتراضية ، والوسائط المسماة ، وعدد متغير من الوسائط. بناء الجملة:
المستوى 2. وظائف البيانات
لا يمكن استخدام اسم الوظيفة فقط في المكالمات المباشرة ، ولكن أيضًا كمعرف يرتبط به الإجراء الخاص بالحصول على القيمة. على سبيل المثال:
function f_x_x(fn, x) fn(x, x) end julia> f_x_x(+, 3) 6
وظائف "الكلاسيكية" التي تأخذ وسيطة وظيفية هي
map
،
reduce
filter
.
تطبق
map(f, x...)
الدالة
f
على قيم جميع العناصر من
x
(أو tuples of i-elements) وترجع النتائج كمجموعة جديدة:
julia> map(cos, [0, π/3, π/2, 2*π/3, π]) 5-element Array{Float64,1}: 1.0 0.5000000000000001 6.123233995736766e-17 -0.4999999999999998 -1.0 julia> map(+, (2, 3), (1, 1)) (3, 4)
reduce(f, x; init_val)
"يقلل" المجموعة إلى قيمة واحدة ، "يوسع" السلسلة
f(f(...f(f(init_val, x[1]), x[2])...), x[end])
:
function myreduce(fn, values, init_val) accum = init_val for x in values accum = fn(accum, x) end accum end
نظرًا لأنه لم يتم تحديده حقًا بالترتيب الذي سوف تمر به المصفوفة أثناء التخفيض ، أو ما إذا كان
fn(accum, x)
أو
fn(x, accum)
، فإن التخفيض سيعطي نتيجة يمكن التنبؤ بها فقط مع المشغلين التبادليين أو
fn(x, accum)
، مثل الإضافة أو الضرب.
filter(predicate, x)
بإرجاع مجموعة من العناصر
x
التي تفي بتقييم
predicate
:
julia> filter(isodd, 1:10) 5-element Array{Int64,1}: 1 3 5 7 9 julia> filter(iszero, [[0], 1, 0.0, 1:-1, 0im]) 4-element Array{Any,1}: [0] 0.0 1:0 0 + 0im
استخدام وظائف الترتيب العالي للعمليات على المصفوفات بدلاً من كتابة حلقة له العديد من المزايا:
- الكود يصبح أقصر
map()
أو reduce()
أوضح دلالات العملية التي يتم تنفيذها ، فلا يزال عليك فهم دلالات ما يحدث في الحلقة.- يتيح
map()
للمترجم أن يفهم أن العمليات على عناصر الصفيف مستقلة عن البيانات ، مما يسمح بتطبيق تحسينات إضافية
المستوى 3. وظائف التجريدية
غالبًا ما تكون في
map()
أو
filter()
تحتاج إلى استخدام دالة لم يتم تعيين اسمها الخاص بها. جوليا في هذه الحالة تسمح لك بالتعبير عن
تجريد العمليات على الوسيطة ، دون إدخال اسمك الخاص لهذا التسلسل. يُطلق
على هذا التجريد
وظيفة مجهولة المصدر ، أو
دالة lambda (لأنه في التقليد الرياضي ، تُشار إلى هذه الوظائف بالحرف lambda). بناء الجملة لهذا العرض هو:
يمكن تعيين كل من الدالتين المسماة والمجهولة للمتغيرات وإعادتها كقيمتين:
julia> double_squared = x -> (2 * x)^2
نطاق متغير والإغلاق المعجمية
في العادة ، يحاولون كتابة الوظائف بطريقة يتم الحصول على جميع البيانات اللازمة للحساب من خلال وسيطات رسمية ، أي أي أسماء المتغيرات التي تحدث في الجسم هي إما أسماء الوسائط الرسمية أو أسماء المتغيرات المقدمة داخل نص الوظيفة.
function normal(x, y) z = x + y x + y * z end function strange(x, y) x + y * z end
حول الوظيفة
normal()
، يمكننا القول أن جميع أسماء المتغيرات في نصها
مرتبطة ، أي إذا استبدلنا في كل مكان (بما في ذلك قائمة الوسائط) ب "x" بكلمة "m" (أو أي معرّف آخر) ، و "y" بكلمة "n" ، و "z" بكلمة "sum_of_m_and_n" ، فلن يتغير معنى التعبير. في الوظيفة
strange()
، الاسم z غير
مرتبط ، أي أ) قد يتغير المعنى إذا تم استبدال هذا الاسم بآخر ؛ و (ب) تعتمد صحة الوظيفة على ما إذا كان المتغير الذي يحمل الاسم "z" قد تم تعريفه في وقت استدعاء الوظيفة.
بشكل عام ، الوظيفة
normal()
ليست أيضًا نظيفة:
- ماذا يحدث إذا تم تعريف المتغير المسمى z خارج الوظيفة؟
- الأحرف + و * ، في الواقع ، هي أيضا معرفات غير ذات صلة.
فيما يتعلق بالنقطة 2 ، لا يمكن فعل شيء سوى الموافقة - من المنطقي أن تكون تعريفات جميع الوظائف المستخدمة في النظام موجودة ، ونأمل أن يكون معناها الحقيقي مطابقًا لتوقعاتنا.
النقطة 1 هي أقل وضوحا مما يبدو. الحقيقة هي أن الجواب يعتمد على مكان تعريف الوظيفة. إذا تم تعريفه على المستوى العالمي ، فسيكون
z
بداخل المتغير
normal()
متغيرًا محليًا ، أي حتى إذا كان هناك متغير
z
فلن يتم استبدال قيمته. إذا كان تعريف الوظيفة داخل كتلة التعليمات البرمجية ، فإذا كان هناك تعريف سابق لـ
z
في هذه الكتلة ، فستكون قيمة المتغير الخارجي التي سيتم تغييرها.
إذا كان نص الدالة يحتوي على اسم متغير خارجي ، فإن هذا الاسم يرتبط بالقيمة الموجودة في البيئة التي تم فيها إنشاء الوظيفة. إذا تم تصدير الوظيفة نفسها من هذه البيئة (على سبيل المثال ، إذا تم إرجاعها من وظيفة أخرى كقيمة) ، فإنها "تلتقط" المتغير من البيئة الداخلية ، التي لم تعد متاحة في البيئة الجديدة. وهذا ما يسمى إغلاق المعجمية.
تكون عمليات الإغلاق مفيدة بشكل أساسي في حالتين: عندما تحتاج إلى إنشاء دالة وفقًا للمعايير المحددة وعندما تحتاج إلى وظيفة بها بعض الحالات الداخلية.
ضع في اعتبارك الموقف باستخدام دالة تتضمن حالة داخلية:
function f_with_counter(fn) call_count = 0 ncalls() = call_count
دراسة حالة: كل نفس الحدود
في
مقال سابق ، يتم النظر في تقديم كثيرات الحدود كتركيبات. على وجه الخصوص ، واحدة من هياكل التخزين هي قائمة المعاملات ، بدءا من الأصغر سنا. لحساب متعدد الحدود
p
عند النقطة
x
اقتراح استدعاء الدالة
evpoly(p, x)
، والتي تقوم بحساب متعدد الحدود وفقًا لمخطط هورنر.
كود التعريف الكامل abstract type AbstractPolynomial end """ Polynomial <: AbstractPolynomial Polynomials written in the canonical form --- Polynomial(v::T) where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) Construct a `Polynomial` from the list of the coefficients. The coefficients are assumed to go from power 0 in the ascending order. If an empty collection is provided, the constructor returns a zero polynomial. """ struct Polynomial<:AbstractPolynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) coeff = isempty(v) ? (0.0,) : tuple([Float64(x) for x in v]...) return new(length(coeff)-1, coeff) end end """ InterpPolynomial <: AbstractPolynomial Interpolation polynomials in Newton's form --- InterpPolynomial(xsample::Vector{<:Real}, fsample::Vector{<:Real}) Construct an `InterpPolynomial` from a vector of points `xsample` and corresponding function values `fsample`. All values in `xsample` must be distinct. """ struct InterpPolynomial<:AbstractPolynomial degree::Int xval::NTuple{N, Float64} where N coeff::NTuple{N, Float64} where N function InterpPolynomial(xsample::X, fsample::F) where {X<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}, F<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} if !allunique(xsample) throw(DomainError("Cannot interpolate with duplicate X points")) end N = length(xsample) if length(fsample) != N throw(DomainError("Lengths of X and F are not the same")) end coeff = [Float64(f) for f in fsample] for i = 2:N for j = 1:(i-1) coeff[i] = (coeff[j] - coeff[i]) / (xsample[j] - xsample[i]) end end new(N-1, ntuple(i -> Float64(xsample[i]), N), tuple(coeff...)) end end function InterpPolynomial(fn, xsample::T) where {T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} InterpPolynomial(xsample, map(fn, xsample)) end function evpoly(p::Polynomial, z::Real) ans = p.coeff[end] for idx = p.degree:-1:1 ans = p.coeff[idx] + z * ans end return ans end function evpoly(p::InterpPolynomial, z::Real) ans = p.coeff[p.degree+1] for idx = p.degree:-1:1 ans = ans * (z - p.xval[idx]) + p.coeff[idx] end return ans end function Base.:+(p1::Polynomial, p2::Polynomial)
تمثيل كثير الحدود في شكل هيكل لا يتوافق تماما مع فهمها الحدسي كدالة رياضية. ولكن من خلال إرجاع القيمة الوظيفية ، يمكن أيضًا تحديد الحدود المتعددة كوظائف مباشرة. لذلك كان:
struct Polynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}})
نقوم بتحويل هذا التعريف إلى دالة تأخذ صفيفًا / مجموعة من المعاملات وتُرجع الدالة الفعلية التي تحسب كثير الحدود:
function Polynomial_as_closure(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}})
وبالمثل ، يمكنك كتابة وظيفة لاستكمال الحدود.
سؤال مهم: هل كان هناك شيء ضاع في التعريف الجديد في التعريف السابق؟ لسوء الحظ ، نعم - تعيين كثير الحدود كهيكل أعطى تلميحات للمترجم ، وبالنسبة لنا ، القدرة على التحميل الزائد للعوامل الحسابية لهذا الهيكل. للأسف ، جوليا لا توفر وظائف مثل هذا النوع القوي من النظام.
لحسن الحظ ، في هذه الحالة ، يمكننا الاستفادة من أفضل ما في العالمين ، حيث تتيح لك جوليا إنشاء ما يسمى بنيات يمكن الاتصال بها. أي يمكنك تحديد كثير الحدود كهيكل ، ولكن تكون قادرًا على تسميته كدالة! إلى تعريفات الهياكل من المقالة السابقة ، تحتاج فقط إلى إضافة:
function (p::Polynomial)(z::Real) evpoly(p, z) end function (p::InterpPolynomial)(z::Real) evpoly(p, z) end
باستخدام الوسائط الوظيفية ، يمكنك أيضًا إضافة مُنشئ خارجي متعدد الحدود للاستيفاء لوظيفة معينة تم إنشاؤها من مجموعة من النقاط:
function InterpPolynomial(fn, xsample::T) where {T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} InterpPolynomial(xsample, map(fn, xsample)) end
نتحقق من التعريف julia> psin = InterpPolynomial(sin, [0, π/6, π/2, 5*π/6, π])
استنتاج
توفر الإمكانيات المستعارة من البرمجة الوظيفية في جوليا لغة أكثر تعبيرًا مقارنة بأسلوب إلزامي بحت. تمثيل الهياكل في شكل وظائف هو وسيلة لتسجيل أكثر ملاءمة وطبيعية للمفاهيم الرياضية.