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

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

טיפים לכתיבת קוד נקי בשפה דינמית

14/05/2023

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

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

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

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

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

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

user = User.find_by(email: 'ynon@tocode.co.il')
language = Language.find_by(name: 'English')
blog_post = BlogPost.find_by('published_at > ?', 1.days.ago)

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

  1. השתמשו בקונבנציות שמות - כשיש טיפוסים מסוימים שחוזרים על עצמם, משתלם להשתמש בקונבנציית שמות כדי לרמוז לקורא למה אנחנו מצפים. לדוגמה בקוד JavaScript הרבה מתכנתים ישתמשו בתחילית $ כדי לרמוז שמשתנה מסוים צריך לקבל DOM Element. אם זו קונבנציה גם בפרויקט שלכם ואתם רואים פונקציה כזו:
function applyTheme($el) { ... }

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

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

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

חדש באתר: מדריך sed

13/05/2023

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

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

כל ההקדמה הזאת באה כדי לספר שני דברים - הראשון, שלא מזמן פנתה אליי תלמידה בבקשה ללמוד יותר על פקודת sed, ובזכותה העליתי עכשיו מדריך וידאו חדש של כחצי שעה על הפקודה. המדריך כולל גם הסבר תיאורטי על sed וגם אינספור דוגמאות לדברים שעבורם אני משתמש ב sed בעבודה היום יומית. אתם יכולים למצוא את המדריך בחינם בקישור: https://www.tocode.co.il/boosters/12

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

שמות משתנים בעלי משמעות

12/05/2023

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

const days = 

קופיילוט משלים את השורה ל:

const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

ההתחלה הזו:

const baseUrl = 

הופכת ל:

const baseUrl = import.meta.env.BASE_URL;

וההתחלה הזו:

const ticTacToeGameBoard = 

קיבלה את ההשלמה ל:

const ticTacToeGameBoard = [
  ['', '', ''],
  ['', '', ''],
  ['', '', ''],
]

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

