ar
Feedback
ToCode

ToCode

الذهاب إلى القناة على Telegram

טיפים קצרים למתכנתים מאת ינון פרק

إظهار المزيد
1 420
المشتركون
+124 ساعات
+17 أيام
-430 أيام
أرشيف المشاركات
ToCode
1 420
בדיקות בתור First Class Citizen הקושי הכי גדול בכתיבת בדיקות לתוכנית קיימת היא רשימת התלויות של תוכניות קיימות. באופן טיפוסי כשאנחנו מגיעים לקודבייס נטול בדיקות הקוד משתמש ברכיבים חיצוניים שיהיה קשה להחליף, ויש תלות משמעותית בין אזורים שונים בקוד שלא מאפשרת "לנתק" מנגנונים מסוימים כדי לבדוק אותם בנפרד. התוצאה של כתיבת בדיקות למערכת כזאת היא או בדיקות אינטגרציה ענקיות שבודקות "מה המשתמש היה עושה", וכשהן נכשלות נותנות מעט מאוד אינפורמציה לגבי איפה הבעיה, או בדיקות יחידה קטנות מדי, שלא בודקות שום דבר מעניין. על פניו TDD היה אמור לפתור את הבעיה הזאת, אבל ב TDD הדגש הוא על כתיבת קוד הבדיקות ולא על התשתית או קוד המערכת. במילים אחרות אנשי TDD ישמחו לכתוב Mock Objects כדי שיוכלו לכתוב בדיקה לרכיב מסוים לפני שכותבים את קוד הרכיב. שלב הריפקטורינג ב TDD מגיע בסוף ומתמקד בריפקטורינג של הקוד אחרי שהבדיקה כתובה. בראיה של בדיקות בתור First Class Citizen אנחנו מסתכלים על כלל הקוד ואוסף התלויות שלו בתור משהו שצריך להיבדק בצורה יעילה. זה לא משנה אם הבדיקה נכתבת לפני או אחרי הקוד, הדבר החשוב הוא מה היא בודקת, וכמה קל להריץ אותה. הדבר משפיע על כל שלבי הפיתוח ובחירת הכלים ולפעמים מחייב החלטות קשות, לדוגמה: 1. נעדיף להשתמש בבסיס נתונים שיכול לרוץ מקומית מתוך קוד הבדיקה, במקום בבסיס נתונים שמחייב חיבור לרשת. 2. נארגן את הקוד כדי שחלק כמה שיותר גדול ממנו יוכל לתפקד לפני שמגיעים לממשקים החיצוניים, וכך אפשר יהיה לבדוק הרבה מהקוד גם בלי מוקים. 3. נעטוף קוד שמגיע מספריות חיצוניות כדי שיהיה יותר קל להחליף אותו בקוד בדיקה צפוי. אנחנו רוצים להגיע למצב שתוך פחות מדקה אנחנו יכולים לקבל הבנה לגבי רמת הבשלות של פיצ'ר חדש שכתבנו, וכמה הקוד החדש שובר מנגנונים קיימים. התיחסות לבדיקות בתור חלק בלתי נפרד מהתהליך והימנעות מתבניות ורכיבים שאי אפשר לבדוק אותם (כי אחרי זה כבר נכתוב לזה mock או קודם נבדוק שזה עובד) הם המפתח לפיתוח מוצרים שמחזיקים מעמד לאורך זמן.

