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

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

הדבר הכי גרוע שיכול לקרות לפרויקט עם בדיקות אוטומטיות

10/12/2019

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

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

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

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

הדרך הכי טובה להתעלם מתיקיה או קובץ עם find

09/12/2019

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

אז בואו ניזכר רגע בהפעלה פשוטה של find למשל בשביל למצוא קבצים שהשם שלהם הוא package.json:

$ find . -type f -name package.json

הפקודה find מקבלת רצף של קריטריונים לחיפוש ובונה מהם שרשרת תנאים. במקביל היא רצה על כל הקבצים והתיקיות החל מתיקיית ההתחלה שלה ומעבירה כל נתיב בשרשרת התנאים שמצאה. במקרה שלנו למשל שרשרת התנאים היא:

  1. אנחנו מחפשים קובץ
  2. השם חייב להיות package.json

כש find מוצא נתיב למשל demo.txt קודם כל הוא בודק מה סוג הדבר שיש שם. בואו נניח ש demo.txt הוא קובץ אז הוא יעבור את החוליה הראשונה וייפול בשניה.

שרשראות תנאים יכולות גם להתפצל, למשל ה find הבא שמשתמש ב or יזהה קבצים בשם package.json או תיקיות בשם node_modules:

$ find . \( -type f -name package.json \) -or \( -type d -name node_modules \)

והנה עלינו לשתי שרשראות: או שעברת את כל השרשרת הראשונה, או שעברת את כל השרשרת השניה. כל שרשרת מורכבת משני תנאים.

עכשיו הגיע הזמן לשאול מה קורה עם הנתיב אחרי שהשרשרת נגמרת? באופן רגיל נתיב שסיים את כל השרשרת פשוט יודפס, אבל find נותנת לנו לשחק גם עם התנהגות זו. הפרמטר --exec הוא דוגמא לפקודה שמסיימת את השרשרת, באמצעות הרצת פקודה על הקובץ שנמצא. זאת הסיבה שהשורה הבאה תמחק לכם קובץ בשם demo.txt למרות שאתם חושבים שאתם מדברים על תיקיה:

$ find . -name 'one.txt' -exec ls -l {} \; -type d

הבעיה היא שיצרנו שרשרת שמסתיימת ב exec ובזה הסיפור שלנו נגמר. הפקודה -type d היא חסרת משמעות כאן כי היא באה אחרי סוף השרשרת.

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

$ find . -type f -not -name 'one.txt' -name 'o*.txt'

ואם רוצים להתעלם מתיקיות הפקודה prune גורמת ל find לסיים את השרשרת ולדלג על התיקיה (היא רלוונטית רק כשהנתיב הוא תיקיה). לכן הפקודה הבאה תחפש את הקובץ package.json בכל מקום מלבד בתוך תיקיית node_modules:

$ find . -type d -name node_modules -prune -false -or -name package.json

ברירת המחדל של -prune היא להדפיס את התיקיה עליה היא דילגה, והתוספת -false מיד אחריה מדלגת על אותה הדפסה. בלי זה נקבל בפלט גם את התיקיה node_modules עליה דילגנו.

שקרים לבנים קטנים

08/12/2019

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

הספריה Immer לקחה את הגישה המתחזה. הקוד הזה לא רומז בשום צורה למה שבאמת קורה בו:

import produce from "immer"

const byId = produce((draft, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            action.products.forEach(product => {
                draft[product.id] = product
            })
            return
    }
})

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

const byId = (state, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            return {
                ...state,
                ...action.products.reduce((obj, product) => {
                    obj[product.id] = product
                    return obj
                }, {})
            }
        default:
            return state
    }
}

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

const byId = (state, action) => {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
          const productsData = action.products.reduce((obj, product) => {
            obj[product.id] = product
            return obj
          }, {})
          const immutableProductsData = Immutable.fromJS(productsData);
          return state.merge(immutableProductsData);

        default:
            return state
    }
}

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

אז מה עדיף? כמו תמיד בחיים - זה תלוי:

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

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

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

מבוא זריז ל Redux

07/12/2019

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

(וכן - ניסיתי את MobX, לא נפלתי מהכסא, ואני נשאר עם רידאקס). מוכנים? נמשיך לקוד.

המשך קריאה

שניהם טובים

06/12/2019

אנגולר או ריאקט? אולי בעצם ויו?

ריילס או נוד? או שאולי בכלל פלאסק?

קלוז'ר או אליקסיר?

פייתון או Java?

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

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

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

חיפוש סיפרה כפולה ב Clojure בהשוואה לפייתון

05/12/2019

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

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

def chunks(seq):
    chunk = []

    for val in seq:
        if len(chunk) == 0:
            chunk.append(val)
            continue

        if chunk[-1] == val:
            chunk.append(val)
            continue

        yield(chunk)
        chunk = [val]

    else:
        yield(chunk)

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

