איך להגדיר Closures ברובי
כבר בדקות הראשונות שתכתבו קוד רובי תגיעו לבעיה הראשונה של השפה: היעדר אופרטור ++
. אחרי שתתגברו על זה נוכל להתחיל לדבר על פונקציות בשפה ועל המגוון הגדול מדי של דרכים להגדיר ולהפעיל אותן.
1. הגדרת פונקציות באמצעות def
הדרך הראשונה וכנראה המרכזית להגדיר פונקציה היא הפקודה def. הפקודה מתבצעת בזמן ריצה, מוסיפה פונקציה חדשה לטבלת השמות ומחזירה את שם הפונקציה. כך הקוד הבא מגדיר פונקציה פשוטה ב ruby:
def greet
puts 'Hello World'
end
greet
אם הייתם כותבים את הקוד בתוך הגדרת מחלקה אותה המילה היתה משמשת להגדרת מתודה, כלומר פונקציה מהמחלקה.
הבעיה הראשונה עם def מופיעה כששוכחים שהיא פקודת זמן ריצה ומנסים להשתמש בה כדי להגדיר פונקציה פנימית בתוך פונקציה אחרת. הקוד הבא נכשל:
def foo
def bar
5
end
end
puts bar
השגיאה שמופיעה היא שהפונקציה bar לא מוגדרת, מה שנותן את התחושה שהצלחנו להגדיר פונקציה פנימית לפונקציה אחרת. זה שקר. בפועל מה שקורה שעד שהופעלה הפונקציה foo גם הפונקציה bar לא מוגדרת מאחר ולא הגענו לפקודה def שמגדירה אותה. כל מה שצריך בשביל לתקן זה להוסיף קריאה ל foo:
def foo
def bar
5
end
bar
end
foo
puts bar
2. הגדרת פונקציות דינמית באמצעות define_method
גירסא אחרת של def מאפשרת הגדרה דינמית של פונקציה כאשר אתם לא יודעים בזמן כתיבת הקוד מה יהיה שם הפונקציה. הלולאה הבאה למשל מגדירה 5 פוקנציות עבור צבעים:
%i(red blue green pink orange).each do |color|
define_method(color) do |text|
puts "<#{color}>#{text}</#{color}>"
end
end
blue('hello world')
# result - prints:
# <blue>hello world</blue>
3. הגדרת פונקציות פנימיות באמצעות lambda
בנוסף לפונקציות שנשמרות בטבלת השמות הרגילה של התוכנית, ולמתודות שנשמרות בטבלת השמות של כל מחלקה (שני הסוגים מוגדרים עם def
או define_method
) האפשרות הבאה היא הגדרת אוביקט מסוג Proc.
רובי כשפת Object Oriented לא נותנת לנו להתיחס לפונקציות שלה כסוגי נתונים שאפשר לשמור במשתנים או להעביר למקומות אחרים בקוד. במקום זה יש אוביקטי Proc המייצגים או עוטפים קטע קוד שאפשר להריץ. הסיפור לא מסתיים כאן מאחר ויש שני סוגים של אוביקטי Proc: הראשון נוצר על ידי הפקודה proc
ואינו בודק את הארגומנטים שהועברו לו, והשני נוצר על ידי הפקודה lambda
ויזרוק שגיאה אם תנסו להפעילו עם מספר ארגומנטים שונה ממה שהוגדר.
מאחר וניתן להגדיר ברירות מחדל לארגומנטים אני לא מוצא יותר מדי שימוש ב proc
, אבל lambda
היא מאוד חשובה ושימושית ופותרת לנו את כל בעיות ה closures שהיו בשימוש ב def.
נתחיל בהגדרת פונקציה בתוך פונקציה:
def foo
helper = lambda do |n|
n.times { puts 'Hello World!' }
end
helper.call(5)
end
foo
הפונקציה helper מוגדרת כביטוי למדה ולכן לא ניתן לגשת אליה מחוץ לפונקציה foo העוטפת אותה. התחביר השונה לא עוצר בהגדרת הפונקציה וממשיך גם לאופן הפעלתה לציין שסוגריים עגולים מפעילים פונקציה לפי שם, אבל כשיש לנו אוביקט Proc יש להשתמש בפונקציה call שלו כדי להפעיל אותו.
נראה דוגמא נוספת והפעם נגדיר Closure באמצעות lambda. הקוד הבא מדפיס 10 פעמים את הברכה המסורתית:
counter = lambda do |n|
i = 0
lambda do
i += 1
i > n ? nil : i
end
end
c = counter.call(10)
while res = c.call
puts 'Hello World!'
end
4. סיכום ומסקנות
התבנית של שמירת פונקציה במשתנה כאוביקט היא לא המצאה של Ruby ואנו מוצאים אותה גם ב Java וגם ב C# כמעט באותו האופן. זה לא אומר שהיא טובה. דווקא הגישה של JavaScript ושפות נוספות לאפשר הגדרה ושימוש בפונקציות כסוג נתונים רגיל בשפה היא הרבה יותר אינטואיטיבית.
יחד עם זאת ולמרות עומס האופרטורים והסימנים ברגע שמבינים את ההבדל בין הגדרת פונקציה לבין יצירת Proc ומבינים את היכולות השונות של כל אחד מהם אפשר לכתוב תוכניות רובי חזקות בדיוק כפי שהייתם כותבים ב JavaScript או שפות מקבילות.