ToCode
1 420
קוד אידמפוטנטי רדיטור אקראי שואל - אני מנסה להשתמש בריאקט כדי לאמת כתובת אימייל. גולש נכנס לכתובת שכוללת קוד אימות בתור Query Parameter, ואז קומפוננטת ריאקט מושכת את הפרמטר ופונה לשרת כדי לדווח על כניסה. כל זה קורה ב useEffect של הקומפוננטה ולכן נקרא פעמיים במצב פיתוח. מה עושים? אם נשים בצד את האתגר הריאקטי, מבחינת מערכת הפיתרון הכי קל כבר כתוב בשאלה והוא דווקא מחוץ לסקופ של ריאקט - אולי אפשר להפוך את ה API לאידמפוטונטי. קוד אידמפוטנטי זה קוד שאפשר להריץ אותו כמה פעמים בלי לשבור שום דבר, והמוסכמה באינטרנט היא שכך יתנהגו בקשות HTTP Get. במקרה של אימות כתובת מייל קל לדמיין שרת שבתגובה לבקשה לאמת כתובת אם הכתובת כבר מאומתת בבסיס הנתונים פשוט יחזיר HTTP 200. בנוסף בהרבה מצבים אפשר להוסיף מזהה למשאב וכך לקבל קוד אידמפוטנטי גם בפעולות "חד פעמיות", לדוגמה אם מישהו רוצה להוסיף קופון לעגלת קניות, אפשר לבקש שישלח גם את קוד הקופון וגם מזהה של עגלת הקניות, וכך אין בעיה של שליחה מרובה. לא תמיד נוכל לארגן את קוד המערכת בצורה אידמפוטנטית, אבל כשאפשר זה שווה את המאמץ.

ToCode
1 420
פיתרון פרויקט אוילר שאלה 12 בסקאלה פרויקט אוילר מציע אוסף אינסופי של תרגילים קטנים ואפשר להיעזר בהם כדי ללמוד שפות תכנות חדשות. מאחר ואני לומד סקאלה חשבתי לנסות לפתור איתה את בעיה 12, כדי ללמוד איך להשתמש ברשימות עצלות. התרגיל ניקח את סידרת הסכומים החלקיים של המספרים הטבעיים כלומר כל איבר הוא סכום כל המספרים הטבעיים עד אליו:
1, 3, 6, 10, 15, 21, 28
מצאו את המספר הראשון בסידרה זו שמתחלק ב 500 מספרים או יותר. פיתרון בסקאלה הדבר הראשון שרציתי לעשות כדי לגשת לזה בסקאלה היה לכתוב פונקציה שמחזירה כמה מחלקים יש למספר מסוים. הפיתרון הנאיבי נראה כך:
def divisorsCount(n: Int): Int =
  (1 to sqrt(n).toInt)
    .filter(n % _ == 0)
    .flatMap(i => List(i, n / i))
    .toSet[Int]
    .size
אחרי שזה מוכן אפשר להשתמש בו כדי לפתור את התרגיל:
def euler12(): Int =
  LazyList
    .from(0)
    .scanLeft(0)(_ + _)
    .dropWhile(divisorsCount(_) < 500)
    .head
מה למדתי על סקאלה השימוש ב LazyList היה מאוד נוח והזכיר את ה Stream של אליקסיר, ואפילו שם הפונקציה scan היה זהה. אני מתחיל גם להתרגל לשימוש בקו תחתי כדי להגדיר פונקציות אנונימיות, למרות שחייב להודות שפונקציית הסכום עדיין מבלבלת בעיניי (הקו התחתי מופיע שם פעמיים וכל פעם מייצג את הארגומנט הבא שעובר). הייתי גם שמח למצוא דרך לוותר על הקו התחתי בשורה:
.dropWhile(divisorsCount(_) < 500)
כי זה ברור שאני עובד על הפרמטר מהפקודה הקודמת. סך הכל הפיתרון בסקאלה היה מאוד אינטואיטיבי, אבל אולי זאת פשוט שאלה שקל לפתור אותה בגישה פונקציונאלית.

ToCode
1 420
זה הממשק, טמבל באחת השאלות הראשונות בפרויקט אוילר עלינו למצוא את המספר הפלינדרומי הגדול ביותר שהוא מכפלה של שני מספרים תלת ספרתיים. פיתרון שאני רואה הרבה מתלמידים שרק מתחילים לכתוב פייתון יהיה משהו כזה:
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))
הפעם לכל שורה יש תפקיד והקשר בין תפקיד כל שורה למשימה הגדולה ברור גם מהקוד וגם משמות המשתנים. בניית קוד כמו לגו, כמו בלוקים שאפשר לחבר ולנתק, היא המפתח לקוד קריא שיוכל לשרוד לאורך זמן.

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

