שלום אורח התחבר

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

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

מעדיפים לקרוא מהטלגרם? בקרו אותנו ב:@tocodeil

או הזינו את כתובת המייל וקבלו את הפוסט היומי בכל בוקר אליכם לתיבה:

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

>>> 'one two three'.split(' ')
['one', 'two', 'three']

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

>>> 'one   two   three'.split(' ')
['one', '', '', 'two', '', '', 'three']

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

>>> 'one    two   three'.split(None)
['one', 'two', 'three']
>>> 'one    two   three'.split()
['one', 'two', 'three']

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

>>> '-'.join(['yo', 'dog'])
'yo-dog'

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

>>> text = 'split    and    join'
>>> ' '.join(text.split(' ')) == text
True

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

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

חיסרון שני בגישה הוא שאנחנו תקועים עם הממשק וחיבור המקשים שג'ימייל בחרו בשבילנו.

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

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

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

  1. לשמור עותק של כל המיילים אצלכם על המכונה

  2. לקרוא ולכתוב אימייל דרך תוכנת דואר משורת הפקודה שנקראת mutt

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

המשך קריאה...

  • יש לך הנדימן להמליץ לי?

  • יש לך טכנאי מזגנים טוב להמליץ?

  • יש לך טכנאי מזגנים שמכיר טוב מזגני טורנדו?

  • יש לך טכנאי מזגנים שמכיר טוב מזגני טורנדו ישנים?

  • יש לך טכנאי מזגנים שמכיר טוב מזגני LG תעשייתיים משנת 2008?

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

בתור פרילאנסרים בתוכנה אתם יכולים לבחור את המשבצת שלכם. במקום להיות "מתכנתת Mobile" את יכולה לבחור להיות:

  • זאת שיודעת למצוא ולתקן זליגות זיכרון ביישומי Objective C

  • זאת שיודעת להתאים את הקוד לכל מכשירי Android שיצאו אי פעם

  • זאת שיודעת לכתוב פלאגינים ל React Native כדי לחבר יותר יכולות של המכשיר לאפליקציות RN

  • זאת שיודעת איך לתקן את הקוד כדי שאפל יסכימו להכניס את האפליקציה לחנות

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

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

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

...

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

"אתה לא תצליח לכתוב את הקוד הזה"

"ממילא המערכת תהיה מלאה באגים"

"אין סיכוי שזה יעבוד אי פעם"

"לא חבל לבזבז את הזמן על דברים שלא יצא מהם כלום?"

"די"

"הגיע הזמן לעשות הפסקה"

"ארוכה"

"צא"

"לך"

"תברח!"

...

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

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

אחד הפוסטים שעלה היום בהאקרניוז סיפר על סימן חלוקה חמוד ב 19:

  1. מסתכלים על מספר מימין לשמאל (סיפרה אחרי סיפרה)

  2. כופלים את הסיפרה הימנית ביותר ב 2 ומחברים לה את הסיפרה הבאה

  3. את התוצאה כופלים ב 2 ומחברים לה את הסיפרה הבאה

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

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

function reduceMod19(n) {
  let accumulator = 0;
  while (n > 0) {
    let digit = n % 10;
    accumulator = accumulator * 2 + digit;
    n = Math.floor(n / 10);
  }

  return accumulator;
}

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

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

digits.reduce((acc, val) => acc * 2 + val, 0);

אני הולך לעבור על כל הרשימה שנקראת digits אחד-אחד, ועבור כל אלמנט אני אכפול את מה שחישבתי עד עכשיו ב-2 ואוסיף את הדבר החדש.

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

function reduceMod19(n) {
  let digits = String(n).split('').map(d => Number(d));
  return digits.reverse().reduce((acc, val) => acc * 2 + val, 0);
}

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

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

מתכנתים לא אוהבים לכתוב CSS ויש לזה כל מיני סיבות. אפילו אנשי Front End רבים שפגשתי העדיפו להתמקד בצד הטכני יותר של פיתוח JavaScript או React ולהשאיר את ה CSS למעצבים. במקביל ככל שהעולם של ה CSS התפתח כך זה רק עוד יותר הפחיד רבים מאיתנו: עכשיו ללמוד Flexbox ו grid? ובאיזה דפדפנים כל דבר עובד? וממילא מה שאני לא אעשה זה לא יראה טוב אז בשביל מה להתאמץ.

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

המשך קריאה...

כבר שנים שמדברים על אופרטור ה Pipeline ב JavaScript שיהפוך את הכתיבה הפונקציונאלית בשפה לקלה יותר, אבל נכון להיום הוא עדיין ב stage 1 ולא נראה שמתקדם. והנה פתאום פגשתי את המשפט הבא בתיעוד על ranges של C++20:

