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

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

היום למדתי: נקודה בפוליגון ופיתרון AoC 2023 יום 10 חלק 2 בסקאלה

04/02/2024

לפני שבוע פרסמתי כאן את הפיתרון של החלק הראשון של יום 10 מ Advent Of Code האחרון בסקאלה, בו ראינו איך למצוא מעגל בגרף באמצעות DFS. החלק השני של התרגיל הציג בעיה מעניינת שנקראת Point In Polygon. בואו נראה איך זה עובד ואיך לפתור אותה עם אלגוריתם Ray Casting.

המשך קריאה

שלושה סיפורים קריטיים לקריירה שלנו

03/02/2024

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

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

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

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

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

02/02/2024

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

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

  1. הקוד לא רץ. גם לא מתקמפל.

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

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

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

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

מה כן עובד לי-

  1. יצירת מנגנון הרצה סטנדרטי, גם לפרויקטים ישנים - כלומר צריך לוודא ש npm start או docker compose up עובדים ומעלים את הסביבה. זה תמיד שווה את ההשקעה.

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

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

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

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

בסקאלה זה לא היה קורה (או: מה חדש בטייפסקריפט 5.4)

01/02/2024

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

function getUrls(url: string | URL, names: string[]) {
    if (typeof url === "string") {
        url = new URL(url);
    }

    return names.map(name => {
        url.searchParams.set("name", name)
        //  ~~~~~~~~~~~~
        // error!
        // Property 'searchParams' does not exist on type 'string | URL'.

        return url.toString();
    });
}

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

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

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

  implicit def stringToURL(url: String): URL =
    new URI(url).toURL

ואם אותה פונקציה נמצאת ב Scope אז אני יכול לכתוב פונקציה שמצפה לקבל URL ולהפעיל אותה עם String והכל פשוט יעבוד:

  def printHost(url: URL): Unit =
    println(url.getHost)

  @main
  def demo(): Unit =
    val url: String = "https://www.tocode.co.il"
    printHost(url)

הקומפיילר מפעיל באופן אוטומטי את פונקציית ההמרה ובעצם הפונקציה נקראת עם אוביקט מסוג URL.

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

היום למדתי: בדיקת מאפיין ב Beautiful Soup בפייתון

31/01/2024

הקוד הבא נכשל כי ללינק השני אין מאפיין href:

from bs4 import BeautifulSoup
text = """<div>
    <a href="#a">a</a>
    <a>not a link</a>
</div>"""

if __name__ == "__main__":
    soup = BeautifulSoup(text, features="html.parser")
    for link in soup.find_all("a"):
        print(link["href"])

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

for link in soup.find_all("a"):
    if "href" in link:
        print(link["href"])

מה עושים? דרך אחת היא להסתכל על מאפיין attrs של התג, שהוא כן מילון:

for link in soup.find_all("a"):
    if "href" in link.attrs:
        print(link["href"])

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

for link in soup.find_all("a"):
    if link.get("href") is not None:
        print(link["href"])

שתי דוגמאות שלא אהבתי של Structural Pattern Matching בפייתון

30/01/2024

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

בדוגמה הראשונה מתוך התיעוד יש לנו קטע קוד שמועתק מדג'נגו:

if (
    isinstance(value, (list, tuple)) and
    len(value) > 1 and
    isinstance(value[-1], (Promise, str))
):
    *value, label = value
    value = tuple(value)
else:
    label = key.replace('_', ' ').title()

ואחרי המעבר ל Structural Pattern Matching הוא יראה כך:

match value:
    case [*v, label := (Promise() | str())] if v:
        value = tuple(v)
    case _:
        label = key.replace('_', ' ').title()

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

[*v, _] = value

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

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

פעולת match/case הופכת אפילו יותר מסובכת כשמדובר על התאמה למחלקות שלנו. ניקח את הספריה regex_spm בתור דוגמה ואת הקוד הבא מתוך התיעוד שלה:

match regex_spm.fullmatch_in("123,45"):
  case r"(\d+),(?P<second>\d+)" as m:
    print("Notice the `as m` at the end of the line above")
    print(f"The first group is {m[1]}")
    print(f"The second group is {m['second']}")
    print(f"The full `re.Match` object is available as {m.match}")

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

@dataclass
class RegexSpmMatch:
  string: str
  _match_func: Callable[[re.Pattern, str], re.Match]
  match: re.Match | None = None

  def __eq__(self, pattern: str | re.Pattern | tuple[str, int | re.RegexFlag]):
    if isinstance(pattern, str):
      pattern = re.compile(pattern)
    elif isinstance(pattern, tuple):
      pattern = re.compile(*pattern)
    self.match = self._match_func(pattern, self.string)
    return self.match is not None

  def __getitem__(
      self,
      group: int | str | tuple[int, ...] | tuple[str, ...]
  ) -> str | tuple[str, ...] | None:
    return self.match[group]

בעצם יש לנו dataclass עם מימוש מקוסטם לפונקציה __eq__ מה שגורם להתאמה מול ה case. ה as המועתק למשתנה m הוא לא Match Object אלא אותו dataclass מהספריה, שכולל מימוש ל __getitem__ כדי לאפשר את כתיב הסוגריים המרובעים.

נשווה את זה לגירסת ה if/else:

if m := re.search("(\d+),(?P<second>\d+)", text):
    ...
elif m := re.search("(\d)-(\d)", text):
    ...
else:
    print("no match")

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

pattern1 = r"my-first-pattern"
pattern2 = re.compile(r"my-second-pattern")
match regex_spm.search_in(my_string):
  case pattern1: print("This does not work, it matches any string. Python interprets `pattern1` "
                       "as simply a new capture variable name, hiding its previous value.")
  case pattern2: print("This does not work either")

סך הכל Structural Pattern Matching זה כן מנגנון ממש אחלה. תשתמשו בו איפה שהגיוני אבל שימו לב להיזהר משימוש יתר.

שגיאות בתור ערכים בפייתון?

29/01/2024

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

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

# Define a function that returns a union of a User and an error
def get_user(user_id: str) -> User | Exception:
    rows = users.find(user_id=user_id)
    if len(rows) == 0:
        return Exception("user not found")
    return rows[0]

def rename_user(user_id: str, name: str) -> User | Exception:
    # Consume the function
    user = get_user(user_id)
    if isinstance(user, Exception):
        return user
    user.name = name
    return user

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

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

נשווה את זה לסקאלה (או שפות אחרות שמאפשרות שגיאה-בתור-ערך):

case class User(id: Long, name: String)

val users: List[User] = List(
  User(1, "glassgrieving"),
  User(2, "wrensponge"),
  User(3, "horizonhow"))

def getUserIndex(userId: Long): Option[Int] =
  users.indexWhere(p => p.id == userId) match
    case i if i >= 0 => Some(i)
    case _ => None

def renameUser(userId: Long, newName: String): Option[List[User]] =
  for {
    index <- getUserIndex(userId)
  } yield users.updated(index, User(userId, newName))

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

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

a?.b?.c?.d

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

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

פיתרון Advent Of Code 2023 יום 10 חלק ראשון בסקאלה

28/01/2024

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

המשך קריאה

מה זה בעצם Premature Optimization?

27/01/2024

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

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

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

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

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

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

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

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

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

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

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

ללמוד מהסיבות הלא נכונות?

26/01/2024

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

ברוב המקרים זה לא נגמר טוב.

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

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

היום אני חושב שיש דרך טובה יותר.

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

  1. ללמוד תכנות לבד מהאינטרנט פעם בשבוע, בקטנה.

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

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

  4. לקחת את הלימוד בפרופורציה, לפחות עד שנהיה בטוחים.

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