ToCode
1 420
5 טיפים למבחני בית טובים יותר מבחני בית הם רע הכרחי בעולם שלנו. כן יש חלופות אבל גם הן לא תמיד אפשריות או טובות - לדוגמה שאלות תכנות על הלוח בזמן הראיון יכולות להלחיץ, ולא תמיד למועמד או למועמדת יש קוד ציבורי שיכולים לשתף. הבעיה עם מבחני בית היא שמועמדים לא יודעים מה אתם רוצים לראות, ויכול להיות שהם יכתבו קוד שנראה לכם "נוראי" אפילו שהם לגמרי מסוגלים לכתוב קוד "מושלם". מאחר והמטרה שלנו בגיוס היא לזהות מועמדים טובים, הנה כמה טיפים שיוכלו לעזור למועמדים שלכם ולכם לתקשר טוב יותר. פיצ'ר עדיף על פרויקט מלא זה שאתם נותנים למועמד שבוע לסיים מבחן בית לא אומר שיש לו שבוע לשבת על הקוד שלכם. יותר מזה, ברוב המוחלט של התפקידים אתם רוצים אנשים שיודעים להשתלב בפרויקט קיים, להוסיף אליו קוד בלי לשבור אותו ולהתאים את עצמם למנטליות של הפרויקט. אל תשלחו אותם לכתוב דברים שממילא הם לא יכתבו בעבודה אצלכם. הכינו מראש פרויקט עם רשימת פיצ'רים מעניינים וכל פעם שצריכים לתת למישהו מבחן בית תנו לו לממש את אחד הפיצ'רים מהרשימה. סטארטר עדיף על להתחיל מאפס ואם כבר מדברים על הפרויקט שהכנתם שימו לב לבנות אותו בהתאמה לקוד שהייתם רוצים לקבל חזרה. חשוב לכם מבנה קבצים ותיקיות מסוים? השתמשו במבנה זה בפרויקט. חשוב לכם שהקוד יכלול בדיקות? כתבו בדיקות לחלקים המוכנים בפרויקט. חשוב לכם שמועמד ישתמש ב TypeScript עם כללי בדיקה מסוימים (למשל בלי any)? שימו מראש את הקובץ tsconfig.json שלכם בפרויקט. סטארטר טוב מכיל בתוכו את התבניות שמועמד יכול לקחת מהן השראה כדי לבנות את הפיצ'ר הבא. מועמדים טובים יתאימו את הקוד שלהם למבנה של הפרויקט, וכך כולם מרוויחים. כמה שיותר קונטקסט יותר טוב הפרויקט צריך להציג רשימה של קטגוריות, ואתם יודעים שלא יהיו לעולם יותר מ-5 קטגוריות? ספרו את זה למועמד. או, משתמשים יכולים ליצור קטגוריות בלי הגבלה וצפויות מאות אלפי קטגוריות? גם זה חשוב לדעת. מה גודל המידע שאמור לעבור ברשת? מה צפויה להיות מהירות האינטרנט אצל המשתמשים שלכם? מי יוצר את התוכן וכמה תוכן יהיה? האם יש דרישות מסוימות שכדאי להיערך אליהן? ואיזה שינויים ממש לא הגיוני שיגיעו אי פעם? ככל שתספרו יותר על המערכת כך אוכל להתאים את הקוד לצרכים שלכם. בסוף יש לנו פה צורך לכתוב קוד מדויק בזמן קצר, והמטרה של שני הצדדים היא למקד את המאמצים במקומות הנכונים. התשתית עלינו עוד דבר שאנשים מסתבכים איתו בפרויקטים הוא ה Deployment, אז בואו נוריד גם את זה מהשולחן. שימו את הפרויקט בגיטהאב, הגדירו Actions שיעלו כל PR לשרת חדש ותנו למועמדים שלכם לשלוח PR ולראות את העבודה שלהם לייב ברשת עם בסיס נתונים אמיתי שהכנתם מראש. גם את כל קבצי העיצוב או כל דבר אחר שצריכים כדי להשלים את המשימה עדיף לחלק מראש. נסו את זה בבית והכי חשוב נסו לפתור את מבחן הבית בעצמכם לפני שאתם נותנים למועמדים להתמודד איתו. תראו שזה אפשרי, תראו כמה זמן זה לוקח, תראו לאיזה רמת פיתרון אתם מצליחים להגיע ועל איזה דברים טפשיים נתקעתם ששווה להכין אליהם מראש את המועמדים. המטרה שלנו במבחני בית היא לזהות מועמדים מתאימים. ככל שנלך יותר לקראתם יש יותר סיכוי לראות ולזהות את אותם מועמדים שמתאימים לנו.

