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

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

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

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

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

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

וכך בתור תלמידים למדנו שאם אפשר ללמוד 10 שעות ולקבל 100 אז אין טעם ללמוד 20 שעות (כי ממילא לא תקבל יותר מ 100).

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

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

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

  2. ככל שתבני יותר משחקים, כך יהיה לך קל יותר לבנות את המשחק הבא.

  3. ככל שתעבירי יותר הרצאות כך תגיעי יותר מוכנה להרצאה הבאה.

לא בשביל הציון. לא בשביל המשכורת. לא בשביל הקידום. פשוט כי אפשר.

מתכנתי JavaScript רבים כבר יודעים לעבוד עם Destructuring כדי לשבור מערך למספר משתנים, לדוגמה הקוד הבא שומר ב x את הערך 10 וב y את הערך 20:

const [x, y] = [10, 20];

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

const [value, setValue] = useState("");

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

function Input(props) {
  const [value, setValue] = props.model;
  return (
    <input
      type="text"
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

function App() {
    const model = useState("");
    const [value, setValue] = model;

    return (
        <div>
            <p>Hello {value}</p>
            <Input model={model} />
        </div>
    );
}

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

const model = [value, setValue] = useState("");

והתשובה מורכבת.

הקוד יעבוד (לפחות בחלק מסביבות ההרצה) אבל לא יעשה את מה שרציתם שהוא יעשה. בכתיב כזה המילה const משפיעה רק על המשתנה הראשון model, והמשתנים value ו setValue יהיו משתנים גלובאליים. לכן הקוד יעבוד בסביבות שמאפשרות הגדרת משתנים גלובאליים בלי var, const או let. בכל מקרה אנחנו יודעים שמשתנים גלובאליים ב JavaScript רק מביאים צרות ולכן גם אם הכתיב עובד לא מומלץ להשתמש בו.

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

export default function App() {
  const model = useState(""),
    [value, setValue] = model;

  return (
    <div className="App">
      <p>Hello {value}</p>
      <Input model={model} />
    </div>
  );
}

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

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

אם לקחתם את הקוד אתמול ואולי ניסיתם להדפיס חלק מהמילים יכולתם לגלות שהמילים house ו houses נספרות כשתי מילים שונות, וכך גם go, goes ו going. בנוסף אולי שמתם לב ששמות כמו George, Bill ו Jane נספרו בתור מילים.

בואו נתקן את שתי הבעיות בעזרת ספריית nltk.

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

  1. היכולת לבצע Stemming למילה, כלומר לקחת את המילים house ו houses, להבין שזו אותה מילה ולהחזיר את הצורה המקורית של המילה - house.

  2. מאגר השמות הפרטיים שמגיע בתוך הספריה שיאפשר לנו לזהות האם מילה מסוימת היא שם פרטי.

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

pip install nltk

ובתוך התוכנית נשתמש בה עם ה import-ים הבאים:

from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
import nltk.corpus

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

names = nltk.corpus.names

all_names = set(names.words('male.txt') + names.words('female.txt'))

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

import nltk
nltk.download('names')

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

# import these modules
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize

ps = PorterStemmer()

# choose some words to be stemmed
words = ["program", "programs", "programer", "programing", "programers"]

for w in words:
    print(w, " : ", ps.stem(w))

התוצאה של כל ההדפסות היא המילה program.

התוכנית שלי אחרי עדכון נראית כך:

import requests, textract, re, os.path
from functools import reduce
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
import nltk.corpus

names = nltk.corpus.names

all_names = set(names.words('male.txt') + names.words('female.txt'))

index_url = 'https://meyda.education.gov.il/bagmgr/Ajax.ashx?search=1&sheelon=&miktzoa=16&safa=1&pagesize=10&page=1'

r = requests.get(index_url)
urls = [q["question"] for q in r.json()]
total_words = {}
ps = PorterStemmer()

s = requests.Session()
for url in urls:
    m = re.search(r'([^/]+\.pdf)', url)
    if m is None: continue

    filename = m[0]
    if not os.path.isfile(filename):
        r = s.get(url)

        with open(filename, 'wb') as f:
            f.write(r.content)

    text = textract.process(filename).decode('utf8')
    words = [
            ps.stem(w.lower()) for w in re.findall(r'\b[a-zA-Z_]+\b', text)
            if w not in all_names
            ]
    print(f"Test {filename} has {len(words)} different words")
    total_words[filename] = set(words)

all_words = reduce(lambda acc, val: acc | val, total_words.values())
print(f"All tests had a total of {len(all_words)} different words")

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

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

תוצאות? מיד אחרי הסקריפט שמחשב את הנתונים...

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

הדרך הכי טובה לאבד מוטיבציה היא להסתכל על התוצאות: האם הצלחתי לפתור את התרגיל? האם התקבלתי לעבודה? כמה אנשים עשו לי "לייק" בסטאק אוברפלו או האם ענו לי על ה Pull Request ששלחתי?

וזה גרוע גם אם התוצאה חיובית וגם אם שלילית.

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

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

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

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

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

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

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

בעקבות פירסום שראיתי לקורס Qt התחלתי לחשוב - החודשיים שהשקעתי באמת היו אפקטיביים, אבל האם היה אפשר לקצר אותם? האם קורס מרוכז היה עוזר לי להגיע לאותה רמה בהשקעה של 5 ימי עבודה בלבד?

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

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

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

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

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

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

לא נעים להגיד אבל יש בטכנולוגיה פיצ'רים שאנשים רוצים ללמוד עליהם ואחרים שלא ממש. אף אחד לא נכנס לקורס ריאקט ומבקש ללמוד איך עובד Higher Order Component או לקורס JavaScript ומבקש ללמוד איך לממש לבד Class System עם פרוטוטייפים.

מצד שני כשאנשים נכנסים לקורס על בדיקות הם קודם כל רוצים לשמוע איך להשתמש ב Mock Objects, ב Spies ובכל היכולות הכי מתקדמות של ספריית הבדיקות. בקורס ריאקט אנשים ישאלו אותי לעתים קרובות איך עובדים עם Concurrent Mode וכמובן על useContext ו useMemo.

ויש סיבה למשחק הזה.

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

האמת הפוכה לגמרי: כמעט כל קוד בדיקה שראיתי שהשתמש ב Mock Objects כדי "לדרוס" חלקים במערכת היה אפשר לשכתב בקלות לגירסה יותר אמינה בלי אותם Mock Objects. כמעט אף אחד לא צריך את PureComponent או useMemo כי ריאקט עובד טוב מספיק בלעדיו.

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

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

זה נראה לך בסדר?

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

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

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

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

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

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

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

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

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