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

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

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

17/11/2020

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

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

המשך קריאה

למה לא מזמינים אותי לראיון עבודה

16/11/2020

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

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

המשך קריאה

היום למדתי: שינוי פרמטר נכנס ב JavaScript (אל תנסו את זה בבית)

15/11/2020

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

function foo(x) {
  x *= 2;
  console.log(`00 x = ${x}`);
  function bar(y) {
    console.log(`11 x = ${x}; y = ${y}`);
    y *= 3;
    console.log(`22 x = ${x}; y = ${y}`);
  }
  return bar;
}

foo(2)(5);

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

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

מהן Callback Functions ב CTypes

14/11/2020

ספריית ctypes של פייתון מאפשרת לנו לטעון כל DLL ולהפעיל כל פונקציה ממנו בצורה שקופה מתוך תוכנית פייתון. הרבה ספריות C עובדות בצורה פשוטה: אנחנו קוראים לפונקציה, C מחשב משהו ומחזיר תוצאה. לדוגמה התוכנית הבאה משתמשת ב libm כדי להעלות את המספר 2 בחזקת 3 ומדפיסה את התוצאה 8:

from ctypes import *
from ctypes.util import find_library

libm = CDLL(find_library("m"))
libm.pow.restype = c_double

print(libm.pow(c_double(2), c_double(3)))

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

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

נתבונן בקוד הבא בשפת C:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>

void *print_message_function( void *ptr );

int call_some_threads(printer_t done)
{
  time_t t;
  srand((unsigned) time(&t));
  pthread_t thread1, thread2;
  int  iret1, iret2;

  iret1 = pthread_create( &thread1, NULL, print_message_function, (void*) done);
  iret2 = pthread_create( &thread2, NULL, print_message_function, (void*) done);

  pthread_join( thread1, NULL);
  pthread_join( thread2, NULL); 

  printf("Thread 1 returns: %d\n",iret1);
  printf("Thread 2 returns: %d\n",iret2);
  return 0;
}

void *print_message_function( void *ptr )
{
  int waittime = rand() % 10;
  sleep(waittime);
  printf("[%d]\n",  waittime);
  printer_t callback = (printer_t) ptr;
  callback(waittime);
  return NULL;
}

הקוד מפעיל שני Threads (כל אחד מהם יחכה מספר שניות אקראי בין 0 ל 9) ומדפיס למסך כמה זמן הוא חיכה. אבל, ופה הטריק, כל Thread כזה מקבל גם מצביע לפונקציה. אנחנו לא יודעים מה תהיה הפונקציה - זו מתקבלת כפרמטר done לפונקציה call_some_threads - אבל בסוף הפונקציה print_message_function ואחרי שהדפסנו הודעת דיווח על התקדמות משפת C אנחנו מפעילים את אותה פונקציה שקיבלנו כפרמטר.

אנחנו קוראים לפונקציה שהתקבלה לפרמטר done בשם Callback Function בגלל שזוהי פונקציה באמצעותה קוד C קורא בחזרה לקוד פייתון שהפעיל אותו.

אחרי שאקמפל את קובץ ה C ואהפוך אותו לספריה דינמית אוכל להפעיל את הקוד הבא מ Python כדי להעביר פונקציית Python בתור ערך לפרמטר done:

from ctypes import *

mylib = cdll.LoadLibrary("./libmydemo.so")

@CFUNCTYPE(None, c_int)
def print_done(n):
    print(f"[Python] waited {n} seconds")

mylib.call_some_threads(print_done)

הדקורטור @CFUNCTYPE אומר ל ctypes שיש פה פונקציה שאפשר להעביר ל C, שהיא מחזירה None ומקבלת פרמטר יחיד מסוג int. זה מספיק בשביל לשלוח אותה בתור פרמטר לפונקציה call_some_threads שהוגדרה בקוד ה C. הרצת התוכנית תגרום ל:

  1. יצירת שני Threads מתוך קוד C.

  2. כל Thread מגריל מספר ומתחיל לחכות מספר שניות לפי הערך שהוגרל.

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

  4. אותה פונקציה לאחר מכן תפעיל את הפונקציה print_done של פייתון שהעברנו כפרמטר (ומופיעה בקוד C בשם done ולאחר מכן בשם callback).

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

אם יש לך שעה ללמד נושא גדול

13/11/2020

אם יש לך שעה ללמד נושא גדול אתה יכול לבחור-

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

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

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

איך למשוך מידע מהשרת בכל מעבר נתיב עם Reach Router

