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

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

טיפ סקאלה: הדפסת דיבג באמצע Pipeline

04/12/2023

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

(->> 10
  (range)
  (map #(* %1 %1))
  (reduce +)
  (println))

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

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

(defn debug [n]
  (println n)
  n)

(->> 10
  (debug)
  (range)
  (debug)
  (map #(* %1 %1))
  (debug)
  (reduce +)
  (println))

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

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

  println(
    (0 to 9)
      .map(_.pow(2))
      .sum
  )

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

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

extension [A](value: A)
  def debug(): A =
    println(value)
    value
}

@main
def main(): Unit =
  println(
    (0 to 9)
      .debug()
      .map(_.pow(2))
      .debug()
      .sum
  )

והפלט:

NumericRange 0 to 9
Vector(0, 1, 4, 9, 16, 25, 36, 49, 64, 81)
285

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


extension [A](value: A)
  def debug(): A =
    println(value)
    value

  def eagerDebug(): A = value match {
    case it: Iterable[_] =>
      println(it.toList)
      value
    case it: Iterator[_] =>
      println(it.toList)
      value
    case _ =>
      println(value.getClass)
      value
  }

@main
def main(): Unit =
  println(
    (0 to 9)
      .eagerDebug()
      .map(_.pow(2))
      .debug()
      .sum
  )

ואם כבר הוספנו את debug נוכל להשתמש בה גם בסוף הפייפליין כדי להדפיס בסגנון קלוז'ר:

@main
def main(): Unit =
  (0 to 9)
    .eagerDebug()
    .map(_.pow(2))
    .debug()
    .sum
    .debug()

סקריפטים ב JavaScript? תנו צ'אנס לדינוזאור

03/12/2023

אחת הבעיות בשימוש ב node.js לכתיבת סקריפטים היא הקובץ package.json, או יותר נכון הצורך לצרף אותו לסקריפט כדי להתקין את התלויות. התוכנית הבאה ב node.js מתוך קוד הדוגמה של המודול wikipedia על npm לא יכולה לרוץ לבד:

const wiki = require('wikipedia');

(async () => {
    try {
        const page = await wiki.page('Batman');
        console.log(page);
        //Response of type @Page object
        const summary = await page.summary();
        console.log(summary);
        //Response of type @wikiSummary - contains the intro and the main image
    } catch (error) {
        console.log(error);
        //=> Typeof wikiError
    }
})();

בשביל שהיא תעבוד אני צריך ליצור איתה בתיקיה קובץ package.json ולהפעיל npm install או להפעיל

npm install wikipedia

משורת הפקודה לפני הרצה.

עכשיו בואו נראה איך אותו טריק יעבוד עם דינו. דבר ראשון צריך לשנות את שורת ה require ל import, ולציין שאני טוען את המודול מ npm כך שקוד התוכנית יהיה:

import wiki from 'npm:wikipedia';


(async () => {
    try {
        const page = await wiki.page('Batman');
        console.log(page);
        //Response of type @Page object
        const summary = await page.summary();
        console.log(summary);
        //Response of type @wikiSummary - contains the intro and the main image
    } catch (error) {
        console.log(error);
        //=> Typeof wikiError
    }
})();

ומכאן זה רק משתפר. אני מריץ את הקוד ודינו מתחיל להזהיר אותי:

$ deno run demo.js

⚠️  ┌ Deno requests env access to "npm_config_no_proxy".
   ├ Run again with --allow-env to bypass this prompt.
   └ Allow? [y/n] (y = yes, allow; n = no, deny) >

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

$ deno run demo.js

✅ Granted env access to "npm_config_no_proxy".
✅ Granted env access to "NPM_CONFIG_NO_PROXY".
✅ Granted env access to "no_proxy".
✅ Granted env access to "NO_PROXY".
✅ Granted env access to "npm_config_https_proxy".
✅ Granted env access to "NPM_CONFIG_HTTPS_PROXY".
✅ Granted env access to "https_proxy".
✅ Granted env access to "HTTPS_PROXY".
✅ Granted env access to "npm_config_proxy".
✅ Granted env access to "NPM_CONFIG_PROXY".
✅ Granted env access to "all_proxy".
✅ Granted env access to "ALL_PROXY".
✅ Granted read access to "/Users/ynonp/Library/Caches/deno/npm/node_modules".
✅ Granted read access to "/Users/ynonp/Library/Caches/deno/node_modules".
✅ Granted read access to "/Users/ynonp/Library/Caches/node_modules".
✅ Granted read access to "/Users/ynonp/Library/node_modules".
✅ Granted read access to "/Users/ynonp/node_modules".
✅ Granted read access to "/Users/node_modules".
✅ Granted read access to "/node_modules".
✅ Granted net access to "en.wikipedia.org".

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

$ deno run --allow-net --allow-read --allow-env demo.js

כדי לדלג על השאלות.

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

  1. אין צורך בקובץ package.json, כל התלויות רשומות בתוך קובץ הסקריפט וההתקנה קורית אוטומטית בהרצה הראשונה.

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

בדיקות בתור First Class Citizen

01/12/2023

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

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

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

  1. נעדיף להשתמש בבסיס נתונים שיכול לרוץ מקומית מתוך קוד הבדיקה, במקום בבסיס נתונים שמחייב חיבור לרשת.

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

  3. נעטוף קוד שמגיע מספריות חיצוניות כדי שיהיה יותר קל להחליף אותו בקוד בדיקה צפוי.

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

קוד אידמפוטנטי

30/11/2023

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

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

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

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

זה הממשק, טמבל

28/11/2023

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

max_found = 0
for i in range(100, 1_000):
    for j in range(100, 1_000):
        product = i * j
        if str(product) == str(product)[::-1] and product > max_found:
            max_found = product

print(max_found)

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

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

  2. איתור כל המספרים מתוכם שהם פלינדרומים.

  3. חישוב המספר המקסימלי.

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

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

import itertools

three_digit_numbers = range(10, 1_000)
pairs = itertools.product(three_digit_numbers, repeat=2)
products = (i * j for (i, j) in pairs)
palindroms = (p for p in products if str(p) == str(p)[::-1])
print(max(palindroms))

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

איך למנוע באגים (שאלה של גישה)

27/11/2023

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

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

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

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

5 טיפים למבחני בית טובים יותר

26/11/2023

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

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

המשך קריאה

היום למדתי: שלוש נקודות ברובי

25/11/2023

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

def bar(x, y, z, **extra)
  puts "x = #{x}, y = #{y}, z = #{z}, #{extra}"
end

def foo(...)
  bar(...)
end

foo(10, 20, 30, hello: "world")

האופרטור מחליף תבנית ישנה יותר וקצת יותר מסורבלת שגם קיימת בפייתון:

def bar(x, y, z, **extra)
  puts "x = #{x}, y = #{y}, z = #{z}, #{extra}"
end

def foo(*args, **kwargs)
  bar(*args, **kwargs)
end

foo(10, 20, 30, hello: "world")

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