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

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

קרוב לקצה

14/04/2019

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

  1. שאנחנו ננקה שאילתות SQL בשורה שמעבירה את השאילתה לדרייבר (באמצעות Bind Variables).

  2. שאנחנו ננקה מידע שנכתב ל HTML בקוד ה View, ממש לפני שהוא נשלח ללקוח (זאת הסיבה שריאקט מנקה את הקלט ואתם צריכים להתאמץ לעקוף את זה עם dangerouslySetInnerHTML).

  3. שאנחנו ננקה מידע שהולך להישלח ל system ממש לפני הקריאה ל system כדי למנוע Shell Injections.

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

העברת נתיבים מ Rails ל JavaScript

13/04/2019

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

אתגר יותר מעניין בשילוב הזה בין Rails לאפליקציות צד-לקוח הוא העברת הנתיבים. בעבודה עם ריילס אנחנו רגילים להשתמש ב Route Helpers כדי ליצור באופן אוטומטי נתיבים בתוך קבצי ה .html.erb של היישום. כך לדוגמא התג הבא מייצר את הקישור למסך הלוגין:

<%= link_to 'Login', new_user_session_path %>

וזה מייצר את הקישור למסך צפיה בהודעה לפי ID שלה:

<%= link_to 'Post #4', post_path(id: 4) %>

במעבר לאפליקציות צד-לקוח יש לנו בעיה. אנחנו לא רוצים לכתוב Hard Coded את הנתיבים עצמם. הם כתובים ב config/routes.rb פעם אחת וזה ממש נוח שאפשר לשנות אותם וכל האפליקציה מתעדכנת בצורה אוטומטית. הבעיה שה Route Helpers כגון post_path הם פונקציות, ועוד פונקציות שמיוצרות אוטומטית על ידי ריילס. איך מעבירים את הפונקציות האלה ל JavaScript?

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

נקודת הכניסה תהיה פונקציה ב ApplicationController שתקרא לפני כל פעולה, נקרא לה init. היא תיצור מילון בו המפתח הוא שם של נתיב והערך הוא הנתיב בפורמט של מחרוזת, כאשר הפרמטרים מיוצגים על ידי נקודותיים (כמו שהיינו כותבים אותם בקובץ config/routes.rb). דוגמא למבנה נתונים כזה:

{
    'new_user_session_path': '/users/sign_in',
    'post_path': '/posts/:id',
}

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

class ApplicationController < ActionController::Base
  before_action :init

  def init
    @state = {}
    @routes_for_js = Rails.application.routes.named_routes.map do |route_name|
      route = Rails.application.routes.named_routes[route_name]
      route_params = route.parts.reject {|part| part == :format }
      route_params_hash = route_params.map {|x| [x, ":#{x}"] }.to_h
      route_method = route_name.to_s + '_path'
      [
          route_method,
          self.method(route_method).call(**route_params_hash),
      ]
    end.to_h.reject {|k, v| k.starts_with?('rails_')}
  end
end

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

const routes = JSON.parse(document.querySelector('#routes').dataset.routes);
const routeHelpers = {};

for (let [routeName, routeString] of Object.entries(routes)) {
  const routeNameJS = routeName.replace(/_([a-zA-Z]+)/g, word => (
    word[1].toUpperCase() + word.slice(2)
  ));

  routeHelpers[routeNameJS] = routeParams => (
    routeString.replace(/:(\w+)/g, x => routeParams[x.slice(1)])
  )
}

export default routeHelpers;

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

<a href={routeHelpers.newUserSessionPath()}>Login</a>
<a href={routeHelpers.postPath({ id: 4 })}>Watch Post #4</a>

הדבר הכי טוב שיכול לקרות לך בקורס

12/04/2019

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

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

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

יאללה תעמיסו

11/04/2019

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

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

@dispatch(tuple)
def go(point):
    x, y = point
    go(x, y)

@dispatch(int, int)
def go(x, y):
    print(f'x = {x}, y = {y}')

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

loc = (10, 10)
go(loc)

ונרצה שהשני ייקרא כשנפעיל את הפונקציה עם שני פרמטרים:

go(10, 10)

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

{
    go: {
        (typle): <function at 0x1088b01e0>
        (int, int): <function at 0x1089d1488>
    }
}

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

מימוש פשוט של המנגנון יכול להיראות כך:

import collections

md = collections.defaultdict(dict)