function SignupForm() {

קיבלה השלמה ל:

function SignupForm() {
  return (
    <form>
      <label htmlFor="email">Email</label>
      <input type="email" id="email" />
      <label htmlFor="password">Password</label>
      <input type="password" id="password" />
      <button type="submit">Sign up</button>
    </form>
  )
}

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

היום למדתי: את המתג `-l` של gcc צריך לכתוב בסוף

11/05/2023

אתם מכירים את המתג -l של gcc, כי בעזרתו בקומפילציה אנחנו מוסיפים ספריה חיצונית. לדוגמה בשביל לקמפל תוכנית עם libmath משתמשים ב:

gcc -l m

אז היום גיליתי שהצלחתי להעביר חיים שלמים בלי לשים לב שיש חשיבות למקום של -l בתוך שורת הפקודה של הקומפילציה, או מתוך התיעוד:

It makes a difference where in the command you write this option; the linker searches and processes libraries and object files in the order they are specified. Thus, foo.o -lz bar.o searches library z after file foo.o but before bar.o. If bar.o refers to functions in z, those functions may not be loaded.

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

#include <stdio.h>
#include <curl/curl.h>
int main(void)
{
  CURL *curl;
  CURLcode res;
  curl = curl_easy_init();
  if(curl) {
    curl_easy_setopt(curl, CURLOPT_URL, "http://example.com/");
    /* Perform the request, res will get the return code */
    res = curl_easy_perform(curl);
    /* Check for errors */
    if(res != CURLE_OK)
      fprintf(stderr, "curl_easy_perform() failed: %s\n",
              curl_easy_strerror(res));
    /* always cleanup */
    curl_easy_cleanup(curl);
  }
  return 0;
}

ושם הקובץ getter.c, אז ניסיון לקמפל את הקובץ עם:

$ gcc -l curl getter.c

מחזיר:

/usr/bin/ld: /tmp/ccToWOcP.o: in function `main':
getter.c:(.text+0x84): undefined reference to `curl_easy_init'
/usr/bin/ld: getter.c:(.text+0xb0): undefined reference to `curl_easy_setopt'
/usr/bin/ld: getter.c:(.text+0xb8): undefined reference to `curl_easy_perform'
/usr/bin/ld: getter.c:(.text+0xdc): undefined reference to `curl_easy_strerror'
/usr/bin/ld: getter.c:(.text+0xf8): undefined reference to `curl_easy_cleanup'
collect2: error: ld returned 1 exit status

והדרך הנכונה לקמפל את הקובץ היא:

$ gcc getter.c -l curl

שילוב asyncio עם Threads בפייתון

10/05/2023

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

נדמיין שיש לנו קוד שמשתמש ב AWS כדי לתרגם מילים:

import boto3

client = boto3.client('translate')

def spanish_to_english(text):
    response = client.translate_text(
        Text=text,
        SourceLanguageCode='es',
        TargetLanguageCode='en',
    )

    return response["TranslatedText"]

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

def blocking_io():
    print(f"start blocking_io at {time.strftime('%X')}")
    # Note that time.sleep() can be replaced with any blocking
    # IO-bound operation, such as file operations.
    time.sleep(1)
    print(f"blocking_io complete at {time.strftime('%X')}")

async def main():
    print(f"started main at {time.strftime('%X')}")

    await asyncio.gather(
        asyncio.to_thread(blocking_io),
        asyncio.sleep(1))

    print(f"finished main at {time.strftime('%X')}")


asyncio.run(main())

קריאה רגילה ל blocking_io היתה חוסמת את ה Event Loop וגורמת להמתנה כוללת של שתי שניות. בזכות השימוש ב to_thread, הפונקציה נקראת בתהליכון נפרד וה sleep מתבצע במקביל ל asyncio.sleep, כך שסך הכל התוכנית ממתינה שניה אחת לשתי הפעולות.

הפונקציה נכנסה לשפה בפייתון 3.9 ואתם יכולים למצוא עליה מידע נוסף יחד עם כל הפרטים הקטנים בדף התיעוד: https://docs.python.org/3/library/asyncio-task.html#running-in-threads

היום למדתי: שיפור ביצועים עם Metadata ב Clojure

09/05/2023

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

בקריאה רגילה של קוד אנחנו לא נראה מתי הקומפיילר משתמש ב Reflection ומתי לא, אבל אפשר לבקש ממנו להדפיס אזהרות על שימוש כזה בעזרת המשתנה *warn-on-reflection*. בפרויקט לניגן נגדיר אותו עם:

:global-vars {*warn-on-reflection* true}

ובתוך ה repl אפשר להפעיל פשוט:

(set! *warn-on-reflection* true)

כדי להדליק את האזהרות.

עכשיו עם המשתנה דלוק אני יכול לראות שבקוד הבא קלוז'ר מבין בקלות מה הקלאס של str:

=> (set! *warn-on-reflection* true)
=> (let [s "hello there"] (.charAt s 3))
\l

אבל בקוד הזה אין לו מושג ולכן הוא צריך להשתמש ב Reflection כדי לזהות בזמן ריצה את הקלאס:

=> (set! *warn-on-reflection* true)
=> (let [s (apply str "hello")] (.charAt s 3)
Reflection warning, NO_SOURCE_PATH:1:30 - call to method charAt can't be resolved (target class is unknown).
\l

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

אנחנו מדביקים Metadata לערך עם המקרו המיוחד ^, ומושכים את ה metadata עם פונקציית meta. הקוד הבא מגדיר את המשתנה x להחזיק רשימה עם המספרים 1,2,3 ומשייך אליו את ה Metadata שנקרא :yay:

user=> (def x ^:yay [1 2 3])
#'user/x
user=> x
[1 2 3]
user=> (meta x)
{:yay true}

נחזור לבעיית ה Reflection - אם נשייך שם של קלאס בתור Metadata הקומפיילר ישתמש בו במקום לחפש דינמית את שם הקלאס. וזה נראה ככה:

user=> (set! *warn-on-reflection* true)
user=> (let [s ^String (apply str "hello")] (.charAt s 3))
\l

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

user=> (let [s ^Integer (apply str "hello")] (.charAt s 3))
Reflection warning, NO_SOURCE_PATH:1:39 - call to method charAt on java.lang.Integer can't be resolved (no such method).
\l

יש מחיר להבנה

09/05/2023

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

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

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

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

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

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

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

הפחד להתאמץ מדי

08/05/2023

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

  1. הנה תרגיל קשה אבל אני יודע בגדול איך פותרים אותו,

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

  3. בגלל שאני יודע מה הפיתרון, לפתור את האתגר לא ייתן לי יותר מדי,

  4. חבל להשקיע מאמץ סתם, עדיף לעשות דברים יותר מועילים.

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

  1. הנה תרגיל קשה אבל אני יודע בגדול איך פותרים אותו,

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

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

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

תיקון שימוש מסורבל בפונקציה sorted בפייתון

07/05/2023

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

import wikipedia

def k(i):
    """ returns the number of words the value i has in wikipedia """
    return len(wikipedia.summary(i))


words = ["wikipedia", "Python (programming language)", "Ruby (programming language)", "Rust (programming language)"]

print([
    w[0] for w in
    sorted([(w, k(w)) for w in words], key=lambda i: i[1], reverse=True)])

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

המשך קריאה

שבע שנים ועדיין לא יודע גיט

06/05/2023

משתמש רדיט כתב השבוע-

Hello everyone,

I've been using Git for the last 7 years, but I still don't fully understand how it works. I'm hoping someone can help me clarify my understanding of how cloning, feature branches, and merging work in Git.

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

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

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

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