12/11/2020

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

  1. בגישת HTML לשרת, כל נתיב יחזיר בדיוק את אותו קוד צד-לקוח.

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

  3. בגישת Ajax לכל נתיב השרת שולח רק את המידע בלי דף ה HTML שמסביבו.

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

חדי העין שבין הקוראים יכולים לזהות את ה Anti Pattern בארכיטקטורה: ריאקט ראוטר לא אוהב שמנסים להריץ קוד "הכנה" לפני שעוברים נתיב. בסופו של דבר ההרפתקה ההיא הסתיימה בזה שזרקתי את React Router ובחרתי ב Router יותר גמיש (נקרא nighthawk ומאוד לא מתוחזק) שדווקא נתן לי לבנות בדיוק את המנגנון שרציתי.

ארבע שנים קדימה ואני שוב בונה ארכיטקטורת צד שרת דומה. הפעם מייקל ג'קסון וריאן פלורנס כבר לא עובדים ביחד והראוטר החדש של ריאן פלורנס נקרא Reach Router. ריאקט עברה להשתמש ב Hooks ואני כבר למדתי שעדיף להריץ את קוד ה Ajax אחרי שנכנסים לנתיב החדש, ולבנות את קוד הקומפוננטות כך שידע לטפל טוב במצב שאין לו עדיין Data להציג. שילוב כל הרעיונות האלה התחבר לקוד הבא עבור הקומפוננטה הראשית:

function App(props) {
  const location = useLocation();
  const loadedUrl = useSelector(state => state.url);
  const dispatch = useDispatch();

  useEffect(function() {
    let active = true;

    if (location.pathname === loadedUrl) {
      return;
    }
    axios.get(location.pathname).then(function(response) {
      if (active) {
        const data = response.data;
        dispatch(actions.newAppstateReceived(data));
      }
    });
    return function abort() {
      active = false;
    }
  }, [location.pathname, loadedUrl]);

  return (
    <Router>
      <Home path="/" />
      <Post path="/posts/:id" />
    </Router>
  )
}

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

  1. כל מה שזז נמצא בקוד הקומפוננטה בעזרת useEffect. אני מחזיק הרבה פחות Middlewares וכמעט שלא משתמש ב thunk.

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

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

  4. אני בכוונה לא שומר Cache של המידע. אם בעתיד זה יידרש אפשר לשלב את axios-cache-plugin או פיתרון Cache אחר בצד הלקוח. ככל שהקוד בתוך האפקט ילך ויסתבך אפשר להוציא אותו ל Custom Hook.

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

לימוד פאסיבי או אקטיבי

11/11/2020

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

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

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

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

  4. לימוד פאסיבי Gets you off the hook (אני לא מכיר גירסה עברית לביטוי הזה). הכל טוב, אני עושה מה שאומרים לי ובסוף אצליח. זה הופך את תהליך הלימוד להרבה פחות מתסכל.

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

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

  1. הרבה אנשים לא נהנים מהמסלול.

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

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

הנה מה שכתב לי תלמיד באתר לפני כמה ימים:

ב 2013 סיימתי קורס בג'אבה בהצלחה בגיון ברייס קבלתי ציון גבוה אך לא הצלחתי למצוא עבודה ללא ניסיון ומאז לא נגעתי בפיתוח.

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

אז מה האלטרנטיבה?

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

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

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

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

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

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

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

שלושה לקחים מסקירת בסיס נתונים ישן

10/11/2020

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

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

המשך קריאה

חידת פייתון ו C

09/11/2020

דני תוכניתני חשב שיוכל לחשב מספרי פיבונאצ'י הרבה יותר מהר אם יעביר את פונקציית החישוב מ Python ל C. הפונקציה ב Python נראתה כך:

def fib(n):
    x, y = 0, 1
    for i in range(n):
        x, y = y, x + y

    return y

ודני שכבר יודע דבר או שניים על חיבור פייתון ל C הלך ותירגם את הקוד ל C באופן הבא:

static PyObject *
spam_fib(PyObject *self, PyObject *args)
{
  int n;
  if (!PyArg_ParseTuple(args, "i", &n))
    return NULL;

  unsigned long x = 0, y = 1;
  unsigned long temp;

  for (int i=0; i < n; i++) {
    temp = x;
    x = y;
    y = temp + x;
  }

  return PyLong_FromUnsignedLong(y);
}

אחרי קומפילציה ובדיקה קצרה של הקוד הבא:

import spam

print(spam.fib(5))

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

מה דני שבר?

התנהגות ברירת מחדל

08/11/2020

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

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

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

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