איך להגדיר Closures ברובי

06/02/2017

כבר בדקות הראשונות שתכתבו קוד רובי תגיעו לבעיה הראשונה של השפה: היעדר אופרטור ++. אחרי שתתגברו על זה נוכל להתחיל לדבר על פונקציות בשפה ועל המגוון הגדול מדי של דרכים להגדיר ולהפעיל אותן.

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 או שפות מקבילות.