פרוייקט "התלמיד והמחשב" יוצא לדרך!

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

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

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

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

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

ובכן, למה לא לבחור בכמה שפות? כל מתכנת מתחיל כבר אמור לדעת שאין שפת תכנות “נכונה” אחת - מתכנת טוב צריך להכיר מגוון שפות כי לכל אחת השימושים שלה ונקודות החוזק שלה. אז מצוין, אבחר בכמה שפות, אבל אילו? שפה שאני מאוד אוהב והיא פופולרית למדי היא Python; השפה שבה אני כותב בדרך כלל למטרות מקצועיות היא ++C, ואילו שפה פופולרית ביותר שנמצאת בשימוש רחב ביותר בימינו היא Java. לא אכתוב באף אחת מהשפות הללו. תחת זאת בחרתי לכתוב ב-Ruby, ב-Haskell וב-Javascript.

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

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

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

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

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

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

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

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

בעיה מס' 1

הדוגמה הראשונה היא הבעיה הבאה: נתון מלבן שאורך צלעותיו הן a,b. יש לחשב את ההיקף שלו.

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

  1. קלוט שני מספרים a,b.
  2. חשב את \( 2(a+b) \).
  3. הוצא כפלט את תוצאת החישוב.

ועם זאת, זו תוכנית טובה להתחיל איתה, כי היא מאפשרת לנו לראות את מה שבהתחלה הוא החלק המסובך ביותר להתעסקות איתו - מנגנון הקלט והפלט של התוכנית. העניין הבסיסי הוא זה: קלט מהמשתמש (או מקובץ) נקרא בתור רצף של תווים. למשל, “sddfg” הוא קלט אפשרי אחד. הקלט הזה מיוצג ברוב שפות התכנות בתור מה שנקרא מחרוזת (String) - רצף של תווים. לא מספר. בשל כך, אי אפשר להפעיל פעולות חשבון רגילות על מחרוזות; כדי לבצע פעולת חשבון צריך קודם כל להגיד לשפת התכנות להמיר את הקלט שלה למספר. ברוב שפות התכנות צריך להגיד את זה באופן מפורש, כדי למנוע תקלות (מצב שבו התוכנית מחליטה “על דעת עצמה” להתייחס למחרוזת בתור מספר למרות שאנחנו לא רוצים - כמה פעמים קרה לכם שעבדתם באקסל וכתבתם מספר זהות של מישהו שמתחיל ב-0, ואקסל על דעת עצמו החליט למחוק את ה-0 בהתחלה? אה, לא קרה לכם? טוב, לי זה קרה המון).

בואו נראה את התוכנית ברובי:

puts "Please insert the length of the sides of the rectangle"
a = gets.to_i
b = gets.to_i
circumference = 2*(a+b)
puts "For a rectangle with sides of length #{a} and #{b} the circumference is #{circumference}"

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

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

שתי השורות הבאות קולטות את a ו-b ממה שהמשתמש מקליד. לצורך כך אני משתמש בפקודה שנקראת gets, ומה שהיא עושה היא להמתין למשתמש בזמן שהמשתמש מקליד משהו ולוחץ אנטר, ואז מחזירה את כל מה שהוקלד עד וכולל האנטר. מה קורה למה שהפקודה מחזירה? אה, מייד מפעילים עליו פקודה אחרת, שנקראת to_i, קיצור של to integer - המרה למספר שלם. הפקודה הזו חכמה למדי; אם היא מקבלת מחרוזת שמתחילה במספר שלם ואז יש עוד “רעש” בדמות אותיות באנגלית, או רווחים, או אנטר וכדומה היא יודעת לסלק אותם ולהשאיר רק את המספר השלם; ואם מה שהיא קיבלה בכלל לא נראה כמו מספר שלם היא פשוט מחזירה 0.

בואו נסתכל על מבנה הפקודה: gets.to_i. הנקודה שמחברת את gets עם to_i אומרת “קח את מה ש-gets מחזירה והפעל עליו את to_i”. אפשר לחשוב על זה בתור שרשור של פעולות. מה שקורה כאן בפועל הוא טיפה יותר מסובך (אנחנו קוראים למתודה to_i של מה ש-gets מחזיר) אבל נעזוב את זה לבינתיים.

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

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

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