Range adaptors accept viewable range as their first arguments and return a view. If an adaptor takes only one argument, it can also be called using the pipe operator: if C is a range adaptor object and R is a viewable_range, these two expressions are equivalent:

ואם אתם לא בטוחים מה זה אומר הם גם נותנים שם את דוגמת הקוד הבאה להמחשה:

#include <vector>
#include <ranges>
#include <iostream>

int main()
{
    std::vector<int> ints{0,1,2,3,4,5};
    auto even = [](int i){ return 0 == i % 2; };
    auto square = [](int i) { return i * i; };

    for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
        std::cout << i << ' ';
    }
}

שימו לב לקו האנכי:

  1. מתחילים בוקטור של המספרים מ-0 עד 5.

  2. שולחים אותו לפונקציה filter כדי להשאיר רק את המספרים הזוגיים.

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

סך הכל יודפס:

0 4 16

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

דרישות חדשות יכולות להגיע משתי סיבות:

  1. או שלא ראית משהו שהיית צריך לראות כשדיברת עם הלקוח.

  2. או שהלקוח שינה את דעתו.

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

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

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

הפקודה הראשונה שקופצת לראש היא ls -t שמסדרת את רשימת הקבצים לפי תאריכי עדכון. הפקודה הבאה מציגה את 3 הקבצים העדכניים ביותר בתיקיה:

$ ls -rtlh | tail -3

אבל זה עדיין דורש copy-paste אם רוצים "לעשות משהו" עם הקובץ האחרון. עד שאנחנו נזכרים בסימן ה $() המופלא של bash. אם הדבר העדכני ביותר הוא תיקיה אז הפקודה הבאה תיכנס אליה:

cd $(ls -rt | tail -1)

ובשביל שיהיה קל יותר להשתמש ברצף אני יוצר עבורו alias:

alias latest='ls -rt | tail -1'

ועכשיו אפשר לכתוב פקודות כמו:

rm $(latest)

כדי למחוק את הקובץ האחרון שהורדתי, או:

$ xdg-open $(latest)

כדי להריץ אותו בתוכנית ברירת המחדל של המערכת.

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

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

def reduce_polymer(polymer):
    i = 0
    while i < len(polymer) - 1:
        if reducts_with(polymer[i], polymer[i+1]):
            polymer = polymer[0:i] + polymer[i+2:]
            i -= 1
            continue

        i += 1

    return polymer


def reducts_with(unit1, unit2):
    return abs(ord(unit1) - ord(unit2)) == 32

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

from aoc2018day5 import reduce_polymer


def test_aA():
    assert reduce_polymer("aA") == ""


def test_bug():
    assert reduce_polymer("aAff") == "ff"


def test_abBA():
    assert reduce_polymer("abBA") == ""


def test_abAB():
    assert reduce_polymer("abAB") == "abAB"


def test_dabAcCaCBAcCcaDA():
    assert reduce_polymer("dabAcCaCBAcCcaDA") == "dabCBAcaDA"

שעברה בלי בעיה.

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

...

עוד לא מצאתם? שימו לב לאינדקסים

...

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

הקוד הבא למשל מכניס את הפונקציה ללולאה אינסופית:

if __name__ == "__main__":
    print(len(reduce_polymer("aAaAffaA")))

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

  1. בשורת התנאי אנחנו בונים על זה ש i אינו שלילי, אבל לא מוודאים את זה.

  2. בשורת המחיקה אנחנו בונים על זה ש i אינו שלילי, ושוב לא דואגים לספר על זה לאף אחד.

שינוי ראושן של הקוד יקרב אותנו לכתיבה נכונה גם בלי לפתור את הבעיה:

def reduce_polymer(polymer: str) -> str:
    i = 0
    while i < len(polymer) - 1:
        if has_reduction(polymer, i):
            polymer = remove_reduction(polymer, i)
            i -= 1
            continue
        i += 1
    return polymer


def has_reduction(polymer: str, i: int):
    if not 0 <= i < len(polymer) - 1:
        raise ValueError(f"Index should be >= 0, got {i}")
    unit1 = polymer[i]
    unit2 = polymer[i + 1]
    return abs(ord(unit1) - ord(unit2)) == 32


def remove_reduction(polymer: str, i: int):
    if not 0 <= i < len(polymer) - 1:
        raise ValueError(f"Index should be >= 0, got {i}")
    return polymer[0:i] + polymer[i+2:]

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

והלקח הכללי יותר מהסיפור? בעבודה עם פונקציות שילוב של Type Hints עם בדיקת קלט בכניסה לפונקציה יכול לעזור לנו למצוא בעיות בקוד הרבה יותר מהר.