ToCode
1 420
היום למדתי: שלוש נקודות ברובי אופרטור שלוש נקודות ברובי קיים מסתבר כבר די הרבה זמן (מגירסה 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 שנים.

ToCode
1 420
שתי גישות ל Code Reviews ובעלות על קוד דרך אחת להסתכל על Code Review היא בתור ה"לקוח" או ה"מנהל" שצריך לוודא את איכות העבודה ועמידה בסטנדרטים. בגישה זו המבקר הוא צוואר בקבוק, הוא חייב לוודא כל שורה בקוד כמו שאנחנו בודקים רכב יד שניה לפני קנייה. קבלת PR היא חותמת איכות, ורק קוד שעובר את תהליך הבקרה רשאי להיכנס למערכת. דרך שניה להסתכל על Code Reviews היא בתור הזדמנות לחזק את חברי הצוות. בגישה זו המבקר מקבל הצצה לתהליך המחשבה של המתכנתת שכתבה את הקוד ויכול לשאול - "למה התכוונת פה?", "מה אם הקובץ לא קיים?" או "שמתי לב לאופטימיזציה שאפשר להוסיף כאן". המבקר בגישה זו אינו צוואר בקבוק. אפשר לקבל את העצות, אפשר להשאיר אותן בצד, ואפשר לדחות אותן לגמרי. אנחנו הרבה פעמים חושבים שהגישה הראשונה היא טובה יותר למערכת, כיוון שהיא שומרת על רמה גבוהה של קוד. אבל לאורך זמן הגישה השניה מנצחת, כי היא שומרת על רמה גבוהה יותר של האנשים (גם אלה שכותבים קוד, וגם אלה שמבקרים אותו).

ToCode
1 420
price = 50,
      ISBN = "1680500546")
  }



@main
def main(): Unit = {
  val item = makeItem()
  val stringified = item.asJson.toString
  println(stringified)
  val restored = decode[Serializable](stringified)
  println(restored)
}
הפעם ה JSON שאנחנו מקבלים מקבל עוד רמה ונראה כך:
{
  "Item" : {
    "Game" : {
      "name" : "Super Mario",
      "price" : 20
    }
  }
}
אבל הניסיון לפענח אותו נכשל עם ההודעה:
Left(DecodingFailure at .Item: type Serializable has no class/object/case named 'Item'.)
ונכון זה לא סוף העולם, מי בכלל צריך להרחיב trait עם trait אחר ואפשר תמיד לכתוב לבד Decoder-ים ו Encoder-ים. אבל זה לא הסיפור פה. אולי צריך להגיד לכותבי ספריות - הקסמים שלכם הם אחלה כשהם עובדים עד הסוף. חצי קסם רק עושה לי יותר עבודה.

ToCode
1 420
משחקים עם JSON בסקאלה יש משהו חמוד כשספריית תוכנה מתאמצת כדי שדברים יעבדו בצורה אינטואיטיבית אבל יש בזה גם סכנה - והיא שהצעד האינטואיטיבי הבא כבר לא יעבוד. זה הסיפור עם ספריית circe של סקאלה, ספריה לעבודה עם JSON-ים. איך כותבים JSON בסקאלה עם circe נתחיל בתוכנית פשוטה שמגדירה case class עם שדה name ושדה price, וכותבת אותו למסך בתור JSON. עם circe הקוד נראה ככה:
import io.circe.*
import io.circe.generic.auto.*
import io.circe.parser.*
import io.circe.syntax.*

case class Game(name: String, price: Int)

def makeItem(): Game =
  Game(name = "Super Mario", price = 20)