def dispatch(*types):
    def decorator(f):
        md[f.__name__][types] = f
        def wrapper(*args, **kwargs):
            res = md[f.__name__][tuple(type(a) for a in args)]
            res(*args, **kwargs)

        return wrapper
    return decorator

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

https://github.com/mrocklin/multipledispatch

בואו נדביק את מספר הקומיט האחרון לקובץ הפלט

10/04/2019

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

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

מתקינים את הספריה עם:

$ pip install dulwich --global-option="--pure"

ומתוך תוכנית פייתון נוכל עכשיו לכתוב:

from dulwich.repo import Repo
r = Repo('.')
last_commit_id = r.head().decode('ascii')

result_filename = f'result.{last_commit_id}.txt'

with open(result_filename, 'w') as f:
    f.write('Hello World\n')

כדי לקבל את קובץ התוצאות עם מזהה הקומיט האחרון.

זה מה שקורה כששוכחים להשתמש ב with

09/04/2019

פייתון הפתיע אותי היום עם הודעת השגיאה הבאה:

Traceback (most recent call last):
  File "...", line 31, in search
    if is_text_in_file(file, word):
  File "", line 10, in is_text_in_file
    for line in fileinput.input(file):
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/fileinput.py", line 93, in input
    raise RuntimeError("input() already active")
RuntimeError: input() already active

וזאת הפונקציה שגרמה לבעיה:

def is_text_in_file(file, text):
    for line in fileinput.input(file):
        if text in line:
            return True

    return False

הפונקציה עובדת לא רע בפעם הראשונה שקוראים לה, אבל בגלל ה return באמצע לולאת ה fileinput, ברגע שמחזירים True בפעם הראשונה אי אפשר יותר לקרוא לה. הפונקציה יוצאת לפני שסגרה את הקובץ.

כיוון אחד לתקן את זה הוא להוסיף קריאה יזומה ל fileinput.close לפני ה return. וזה עובד:

def is_text_in_file(file, text):
    for line in fileinput.input(file):
        if text in line:
            fileinput.close()
            return True

    return False

אבל בינינו למה להתאמץ כשאפשר לתת ל Python לעבוד בשבילנו? כיוון הרבה יותר טוב יהיה לעטוף את כל הבלוק שמשתמש ב fileinput בפקודת with:

def is_text_in_file(file, text):
    with fileinput.input(file) as f:
        for line in f:
            if text in line:
                return True

    return False

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

איזו שפה ללמוד?

08/04/2019

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

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

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

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

איך לשמור סיסמאות בבסיס הנתונים עם Argon2 ב Python

07/04/2019

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

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

לארגון2, כמו לאלגוריתמי Password Hashing מקבילים יש שתי מטרות:

  1. למנוע אפשרות מתוקפים להכין מראש "בסיס נתונים" של כל הסיסמאות האפשריות (נקרא חישוב מקדים), ובעצם תוקף יכול להתחיל לנסות לפרוץ סיסמאות רק אחרי שקיבל לידיו בסיס נתונים עם סיסמאות שמוצפנות ב Argon2.

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

נלך לראות איך להשתמש ב Argon2 כדי לשמור סיסמאות בבסיס הנתונים בשפת Python. דרך טובה לגשת ל Argon2 מתוך פייתון היא הספריה passlib. נתקין את הספריה:

pip install passlib argon2_cffi

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

נראה איך זה עובד בתוכנית קטנה:

from passlib.hash import argon2
h = argon2.hash("ninja")
print(h)

# prints: $argon2i$v=19$m=102400,t=2,p=8$SkkpRci5tzZGaG0thbB2bg$OzPDd59KfGJbfqg4SsJLYw

# prints True
print(argon2.verify('ninja', h))

# prints False
argon2.verify('demo', h)

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

>>> argon2.hash("ninja")
'$argon2i$v=19$m=102400,t=2,p=8$6F3rHeN8790b4/yfMyYEwA$4aFNx5iWvsAt0u8U2Mdm6g'
>>> argon2.hash("ninja")
'$argon2i$v=19$m=102400,t=2,p=8$nhMCQMiZ0/ofA2BM6Z1TKg$WincHSs3Afkgqp+qn0Emdw'
>>> argon2.hash("ninja")
'$argon2i$v=19$m=102400,t=2,p=8$8D6nVArh/B9DaI3x/r83Jg$jZSKc5iwdFAVF8RN8G8xtg'

