از نظر فنی، یک 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 از چپ به راست ممکن است پاسخ کمی متفاوتی از راست به چپ بدهد.
جهت تابع 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 را در هم میآمیزد.
آرگومان اول تابعی است که با آن نگاشت انجام میشود و آرگومان دوم عملگر 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() را پیادهسازی کنید که یک بردار از امتیازها میگیرد و اگر همهی امتیازها 1 یا 5 باشند true و در غیر این صورت false برمیگرداند.
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 استفاده میکنند، بنابراین باید دادهها را به یک 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