הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

מימוש סינגלטון ב Ruby

12/12/2018

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

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

class Critter
  @@instance = Critter.new
  def self.instance
    @@instance
  end

  def val
    5
  end
end

ואכן יש לנו מחלקה Critter עם מתודה בשם instance שמחזירה תמיד את אותו אוביקט:

c = Critter.instance
d = Critter.instance

puts c == d # true

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

require 'singleton'

class Critter
  include Singleton
  def val
    5
  end
end

c = Critter.instance
d = Critter.instance

puts c.val
puts d == c

הבעיות מתחילות כשננסה להסתיר את הפונקציה new. כאן לרובי יש סוג של פיתרון באמצעות הפונקציה private_class_method. כך נראה Critter שמסתיר את פונקציית new שלו:

class Critter
  include Singleton
  def val
    5
  end
  private_class_method :new
end

ובאמת מי שינסה לקרוא עכשיו ל Critter.new יקבל את השגיאה:

NoMethodError: private method `new' called for Critter:Class

הבעיה שרובי מספקת עוד כמה דרכים להפעיל פונקציות, לדוגמא באמצעות הפקודה send. הקוד הבא עובד ומדפיס false:

require 'singleton'

class Critter
  include Singleton
  def val
    5
  end
  private_class_method :new
end

c = Critter.instance
d = Critter.send(:new)

puts d.val # print 5
puts d == c # false

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

טיפ רובי: בואו נחליף את הסוף של המחרוזת

11/12/2018

ל Bash יש פיצ'ר מדליק שמחליף במהירות את הסוף של מילה, מה שעוזר לנו לכתוב סקריפטים שמשנים שמות של קבצים. הקוד הבא ב Bash יחליף במהירות את כל הקבצים שמסתיימים ב old לסיומת new:

for fname in *.old
do
    mv "$fname" "${fname%old}new"
done

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

בקוד זה נראה כך:

glob("*.old").each do |fname|
    File.rename(fname, fname.chomp("old") + "new")
end

קסם נוסף שיש ל Bash בשרוול הוא מחיקת קטע מהתחלה של מחרוזת עם סימן הסולמית - אבל לא הצלחתי למצוא מקבילה טובה ברובי אליו.

מה שהלקוח רוצה

10/12/2018

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

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

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

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

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

טיפ קצר: בואו נגדיר קבועים ב Node.JS

09/12/2018

הפקודה const מגדירה ערך בתור קבוע נכון? אז למה כל כך מסובך להשתמש בה נכון ב Node, ואיך אפשר להגדיר קבוע שקבצים בכל התוכנית יכירו אותו?

נתחיל עם מה שעובד - כל עוד אנחנו בקובץ יחיד אפשר להשתמש ב const כדי לתת שם לערך קבוע:

const MESSAGE_SIZE = 48;

ומכאן ועד סוף הקובץ לא משנה מה תעשו השם MESSAGE_SIZE תמיד יתייחס למספר הקבוע 48. אבל העסק מסתבך מהר מאוד כשאנחנו רוצים לשתף מידע בין מספר קבצים. נניח שהקובץ utils.js מגדיר הפעם את הקבוע ומייצא אותו:

// utils.js
const MESSAGE_SIZE = 48;
exports.MESSAGE_SIZE = MESSAGE_SIZE;

והקובץ app.js מייבא את הקבוע:

const { MESSAGE_SIZE } = require('./utils');
console.log(MESSAGE_SIZE);

זה עבד לא רע והדפיס 48, אבל הולך להישבר די בקלות. נסו לכתוב במקום את הקוד הבא ב app.js:

const utils = require('./utils');
utils.MESSAGE_SIZE = 999;

console.log(utils.MESSAGE_SIZE);

הקוד ידפיס 999 וגם כל קובץ אחר שיטען את הקבוע מ utils יקבל עכשיו את הערך 999. מסתבר שברגע שאנחנו עוברים להשתמש במנגנון ה exports מה שאנחנו בעצם מייצאים הוא אוביקט. המילה const לא מונעת שינויים בשדות של האוביקט וכך כל מי שרוצה יכול לשנות את ה"קבועים" שלנו.

מה אפשר לעשות? אז אומנם const לא תעזור לשמור על שדות של אוביקטים משינויים, אבל Object.freeze דווקא כן. אם נפעיל אותה לפני ה export נוכל לקבל קבועים של ממש.

החליפו את תוכן הקובץ utils.js עם הקוד הבא:

module.exports = Object.freeze({
  MESSAGE_SIZE: 48,
});

ועכשיו בלי לשנות את app אפשר להריץ אותו שוב ולקבל את ערך הקבוע 48. מספר זה לא ישתנה לא משנה מה נכתוב בקבצים שטוענים אותו.

איך להגיב לשאלה שאת כבר מכירה בראיון עבודה?

08/12/2018

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

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

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

לכן הגישה שלי היא כן להתיחס לנושא ולעשות מאמץ להעלות את רמת הדיון. משפט כמו "איזה כיף שאתה שואל את זה! בדיוק התכוננתי על הנושא הזה השבוע" הוא נקודת פתיחה טובה. אחרי זה אפשר להמשיך עם התשובה לשאלה המקורית (אותה את כבר יודעת) ולנצל את ההזדמנות להעלות עוד התלבטויות והתעמקויות לגבי נושא זה.

דוגמא? יאללה בשמחה-

המראיין: מהי בעיית N+1 בעת שליפת נתונים מ DB, ואיך היית מתמודד איתה?

מרואיין: איזה כיף ששאלת את זה, בדיוק עברתי על זה כשהתכוננתי לשיחה היום. הבעיה קורית כשאנחנו רוצים לטעון מידע מבסיס הנתונים אבל משתמשים בשאילתה נפרדת לכל רשומה (N רשומות מייצרות N שאילתות, פלוס שאילתה אחת ראשונית ששולפת את מזהי הרשומות שאנחנו צריכים). הבעיה אופיינית במערכות עם ORM. [ממשיך לספר על דרכי ההתמודדות] ואז שואל, אגב אצלכם בחברה אתם משתמשים בכלים אוטומטיים לזהות בעיות כאלה? איך אתם מוודאים שמתכנתים לא יגרמו לבעיית ביצועים מסוג זה?

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

שבוע אחר כך ועם הקוד הלא נכון

07/12/2018

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

אבל כמובן שזה משנה.

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

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

בלוקים ב Ruby

06/12/2018

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

המשך קריאה

הבעיה עם תכנות מונחה עצמים

05/12/2018

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

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

המשך קריאה

מצאו את ההבדלים

04/12/2018

שני קטעי הקוד האלה ברובי מדפיסים את אותו ערך ונראים מאוד דומה:

require 'set'

s = Set.new([10, 20, 30])
t = Set.new([30, 40])
puts (s | t).inspect
puts s.merge(t).inspect

בשני המקרים תוצאת ההדפסה היא:

#<Set: {10, 20, 30, 40}>

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

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

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

עכשיו אתם ועדיין ברובי- מה ההבדל בין ארבעת אלה? מה יכול להשתבש? ואיך?

arr = [10, 20, 30]

arr.inject(0) { |acc, val| acc + val }
arr.inject { |acc, val| acc + val }
arr.inject do |acc, val| acc + val; end
arr.inject(0) do |acc, val| acc + val; end

טרנספורמציות של סדרות

03/12/2018

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

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

numbers = (0..Float::INFINITY)

זאת סידרת המספרים 0,1,2,3,4,5,6 וכן הלאה עד אין סוף. אנחנו יודעים שאם נכפיל כל איבר בסידרה ב-2 נקבל את הסידרה 0,2,4,6,8,10,12 כלומר כל המספרים הזוגיים - ואנחנו יכולים להשתמש ב map כדי לעשות את זה ב Ruby:

even = (0..Float::INFINITY).lazy.map {|i| i * 2}

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

puts (0..Float::INFINITY).lazy.map {|i| i * 2}.first(10)

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

boy
girl
helllo
good
boxer

אז המילה helllo עם שגיאת הכתיב כוללת את האות l שלוש פעמים, וזה הכי הרבה שאות מופיעה במילה בכל הרשימה. אם נרצה לגשת לבעיה דרך סדרות נוכל לקחת את רשימת המילים ולהפוך אותה לרשימה של Hash-ים כך שבמקום לשמור מילה נשמור כמה פעמים כל אות מופיעה בה. במילים אחרות נשתמש ב map כדי להפוך את רשימת המילים לרשימת מבני הנתונים הבאה:

["boy", {1=>["b", "o", "y"]}]
["girl", {1=>["g", "i", "r", "l"]}]
["helllo", {1=>["h", "e", "o"], 3=>["l", "l", "l"]}]
["good", {1=>["g", "d"], 2=>["o", "o"]}]
["boxer", {1=>["b", "o", "x", "e", "r"]}]

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

["helllo", {1=>["h", "e", "o"], 3=>["l", "l", "l"]}]
["good", {1=>["g", "d"], 2=>["o", "o"]}]
["boxer", {1=>["b", "o", "x", "e", "r"]}]
["boy", {1=>["b", "o", "y"]}]
["girl", {1=>["g", "i", "r", "l"]}]

בה האיבר הראשון הוא בדיוק המילה שחיפשתי.

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

class String
  def count
    Hash.new(0).tap { |h| self.each_char { |word| h[word] += 1 } }
  end

  def icount
    counts = self.count
    each_char.group_by {|c| counts[c]}
  end
end

words = [
  'boy',
  'girl',
  'helllo',
  'good',
  'boxer',
]

puts words
  .map {|w| [w, w.icount]}
  .sort {|a, b| a[1].keys.max - b[1].keys.max}
  .reverse
  .map {|p| p[0]}
  .first

שימוש בסדרות ומיפויים שלהן כדי לפתור בעיות נותן לכם להשתמש בהרבה קוד קיים של השפה (כגון sort, reverse ו first במקרה שלנו, ופקודות רבות נוספות במקרים אחרים) כדי להתקדם בפיתרון הבעיה שלכם. המשימה הקשה כאן היא כמובן לזהות את הסידרה והמיפויים המתאימים ביותר לבעיה.