כל הפעלה יצרה תוצאה קצת שונה. הסיבה היא מנגנון ה Salt שמוטמע ב Argon2. מנגנון זה הוא שמונע מתוקפים לנסות לייצר את כל ערכי ה Hash האפשריים ממילון או ב Brute Force מבעוד מועד. רק אחרי שתוקף הצליח לקחת את כל בסיס הנתונים שלכם הוא יודע מה ה Salt של כל סיסמא ויכול לנסות לפרוץ את הסיסמאות אחת אחרי השניה. בזכות המבנה של Argon2, ביצוע מתקפת Brute Force על הסיסמאות ייקח הרבה מאוד זמן.

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

גיט ריסט

06/04/2019

ראיתי השבוע חבר מקליד את הפקודה הבאה בגיט כדי לצאת ממצב מביך:

$ git reset --hard origin/master

בואו נראה מה היא עושה כדי להבין אם היא יכולה לעזור גם לכם.

מתחילים בתיעוד של גיט. יש לגיט ריסט שלושה מצבי פעולה:

       git reset [-q] [<tree-ish>] [--] <paths>...
       git reset (--patch | -p) [<tree-ish>] [--] [<paths>...]
       git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<comm
it>]

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

In the third form, set the current branch head (HEAD) to
<commit>, optionally modifying index and working tree to match.

לכן הדבר הראשון שריסט יעשה הוא לשנות את HEAD כך שיצביע על הקומיט שעובר בפרמטר האחרון. הקומיט הזה במקרה שלנו הוא origin/master שזה אומר הקומיט שאנחנו חושבים שהשרת קורא לו master. זה אומר שאם עשינו כבר קומיטים אצלנו ב master אנחנו הולכים לחזור לנקודה לפני הקומיטים שלנו. מצד שני פקודת reset לא פונה לשרת לכן אם מישהו אחר עשה push לשרת אנחנו לא נדע מזה. אנחנו נלך למקום ש origin/master היה בו בפעם האחרונה שפנינו לשרת.

אפשר לראות את הנקודה הזאת עם:

$ git log -1 origin/master

או אם מה שמעניין אתכם זה רק ה Commit Hash תוכלו להפעיל:

$ git rev-parse origin/master
a08935913801409ef7437ba9038d4a0580f1f6c5

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

$ git rev-parse master
a08935913801409ef7437ba9038d4a0580f1f6c5

דרך אחרת היא פשוט להפעיל status. אם ה master שלכם לא תואם ל origin/master תקבלו על זה הודעה בשורה השניה של הפלט. במקרה שלי הם תואמים אז יש לנו:

$ git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

בחזרה ל reset - אז הדבר הראשון שיקרה זה ש HEAD יעבור להצביע על הקומיט שנקרא origin/master. הדבר השני ש reset עושה זה לעדכן את הקבצים בתיקיית העבודה. המתג --hard אומר:

Resets the index and working tree. Any changes to tracked files in the working tree
since <commit> are discarded.

לכן הפקודה תנקה את כל ה Staging Area ותעתיק את הקבצים מהקומיט אליו אנחנו חוזרים ל Staging Area. אחרי זה היא תעשה את אותו דבר עם הקבצים בתיקיית העבודה שלנו. נשים לב שמדובר רק על שינויים בקבצים במעקב בתיקיית העבודה, לכן קבצים שיש לכם בתיקיית העבודה שלא נמצאים בריפוזיטורי לא יושפעו מהפקודה (בשביל למחוק אותם תרצו לקרוא על git clean, אבל זה סיפור לפוסט אחר).

לסיכום הפקודה git reset --hard origin/master עושה את הדברים הבאים:

  1. היא בודקת מה מזהה הקומיט של הענף origin/master.

  2. היא מזיזה את הענף הנוכחי כך שיצביע גם הוא לאותו קומיט. יחד איתו יזוז גם ה HEAD.

  3. היא מאפסת את כל הקבצים ב Staging Area ולוקחת את הגירסא מהריפוזיטורי.

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

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

וקטנה לסיום- אם הפעלתם את git reset --hard origin/master בטעות ופתאום נעלמו לכם הקומיטים שעשיתם, אתם תמיד יכולים לגשת ל git reflog כדי למצוא את הקומיטים האבודים. אחרי שמצאתם את הקומיט ש master הצביע עליו קודם, תוכלו להשתמש ב git reset נוסף כדי לחזור אליו.

יסודות

05/04/2019

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

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

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

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