בלוקים ב Ruby

ברובי יש פעולה ייחודית של העברת בלוק לפונקציה, ועוד הם הוסיפו שתי דרכים לעשות את זה עם הבדל קטן ביניהן. בואו נראה למה צריך להעביר בלוקים לפונקציות ומה ההבדל בין שתי הדרכים.

1. העברת בלוק לפונקציה

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

const arr = [10, 20, 30];
const doubles = arr.map(x => x * 2);
const doubles2 = arr.map(function(x) { return x * 2; });

הכתיב הזה גנרי - הוא מאפשר להעביר את פונקציית ה Callback בתור הפרמטר הראשון, השני או החמישי, או להעביר מספר פונקציות Callback. מצד שני זה אומר שמבחינת המתכנת צריך להסתכל בתיעוד ולזכור בדיוק מה סדר הפרמטרים בפונקציות שמקבלות גם בלוק וגם משתנה. קחו את Reduce של JavaScript בשביל הדוגמא:

const arr = [10, 20, 30];
const sum = arr.reduce((acc, val) => acc + val, 0)

אבל אם תעברו ל Node תגלו שהסדר יכול בקלות להתהפך למשל בפונקציה readFile:

const fs = require('fs');
fs.readFile('/etc/shells', (err, data) => {
    console.log(data);
});

רובי לוקחת גישה יותר אחידה ועבור פונקציה שלוקחת בלוק יחיד בתור פרמטר ברובי יש תחביר מיוחד. הנה map של רובי:

arr = [10, 20, 30]
doubles = arr.map { |x| x * 2 }
doubles2 = arr.map do |x|
    x * 2
end

שתי הגירסאות עובדות ושתיהן יחזירו מערך עם 20, 40, 60. כשמעבירים פרמטרים הבלוק תמיד יימצא בגיזרה משלו למשל בחישוב סכום:

arr = [10, 20, 30]
arr.reduce(0) {|acc, val| acc + val}

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

2. ומה ההבדל בין שני סוגי הבלוקים?

ועדיין נשארת השאלה - למה לא להסתפק בתחביר אחד? למה צריך שתי דרכים להגדיר בלוק? שני קטעי הקוד האלה יעשו בדיוק את אותו דבר ושניהם ידפיסו בסוף 60:

arr = [10, 20, 30]
arr.reduce(0) {|acc, val| acc + val}
arr.reduce(0) do |acc, val| acc + val; end

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

 puts arr.reduce(0) {|acc, val| acc + val}
 60

אבל הדוגמא השניה נשברת:

puts arr.reduce(0) do |acc, val| acc + val; end
TypeError: 0 is not a symbol nor a string
    from (irb):6:in `reduce'
    from (irb):6
    from /Users/ynonperek/.rvm/rubies/ruby-2.4.4/bin/irb:11:in `<main>'

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