@main
def main(): Unit = {
  val item = makeItem()
  println(item.asJson.toString)
}
איך קוראים JSON בסקאלה עם circe גם הקריאה מ JSON בחזרה ל case class עובדת בלי בעיה ובצורה מובנית בעזרת אותו קסם שעבד לנו בכתיבה. הקוד הזה ב main גם יקרא את ה json חזרה וידפיס את האוביקט שפוענח ממנו:
def main(): Unit = {
  val item = makeItem()
  val stringified = item.asJson.toString

  val restored = decode[Game](stringified)
  println(restored)
}
מה קורה כשצריך לכתוב משהו מטיפוס לא ידוע היכולת המלהיבה האחרונה של circe וזו שהייתי שמח לוותר עליה קשורה לעבודה עם case class-ים מסוגים שונים. בואו נרחיב את התוכנית כדי לתמוך בעוד כמה מוצרים:
trait Item {
  val name: String
  val price: Int
}
case class Game(name: String, price: Int) extends Item
case class Book(name: String, ISBN: String, price: Int) extends Item

def makeItem(): Item =
  if (Random.nextInt(10) >= 5) {
    Game(name = "Super Mario", price = 20)
  } else {
    Book(name = "Pragmatic Scala: Create Expressive, Concise, and Scalable Applications",
      price = 50,
      ISBN = "1680500546")
  }
ניסיון לקמפל את אותה תוכנית main ייכשל עם הודעת השגיאה:
No given instance of type io.circe.Encoder[Item] was found for parameter encoder of method asJson in class EncoderOps.
הסיפור הוא ש circe לא הצליח לייצר אוטומטית את ה Encoder בגלל ש Item הוא trait פתוח, ואולי אין לנו עדיין את הגישה לכל המחלקות שיורשות אותו. התיקון הקל הוא להפוך את Item ל sealed trait ואז התוכנית חוזרת להתקמפל.
sealed trait Item {
  val name: String
  val price: Int
}
בשביל שהקוד גם יעבוד עלינו לסדר עוד פרט קטן ב main - זיכרו שהפעלנו decode עם העברה מפורשת של שם המחלקה כדי לקרוא את ה JSON חזרה ל case class. בגלל שאנחנו עובדים עם sealed trait צריך לשנות את שורת הפיענוח וקוד ה main אחרי התיקון הוא:
@main
def main(): Unit = {
  val item = makeItem()
  val stringified = item.asJson.toString

  val restored = decode[Item](stringified)
  println(restored)
}
איפה זה נשבר התיעוד של circe עוצר פה כי באמת עבודת הזיהוי האוטומטי של שדות ב case class-ים היתה מרשימה. אבל בואו נעצור רגע כדי לשים לב מה בדיוק קרה כאן, איך ה decode הצליח לזהות לאיזה case class ה JSON צריך להפוך? לפי מה הוא יודע להחליט אם זה Game או Book ? התשובה מתחבאת במבנה ה JSON. אני מוסיף הודעת הדפסה ל stringified ומקבל שה JSON הוא:
{
  "Book" : {
    "name" : "Pragmatic Scala: Create Expressive, Concise, and Scalable Applications",
    "ISBN" : "1680500546",
    "price" : 50
  }
}
ובעצם במקום לכתוב רק את השדות circe הוסיף עוד רמה לאוביקט שהמפתח שלה הוא שם ה case class ממנו יצרנו את ה JSON. בצורה כזאת הוא יכול גם לקרוא את האוביקט ולדעת באיזה קלאס להשתמש. זה די מדליק עד שמנסים להוסיף עוד רמה בירושה. למשל מה אם כל Item בעצם מרחיב trait אחר? משהו כזה:
import io.circe.*
import io.circe.generic.auto.*
import io.circe.parser.*
import io.circe.syntax.*
import scala.util.Random

sealed trait Serializable

sealed trait Item extends Serializable {
  val name: String
  val price: Int
}

case class Game(name: String, price: Int) extends Item
case class Book(name: String, ISBN: String, price: Int) extends Item

def makeItem(): Serializable =
  if (Random.nextInt(10) >= 5) {
    Game(name = "Super Mario", price = 20)
  } else {
    Book(name = "Pragmatic Scala: Create Expressive, Concise, and Scalable Applications",

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