من الناحية التقنية، Higher Order Function هي ببساطة دالة تفعل واحدًا على الأقل مما يلي:
داخل عالم البرمجة الوظيفية، يميل الاستخدام إلى أن يكون أضيق نطاقًا.
يشير المصطلح عادةً إلى دوال مثل filter وmap وreduce التي تطبّق دالة مُمرَّرة على عناصر مجموعة.
في هذه المرحلة من المنهج، رأينا بالفعل طرقًا متعددة لتطبيق عملية على جميع عناصر مجموعة قابلة للتكرار، مثل Vector:
سيركّز هذا المفهوم على الدوال عالية الرتبة (المألوفة في أي لغة وظيفية، مثل Haskell أو F#).
تشمل الأساليب الأخرى الممكنة:
تأخذ الدالة filter() دالةً مُمرَّرة تُرجع قيمة منطقية وتطبّقها على مجموعة.
لا تُضمَّن في القيمة المُرجَعة إلا العناصر التي تُرجع true، وهي من النوع الأساسي نفسه الذي للمُدخل (انظر أدناه).
julia> filter(iseven, 1:6)
3-element Vector{Int64}:
2
4
6
# String is a collection of Chars, so String in -> String out
julia> filter(!isascii, "Hrōðgār")
"ōðā"
# tuple input -> tuple output
julia> filter(iseven, (1, 2, 3, 4, 5))
(2, 4)
مع المصفوفات متعددة الأبعاد، تُسطّح الدالة filter أبعاد المُدخل وتُرجع Vector: وهذا هو الاستثناء الرئيسي لأي قاعدة تتعلق بمطابقة نوع المُخرَج لنوع المُدخل.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> filter(isodd, m)
3-element Vector{Int64}:
1
5
3
تستخدم الأمثلة أعلاه دوالًا مدمجة، لكن استخدام الدوال المجهولة شائع جدًا في هذا السياق.
julia> filter(x -> x % 3 == 0, 1:20)
6-element Vector{Int64}:
3
6
9
12
15
18
توجد أيضًا نسخة تُعدّل في المكان نفسه، filter!()، كما هو الحال مع كثير من الدوال في هذا المفهوم.
تحوّل الدالة map() مجموعةً بتطبيق دالة على كل عنصر.
قد يكون هذا مشابهًا للبث في الحالات البسيطة، حيث يطابق شكل المُخرَج شكل المُدخل.
julia> map(√, [1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
julia> map(x -> x^2 + 1, 1:4)
4-element Vector{Int64}:
2
5
10
17
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> map(√, m)
2×3 Matrix{Float64}:
1.0 1.41421 1.73205
2.0 2.23607 2.44949
تعمل map() أيضًا عنصرًا بعنصر على مجموعات متعددة.
julia> map(*, [1, 2], [3, 4])
2-element Vector{Int64}:
3
8
من الناحية المفاهيمية، يمكننا اعتبار هذا مكافئًا لتشغيل zip() على مجموعات المُدخل المتعددة، ثم تشغيل map() على كل عنصر من النتيجة الوسيطة.
هذا مجرد تشبيه تقريبي، ولا يعني شيئًا عن طريقة التنفيذ!
وكما هو الحال مع zip()، تُقتطع المجموعات ذات الأشكال غير المتطابقة إلى بُعد (أو أبعاد) الأصغر منها.
أحيانًا لا نحتاج إلا إلى الآثار الجانبية للدالة المُمرَّرة، مثل الكتابة في قاعدة بيانات أو push! إلى مصفوفة.
عندئذٍ تتوفر الدالة عالية الرتبة foreach()، التي تُرجع دائمًا nothing.
تأخذ الدالة reduce() دالةً بوسيطين وتطبّقها على مجموعة، مما يؤدي إلى تقليص الأبعاد.
قد يبدو هذا محيّرًا عند الحديث عنه بشكل مجرد، لكن فكّر في دوال مثل sum() أو prod() التي تأخذ مجموعة وتُرجع قيمة واحدة.
julia> sum(1:4) # add
10
julia> prod(1:4) # multiply
24
هذه الدوال الخاصة عالية التحسين وينبغي استخدامها دائمًا عند توفرها.
تشمل الأمثلة الأخرى maximum() وminimum()، والدالتين المنطقيتين all() وany()، وكثيرًا من الدوال الإحصائية.
للتوضيح فقط، لنتأمل الوظيفة نفسها مُنفَّذة بالدالة الأكثر عمومية reduce() (تذكّر أن العاملين البينيين + و* هما في الحقيقة دالتان داخليًا).
julia> reduce(+, 1:4) # add
10
julia> reduce(*, 1:4) # multiply
24
وكما هو الحال مع sum() ودوال التجميع الأخرى، يمكن لـ reduce() أن تأخذ وسيطًا اختياريًا بكلمة مفتاحية dims، لتحديد البُعد (أو الأبعاد) المراد تقليصها.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> reduce(+, m; dims=1)
1×3 Matrix{Int64}:
5 7 9
هذه أمثلة سهلة، لأن الجمع والضرب عمليتان تبديليتان (1+2 == 2+1) وتجميعيتان ((1+2)+3 == 1+(2+3)).
وهذا أبعد ما يكون عن أن يكون قاعدة عامة! فحتى عمليات شائعة مثل الطرح والقسمة ليست تجميعية.
وهناك مشكلة إضافية هي أن أخطاء الفاصلة العائمة قد تتراكم عبر المجموعات الكبيرة، لذا قد يُنتج الاختزال من اليسار إلى اليمين إجابةً مختلفة قليلًا عن الاختزال من اليمين إلى اليسار.
اتجاه الدالة reduce في Julia يعتمد على طريقة التنفيذ وغير مضمون.
للتحكم في الاتجاه بشكل صريح، توجد الدالتان foldl() وfoldr()، وتبدآن نظريًا من «اليسار» و«اليمين» على الترتيب (وفي الحقيقة من الأعلى والأسفل، في حالة Vector).
julia> foldl(-, 1:3) # (1 - 2) - 3
-4
julia> foldr(-, 1:3) # 1 - (2 - 3)
2
لاحظ أن هاتين الدالتين مخصصتان للمجموعات التي يمكن التعامل معها كأحادية البُعد، فتُرجعان نتيجة قياسية.
لا يُدعم استخدام وسيط dims مع foldl وfoldr، بل مع reduce فقط.
الجمع بين عملية map وعملية reduce شائع جدًا في مجالات برمجية متنوعة.
يمكننا تشغيل map بالتتابع، ثم تشغيل reduce على مجموعة وسيطة.
لكن هذا غير فعّال في أفضل الأحوال، ويسوء أداؤه كثيرًا مع نمو المجموعة.
يُوصى بشدة باستخدام الدالة المُدمَجة mapreduce() بدلًا من ذلك.
فهي قادرة على تنفيذ خوارزمية أعلى أداءً تُشابك بين عمليتي map وreduce.
الوسيط الأول هو الدالة التي ستُستخدم في التعيين، والوسيط الثاني هو عامل الاختزال.
julia> mapreduce(x -> x^2 + 1, +, 1:3)
17
# equivalent to (2 + 5 + 10)
julia> sum(map(x -> x^2 + 1, 1:3))
17
وكما قد نتوقع، توفّر Julia أيضًا الدالتين mapfoldl() وmapfoldr() للحالات التي يكون فيها الاتجاه مهمًا.
نحن بصدد إطلاق نادي للجبن، سيستخدم تعلّم الآلة لاختيار أجبان جديدة نقدّمها لعملائنا من محبي الجبن، بناءً على تاريخهم وأذواقهم.
يُطلب من الأعضاء الجدد ملء استبيان أولي حتى نجمع بعض البيانات الأساسية للبداية. وقد تبيّن من خلاله أن هناك مجموعة فرعية من العملاء الحاسمين تفتقر انتقاداتهم إلى الفروق الدقيقة. ولأن ذلك قد ينتهي إلى تحيّز لا رجعة فيه تجاه خوارزمية أكثر مراعاة للفروق الدقيقة، فقد أُعدّت خوارزمية منفصلة للتعامل مع احتياجاتهم. ويُطلب منك توفير بعض الدوال المساعدة للتعامل مع بياناتهم.
قد تكون هناك أساليب مختلفة لحل المهام التالية، لكن يمكن حل كل مهمة بدالة واحدة مختلفة من الرتبة الأعلى.
نظام التقييم قائم على خمس نجوم، ويتألف ببساطة من الأعداد الصحيحة 1:5.
لا يمنح العملاء الحاسمون إلا تقييمات 1 أو 5، ونريد معرفة ما إذا كان العميل يُظهر هذا السلوك.
نفّذ all_15()، وهي تأخذ متجهًا من التقييمات وتُرجع true إذا كانت جميع التقييمات إما 1 أو 5، وخطأ فيما عدا ذلك.
julia> ratings = [2, 3, 4, 4, 1];
julia> all_15(ratings)
false
julia> ratings = [1, 5, 5, 1, 5];
julia> all_15(ratings)
true
نحتاج إلى فصل العملاء الأكثر حسمًا عن البقية.
نفّذ emphatics()، وهي تأخذ قاموسًا يربط العملاء بالتقييمات.
وتُرجع قاموسًا مماثلًا يضم من يستخدمون تقييمات 1 أو 5 نجوم فقط.
julia> ratings = ([2, 3, 5, 1, 1], [1, 1, 5, 5, 1], [4, 5, 5, 3, 2], [5, 5, 1, 1, 5]);
julia> names = ("c1", "c2", "c3", "c4");
julia> customers = Dict(zip(names, ratings))
Dict{String, Vector{Int64}} with 4 entries:
"c2" => [1, 1, 5, 5, 1]
"c1" => [2, 3, 5, 1, 1]
"c3" => [4, 5, 5, 3, 2]
"c4" => [5, 5, 1, 1, 5]
julia> emphatics(customers)
Dict{String, Vector{Int64}} with 2 entries:
"c2" => [1, 1, 5, 5, 1]
"c4" => [5, 5, 1, 1, 5]
بما أن العملاء الحاسمين لا يستخدمون إلا تقييمات 1 و5، فسيكون من الأنسب حسابيًا تحويلها إلى 0 و1.
نفّذ tobinary()، وهي تأخذ متجهًا من التقييمات الحاسمة.
وتُرجع تقييمات ثنائية، حيث حُوِّل 1 إلى 0 وحُوِّل 5 إلى 1.
julia> ratings = [1, 1, 5, 5, 1];
julia> tobinary(ratings)
5-element Vector{Int64}:
0
0
1
1
0
تستخدم خوارزمياتنا مدخلات من نوع Matrix، لذا سنحتاج إلى تحويل البيانات إلى مصفوفة.
نفّذ tobinarymatrix()، وهي تأخذ متجهًا من متجهات التقييمات الحاسمة.
وتُرجع Matrix من البيانات المحوَّلة، بحيث يكون كل متجه تقييمات صفًا في المصفوفة.
julia> customersratings = [[1, 1, 5, 5, 1],[5, 5, 1, 1, 5]];
julia> tobinarymatrix(customersratings)
2×5 Matrix{Int64}:
0 0 1 1 0
1 1 0 0 1
سجّل في Exercism لتتعلّم وتتقن Julia عبر 35 مفهومًا128 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.