יום ראשון, 25 בדצמבר 2011

ללמוד מחיפושיות



"באגים הם חשובים"








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


והוא לימד אותי דבר חשוב מאוד על באגים. הוא לימד אותי שבאגים הם חשובים. 


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


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




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




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


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




באגים הם סימן מאלוהי הביטים, שיש לנו משהו ללמוד.












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


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


מה זה אומר? ניתן דוגמה פשוטה. 


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


echo "Hello World \n;

"מה יקרה בפעם הבאה שאני שתקרה לי אותה טעות?"



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

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



תבדקו את ההנחות שלכם



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

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

תבדקו את ההנחות שלכם

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

כלל נוסף:

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

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

זה לא משהו שאני עשיתי בקוד. (וזה כבר טוב :) ) 





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

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

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

var  enemyA:Enemy = new Enemy();
enemyA.x = enemyPositions[1].x;
enemyA.y = enemyPositions[1].y;
this.addChild(enemyA);

var  enemyB:Enemy = new Enemy();
enemyB.x = enemyPositions[1].x;
enemyB.y = enemyPositions[1].y;
this.addChild(enemyB);

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

this.addChild(enemyA);

אז אני חושב שאולי לא מיקמתי אותו טוב במסך, אבל אני בודק את המיקום: 

enemyA.x = enemyPositions[1].x;
enemyA.y = enemyPositions[1].y;

וזה נראה טוב. 



זה באמת קשור לשכפול




אז אני חושב אולי כששיכפלתי את הקוד, שכחתי להחליף איפה שהוא את enemyA ל enemyB אבל אני בודק ורואה שהחלפתי בכל המקומות הנכונים.

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

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



"מה אני יכול לעשות בעתיד כדי להמנע מאותה טעות."




אבל פה כאמור זה לא נגמר, מה אפשר ללמוד מזה:

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

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

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

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

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



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





אין תגובות:

הוסף רשומת תגובה