مسیرها
/
Julia
Julia
/
تمرین‌ها
/
باشگاه پنیر
باشگاه پنیر

باشگاه پنیر

تمرین یادگیری

مقدمه

از نظر فنی، یک Higher Order Function به سادگی تابعی است که دست‌کم یکی از این کارها را انجام می‌دهد:

  • یک تابع را به عنوان یکی از آرگومان‌هایش می‌پذیرد.
  • یک تابع را به عنوان نتیجه برمی‌گرداند.

در دنیای برنامه‌نویسی تابعی، کاربردش معمولاً محدودتر است. این اصطلاح معمولاً به توابعی مانند filter، map و reduce اشاره دارد که تابعی را که به آن‌ها داده شده روی عنصرهای یک مجموعه اعمال می‌کنند.

کار روی مجموعه‌ها

تا اینجای برنامه‌ی درسی، راه‌های مختلفی دیده‌ایم برای اعمال یک عمل روی همه‌ی عنصرهای یک مجموعه‌ی قابل پیمایش، مثل یک Vector:

  • از یک حلقه استفاده کنید (مثل بیشتر زبان‌های برنامه‌نویسی از ابتدای رایانش دیجیتال).
  • از یک «درک» استفاده کنید (به سبک پایتون).
  • از «پخش» استفاده کنید (نگارش ویژه‌ی Julia، هرچند با وامی بزرگ به R، Matlab و NumPy).

این مفهوم روی «توابع مرتبه‌ی بالاتر» تمرکز می‌کند (که از هر زبان تابعی مثل Haskell یا F# آشناست).

رویکردهای ممکن دیگر عبارت‌اند از:

  • «بازگشت» (همان‌طور که در زبان‌های خانواده‌ی ML).
    • Julia این را مجاز می‌داند، اما بدون بهینه‌سازی فراخوانی پایانی خطر سرریز پشته وجود دارد.
  • فرابرنامه‌نویسی با ماکروها (که سنتاً ویژگی Lisp است).
    • این در برنامه‌نویسی پیشرفته‌ی Julia بسیار استفاده می‌شود، اما در بیشتر موارد با احتیاط به آن نزدیک شوید.
    • احتمالاً گزینه‌های دیگر نوشتنشان ساده‌تر و debug کردنشان بسیار آسان‌تر است.

فیلتر کردن

تابع 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.

MapReduce

ترکیب یک عملیات 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() را هم برای موقعیت‌هایی که جهت مهم است دارد.

دستورالعمل‌ها

ما در حال راه‌اندازی یک باشگاه پنیر هستیم که با یادگیری ماشین پنیرهای تازه‌ای را برای مشتریان پنیردوستمان بر اساس تاریخچه و سلیقه‌ی آن‌ها انتخاب می‌کند.

اعضای جدید باید یک نظرسنجی اولیه پر کنند تا بتوانیم چند داده‌ی پایه برای شروع جمع‌آوری کنیم. در این میان مشخص شده است که زیرمجموعه‌ای از مشتریان پرشور وجود دارند که در نقدهایشان ظرافت ندارند. از آنجا که این می‌تواند به‌طور جبران‌ناپذیری یک الگوریتم ظریف‌تر را دچار سوگیری کند، الگوریتم جداگانه‌ای برای رسیدگی به نیازهایشان راه‌اندازی شده است. از شما خواسته می‌شود چند تابع کمکی بنویسید تا داده‌هایشان را سر و سامان دهید.

Note

هرچند ممکن است راه‌های متفاوتی برای حل کارهای زیر وجود داشته باشد، هر کدام را می‌توان با یک تابع مرتبه‌بالای متفاوت حل کرد.

1. دسته‌بندی مشتریان

سیستم امتیازدهی بر پایه‌ی پنج‌ستاره است که به‌سادگی از اعداد صحیح 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

2. جدا کردن مشتریان پرشور

باید مشتریان پرشورتر را از بقیه جدا کنیم.

تابع 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]

3. تبدیل امتیازها به دودویی

از آنجا که مشتریان پرشور فقط از امتیازهای 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

4. تبدیل امتیازها به ماتریس

الگوریتم‌های ما از ورودی‌های 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
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
Julia Exercism

آماده‌اید باشگاه پنیر را شروع کنید؟

در Exercism ثبت‌نام کنید تا Julia را همراه با 35 مفهوم128 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.