• בלוג
  • ruby
  • פיצ'רים חדשים ב Ruby מהשנים האחרונות (גירסאות 2.5 ו 2.6)

פיצ'רים חדשים ב Ruby מהשנים האחרונות (גירסאות 2.5 ו 2.6)

02/04/2019

גירסא חדשה של רובי יוצאת כל שנה בערך, עם רובי 2.5 שיצאה במרץ 2018 ואחריה בדצמבר יצאה 2.6. התוכנית היא לשחרר את 2.7 בדצמבר השנה, מה שנותן לנו כמה חודשים להנות משתי הגירסאות האחרונות של 2018. נעבור לראות כמה יכולות חדשות שלהן כדי שגם לכם יהיה חשק לשדרג.

1. רובי לא יחפש יותר קלאסים במקום הלא נכון

ברובי 2.5 הקוד הבא סוף סוף ייכשל:

class Staff; end
class ItemsController; end

puts Staff::ItemsController

ואני יודע שזה לא נשמע כמו איזה דבר גדול כי הרי ברור שזה אמור להיכשל: בשום שלב לא הגדרנו את Staff::ItemsController. אבל מסתבר שהחיים לא כל כך פשוטים. ב Ruby עד גירסא 2.5 ההתנהגות היתה פחות או יותר כזאת:

  1. כל פעם שהגדרנו קבוע הוא הוגדר בתוך מרחב השמות Object, מה שאומר שכשאתם מגדירים מחלקה בשם ItemsController אתם יכולים להגיע אליה גם דרך השם Object::ItemsController.

  2. כל המחלקות ב Ruby יורשות מ Object, כולל המחלקה Staff שהגדרנו בתוכנית הדוגמא.

  3. לכן, כשאנחנו מנסים לפנות ל Staff::ItemsController, ורובי מגלה ש ItemsController לא מוגדר בתוך Staff, רובי מנסה לחפש את השם הזה בתוך מרחב השמות של ההורים של Staff, כלומר בתוך מרחב השמות Object. ושם הוא נמצא.

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

הקוד הבא למשל בריילס שבור ועלול להתפוצץ בדיוק בזמן שאתם לא מוכנים לזה:

# models/item.rb
class Item < ApplicationRecord
  scope :published, -> { where(published: true) }
end

# models/staff.rb
class Staff < ApplicationRecord
end

# controllers/items_controller.rb
class ItemsController < ApplicationController
  def index
    @items = Item.published
  end
end

# controllers/staff/items_controller.rb
class Staff::ItemsController < ApplicationController
  def index
    @items = Item.all
  end
end

מנגנון הטעינה האוטומטית עלול לחשוב שהוא כבר טען את Staff::ItemsController רק בגלל שהוא כבר טען את הקלאס Staff ואת הקלאס ItemsController.

2. אפשר לכתוב Pipelines טובים יותר עם yield self ו then

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

> 10.yield_self {|n| n * 4 }
40

האמת ששם קצת יותר טוב לפונקציה הזאת שנכנס ב 2.6 הוא then, אז אפשר לכתוב את אותה הדוגמא כך:

> 10.then {|n| n * 4 }
40

ו then הגיוני יותר כי הוא באמת מרמז על השימוש הנפוץ במנגנון - והוא כתיבת Pipelines קריאים יותר. נתבונן רגע בקוד הבא:

CSV.parse(File.read(File.expand_path("data.csv", __dir__)))
   .map { |row| row[1].to_i }
   .sum

הקוד מפעיל את expand_path כדי לחשב שם של תיקיה, את התוצאה מעביר ל File.read, את התוצאה מעביר ל CSV.parse ועל האוביקט שיצא מפעיל את הפונקציות map ו sum. זה לא קריא כי אנחנו מערבבים כאן שני מנגנונים: גם הפעלת פונקציה על פונקציה על פונקציה, וגם קריאה למתודות של אוביקטים בשרשרת.

בעזרת then אפשר לעבור רק לכתיב אחד - כתיב האוביקטים. הנה גירסא קריאה יותר של אותו הקוד:

"data.csv"
    .then {|name| File.expand_path(name, __dir__) }
    .then {|fullname| File.read(fullname) }
    .then {|data| CSV.parse(data) }
    .map {|row| row[1].to_i }
    .sum

בצורה כזאת יש לנו אחידות בשיטת הקריאה והרבה יותר קל להרכיב ולפרק Pipelines או לשנות את סדר הפעולות.

דוגמא נוספת הפעם לגישה ל Github API עשויה להיראות כך:

"https://api.github.com/repos/rails/rails"
  .yield_self { |_| URI.parse(_) }
  .yield_self { |_| Net::HTTP.get(_) }
  .yield_self { |_| JSON.parse(_) }
  .yield_self { |_| _.fetch("stargazers_count") }
  .yield_self { |_| "Rails has #{_} stargazers" }
  .yield_self { |_| puts _ }

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

3. אופרטור הרכבת פונקציות חדש

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

f = -> (n) { n * 2 }
g = -> (n) { n + 1 }
(f << g).(10)
=> 22

ויש גם חץ כפול הפוך בשביל הרכבה בכיוון השני:

(f >> g).(10)
=> 21

המנגנון הזה מאפשר לבנות פונקציות באמצעות שילוב של פונקציות אחרות, למשל הדוגמא הבאה:

require 'net/http'
require 'uri'
require 'json'

fetch =
  self.method(:URI) \
  >> Net::HTTP.method(:get) \
  >> JSON.method(:parse) \
  >> -> (response) { response.dig('bpi', 'EUR', 'rate') || '0' } \
  >> -> (value) { value.gsub(',', '') } \
  >> self.method(:Float)

fetch.call('https://api.coindesk.com/v1/bpi/currentprice.json') # => 3530.6782

4. לא צריך יותר Regexp בשביל למחוק את התחילית או הסיומת

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

'demo.txt'.chomp('.txt')
=> "demo"

אבל מה לגבי התחילית? בשביל זה הייתם צריכים לדעת ביטויים רגולאריים. עד 2.5 כמובן. היום כבר אפשר להשתמש ב delete_prefix בשביל להיפטר מהתחילית. אגב בשביל האחידות נוספה גם delete_suffix שבשונה מ chomp לא תמחק לכם באופן אוטומטי את תו ירידת השורה וחייבת לקבל פרמטר. בקוד זה נראה ככה:

> "demo.txt\n".chomp
"demo.txt"

> "demo.txt\n\n".chomp("")
"demo.txt"

> "demo.txt".delete_suffix('.txt')
"demo"

> "demo.txt".delete_prefix('demo.')
"txt"

5. הפונקציה merge מקבלת כמה Hash-ים שתרצו

והפיצ'ר האחרון שאהבתי ברשימה הוא היכולת להעביר ל Hash::merge מספר פרמטרים, מה שאומר שעכשיו אפשר לכתוב:

> {}.merge({a: 10, b: 20}, {c: 30}, {d: 40})
=> {:a=>10, :b=>20, :c=>30, :d=>40}