import itertools

def find_sequence_of_size(number, sequence_size=2):
    for key, chunk in itertools.groupby(str(number)):
        if len(list(chunk)) == sequence_size:
            return True

    return False

בקלוז'ר הפיתרון ממש דומה ואני חושב שזה נחמד שפונקציות עזר שאנחנו רגילים אליהן משפה אחת מוצאות את דרכן גם לשפות אחרות. ל groupby הם קוראים כאן partition-by אבל אנחנו לא לוקחים ללב הבדלים בשמות.

הקוד המתאים בקלוז'ר נראה כך:

(defn only-two-adjacent-digits
  [num]
  (let [
        snum (str num)
        chunks (partition-by identity snum)
        ]
    (not (empty? (filter (fn [chunk] (= (count chunk) 2)) chunks)))))

אפקט חדר הכושר

04/12/2019

ביום הראשון 50,852 מתכנתים הגישו לאריק ווסטל את הפיתרון לחידה. ביום השני המספר ירד ל 34,373 והיום נכון לזמן כתיבת הפוסט אנחנו על 13,481, כך לפי דף הסטטיסטיקה של הפרויקט. הירידה הולכת עוד להימשך ואנחנו מכירים את התופעה מכל אתגר ארוך שהיינו בו.

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

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

שימוש חוזר בקוד עם קלוז'ר ומפות

03/12/2019

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

https://adventofcode.com/2019/day/2

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

(defn process-add
  [ip program]
  (let [
        [opcode idx1 idx2 idx3] (subvec program ip)
        val1 (nth program idx1)
        val2 (nth program idx2)
        ]

    (assoc program idx3 (+ val1 val2))
    ))

(defn process-mul
  [ip program]
  (let [
        [opcode idx1 idx2 idx3] (subvec program ip)
        val1 (nth program idx1)
        val2 (nth program idx2)
        ]

    (assoc program idx3 (* val1 val2))
    ))

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

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

(defn process
  [ip program]
  (let [
        [opcode idx1 idx2 idx3] (subvec program ip)
        val1 (nth program idx1)
        val2 (nth program idx2)
        op (get operators opcode)
        ]

    (case opcode
      1 (assoc program idx3 (+ val1 val2))
      2 (assoc program idx3 (* val1 val2))
    )))

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

שפות תכנות רבות (וקלוז'ר ביניהן) מאפשרות להעביר את הפונקציה עצמה של הכפל או החיבור בתור פרמטר, או לשמור אותם בתור ערכים ב Hash Map. שמירה כזו אומרת שנוכל "לשלוף" את הפעולה מה Hash Map במקום לכתוב פקודת switch/case ארוכה שמכסה את כל המקרים.

שכתוב הקוד שישתמש ב Hash Map נותן את הפונקציה הבאה:

(def operators 
  {
   1 +
   2 * 
   })

(defn process
  [ip program]
  (let [
        [opcode idx1 idx2 idx3] (subvec program ip)
        val1 (nth program idx1)
        val2 (nth program idx2)
        op (get operators opcode)
        ]

    (assoc program idx3 (op val1 val2))
    ))

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

הודעת השגיאה "can only recur from tail position" ב Clojure

02/12/2019

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

Can only recur from tail position

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

(defn factorial-no-tail
  [n]
  (cond
        (< n 1) n
        :else (* n (recur (dec n)))))

הפונקציה factorial-no-tail היא פונקציה רקורסיבית פשוטה לחישוב עצרת, שאם תרשמו אותה בתוכנית קלוז'ר התוכנית תסרב להתקמפל ולרוץ. אז מה קורה כאן?

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

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

4! = 4 * 3!
             3 * 2!
                   2 * 1!
                         1
=>
4! = 4 * 3 * 2 * 1 = 24

בחישוב 4! נרשום בצד את 4 ואז נלך לחשב את 3!. בשביל למצוא את 3! נרשום בצד את 3 ואז נלך לחשב את 2!, בשבילו נרשום בצד את 2 ונלך לחשב את 1! ולשמחתנו 1! הוא 1. עכשיו מתחילים לגלגל אחורה ובעזרת ה-1 מגלים ש 2! הוא 2, בעזרתו אפשר לגלות ש 3! הוא 6 ואיתו נגלה ש 4! הוא 4.

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

(defn factorial-tail
  [n]
  (loop [n n acc 1]
    (cond
      (< n 1) acc
      :else (recur (dec n) (* n acc)))))

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

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

(defn factorial-recursive
  [n]
  (cond
        (< n 1) 1
        :else (* n (factorial-recursive (dec n)))))

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

הלינקיה סיכום 2019

01/12/2019

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

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

המשך קריאה