circumference :: Int -> Int -> Int
circumference a b = 2*(a+b)

main = do
  putStrLn "Please insert the length of the sides of the rectangle: "
  a <- getLine
  b <- getLine
  putStrLn ("For a rectangle with sides of length " ++ a ++ " and " ++ b ++ " the circumference is " ++ show (circumference (read a) (read b)))

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

בואו נתעלם לרגע משתי השורות הראשונות בתוכנית, שבהן מגדירים את הפונקציה שמעניינת אותנו כאן, ונקפוץ דווקא להמשך. את השורה של main = do עזבו - חשבו עליה בתור דרך לומר “כאן מתחילה ריצת התוכנית”. מייד אחריה באה שורת הדפסה, בדומה לזו של התוכנית ברובי, ואחר כך שתי שורות שבהן קוראים ל-a,b את הערכים שלהם. שימו לב שכאן לא ביצעתי המרה ממחרוזת למספר עדיין. בשורה האחרונה אני מוציא את הפלט כמו ברובי, רק שכאשר השילוב של משתנים בתוך מחרוזת הוא מסורבל יותר- אני סוגר את המחרוזת ומשרשר לה את המחרוזות של המשתנים עם האופרטור ++. החלק המסובך ביותר הוא האחרון - אני משרשר למחרוזת את

show (circumference (read a) (read b))

מה הולך כאן? ובכן, אני מפעיל את הפונקציה circumference על שני קלטים - read a ו-read b. כפי שאפשר לנחש, read היא מה שממיר את המחרוזות של a,b למספרים, ו-show לוקח את הפלט המספרי של circumference וממיר אותה למחרוזת.

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

ראשית בואו נסתכל על השורה השניה דווקא:

circumference a b = 2*(a+b)

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

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

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

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

<html>
<head>
<title>Targil 1</title>
</head>
<body>
  <script type="text/javascript">
    compute_circumference = function(){
		var a = parseInt(document.getElementById("a").value);
		var b = parseInt(document.getElementById("b").value);
		var circumference = 2*(a+b);
		document.getElementById("circumference").value = circumference;
    }
  </script>
  a = <input type="textbox" id="a" value = "0" onkeyup = "compute_circumference()"/>
  <br />
  b = <input type="textbox" id="b" value = "0" onkeyup = "compute_circumference()"/>
  <br />
  circumference = <input type="textbox" id="circumference" value = "0"/>
</body>
</html>

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

עכשיו בואו נביט טיפה בקוד. השורה

a = <input type="textbox" id="a" value = "0" onkeyup = "compute_circumference()"/>

מייצרת לנו את הסימנים =a ומייד אחר כך תיבת טקסט (textbox) עם ערך התחלתי 0, מזהה ייחודי בשם “a”, ובנוסף לכך אומרת לתיבה הזו שבכל פעם שבה כותבים בה משהו, אז ברגע שבו המקש במקלדת מפסיק להיות לחוץ, היא מתבקשת להפעיל את הפונקציה compute_circumference() שהוגדרה קודם. זה מבטיח שהמשתמש ייראה את החישוב מתבצע אל מול עיניו כשהוא מזין מידע לתוך תיבות הטקסט כך שהעסק ירגיש אינטראקטיבי. אפשר היה גם לעשות את זה מסורבל - שקודם המשתמש יזין את המידע ואחר כך ילחץ על כפתור והחישוב יתבצע - אבל בשביל מה?

מתחת לתיבה עבור a יש גם תיבות עבור b ועבור הפלט circumference. החישובים הרלוונטיים מתבצעים בסקריפט שלמעלה, שנמצא בין תגי ה-script. ראשית כל אנחנו מגדירים שיש פונקציה שנקראת compute_circumference, באופן הבא:

compute_circumference = function(){

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

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

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


נהניתם? התעניינתם? אם תרצו, אתם מוזמנים לתת טיפ:

Buy Me a Coffee at ko-fi.com