שפות תכנות - עושים קצת סדר - טיפוסים
לפעמים יוצא לי להתקל בקונספציות מסויימות לגבי שפות תכנות. ההבדלים בין טיפוסיות דינמית לסטטית, טיפוסיות חזקה לחלשה, שפות מתקמפלות או מפוענחות, שפות פונקציונליות או מונחות עצמית, ועוד. בואו נעשה קצת סדר.
והפעם נדבר על: טיפוסים.
טיפוסים וטיפוסיות
מה ההבדלים בין טיפוסיות סטטית, דינמית, חלשה או חזקה? מה זה בכלל טיפוס?
טיפוס (type) מייצג קבוצת ערכים אפשריים שערך מסויים שייך אליה. לדוגמא, מספרים שלמים, טקסט, קואורדינטות על המסך, וכו'. בסופו של דבר, כל המידע במחשב מיוצג באמצעות המון המון ספרות בינאריות - או אפס, או אחד. כדי לתת משמעות לסדרות של ספרות בינאריות, עלינו לקבוע קונבנציה שמגדירה מה אסופה כזו של ספרות מייצגת.
דוגמא לקונבנציה כזו היא ASCII, בעזרתה ניתן לייצג תוים כמו אותיות באנגלית, ספרות, מקפים, ורווחים באמצעות 8 ביטים (ספרות בינאריות). באמצעות שמונה ביטים אנחנו יכולים לייצג 256 אופציות שונות. טבלת ה-ASCII ממפה כל אופציה לתו מסויים.
כאשר אנחנו יודעים מה הטיפוס של ערך או משתנה מסויים אנחנו למדים מה משמעות הביטים שמייצגים אותו, וכמה זכרון לקרוא כדי להבין אותו. בנוסף, אנחנו יכולים לדעת אילו פעולות ניתן לבצע על הערך. לדוגמא, ערכים מטיפוס מספר שלם ניתן לחבר אחד לשני, או להעלות בריבוע. במחרוזות טקסט אפשר לחפש האם הן מתחילות בטקסט מסויים. אנחנו גם יכולים לדעת אילו פעולות לא ניתן לבצע על הערך, למשל, מה המשמעות של להעלות מחרוזת טקסט בריבוע?
אחרי שהבנו מה הם טיפוסים ומה החשיבות שלהם, מה ההבדלים בין טיפוסיות סטטית, דינמית, חלשה או חזקה?
טיפוסיות סטטית
בשפות תכנות בעלות טיפוסיות סטטית אנחנו יודעים מה הטיפוס של כל ביטוי (ערך, משתנה, פונקציה וכו') לפני שאנחנו מריצים את התוכנית, ויש לנו יכולת להחזיר הודעות שגיאה על קוד לא נכון (למשל, העלאה בריבוע של משתנה המכיל טקסט) לפני שהתוכנית הכתובה בשפה מתחילה לרוץ. (שימו לב שלא אמרתי זמן קומפילציה! נדבר על זה בפוסט אחר)
לטיפוסיות סטטית יש הרבה יתרונות, פידבק מוקדם ומקיף עוזר לגלות באגים מוקדם, יכול לסייע בהבנה של קוד לא מוכר, ויכול לספק כלים שעוזרים לכתיבת הקוד, למשל, קל לענות על השאלה "אילו פעולות אפשר לבצע על המשתנה הזה?" כשיודעים מה הטיפוס שלו.
לטיפוסיות סטטית יש גם חסרון. מאחר ולא ניתן להסיק תכונות מעניינות לגבי תוכנה שרירותית, אנחנו נאלצים לדחות תוכניות תקינות שיכולות לרוץ בהצלחה בלי בעיה אבל לא עומדות בקריטריונים של טיפוסיות סטטית.
המחקר במערכות טיפוסים של שפות תכנות מתעסק הרבה בשאלה "כיצד אנחנו יכולים לצמצם את כמות התוכניות התקינות שאנחנו נאלצים לדחות", שהיא גם באיזשהו מובן "איך אפשר להפוך את העבודה עם טיפוסיות סטטית לארגונומית יותר?".
עוד תפיסה מוטעית שיצא לי להתקל בה היא שטיפוסיות סטטית אומרת שעל המתכנת לציין מה הטיפוס של כל משתנה ופונקציה. לא כך הדבר. בשפות ישנות מאוד ובשפות בעלות פיצ'רים שמקשים על גילוי אוטומטי של טיפוסים (בדרך כלל מונחות עצמים), צריך לציין מה הטיפוסים של דברים, אבל שפות פונקציונליות רבות ממשפחת ML, לדוגמא OCaml, הן שפות בעלות טיפוסיות סטטית בה הטיפוסים מוסקים בצורה אוטומטית ואין כלל הכרח לציין מה הטיפוס של אף ביטוי. בגלל שאילו שפות לא כל כך נפוצות, ושפות עם טיפוסיות סטטית כן נפוצות הן או מונחות עצמים, או ישנות, או חושבות שהן ישנות, צצה התפיסה ש"טיפוסיות סטטית" אומר שקוד חייב להראות כך:
ArrayList<String> names = new ArrayList<String>();
אבל לא, זה לא נכון.
טיפוסיות דינמית
בשפות בעלות טיפוסיות דינמית, אנחנו לא בודקים טיפוסים לפני ריצת התוכנית, אלא תוך כדי הרצת התוכנית.
זה אומר שבשפה כמו Python, אנחנו יכולים לכתוב קוד שנראה כך:
def stringify(x):
if STRINGIFY_ENABLED:
return x
else:
return str(x)
כלומר, פונקציה שיכולה להחזיר טיפוסים שונים בתלות ב-if
מסויים, ואם נשתמש בתוצאה בצורה נכונה, ריצת התוכנית תסתיים בהצלחה.
לטיפוסיות דינמית יש יתרונות. למשל, אם אנחנו רוצים לכתוב אלגוריתם גנרי שיעבוד לכל מבנה נתונים שניתן "לטייל" עליו משמאל לימין ומכיל כל טיפוס שאפשר להדפיס ייצוג שלו למסך, צריך רק לדאוג שמבנה הנתונים מממש את ה-API האחיד של ה"טיול", ושגם הטיפוס מממש את ה API המתאים להדפסה.
בשפות בעלות טיפוסיות סטטית, על השפה לספק דרך להגדיר ממשקים בקוד בצורה אותה בודק הטיפוסיות יבין, לספק דרך לתאר מבני נתונים בעלי ממשק אחיד, או בכלל, לספק דרך להגדיר מבני נתונים שיכולים להחזיק טיפוס מסוג משתנה (מה שבחלק מהשפות קוראים לו Generics). עוד בעיה שיכולה לעלות היא בעיה של תלויות בקוד. למשל, היכן מוגדרת ההגדרה של הממשק? אם אני מגדיר אותה בקוד שלי, הרי שקוד ספרייה חיצוני לא יכול להכיר את הקוד שלי, אז אני צריך ליצור בעצמי את הקשר בין הממשק שהגדרתי לבין פונקציות הספרייה.
ועדיין, יש גם מספר חסרונות לטיפוסיות דינמית. המשמעותי והברור ביותר ביניהם הוא זה שאין לנו כל הבטחות על נכונות הקוד שלנו לפני ריצת התוכנית, אלא אם נוסיף אותן בעצמנו בצורה של בדיקות. אם חיברנו מספר למחרוזת, קראנו לפונקציית חיפוש על בוליאני? ניסינו להשוות שתי פונקציות? התוכנה תזרוק שגיאה ותיסגר. כל אלו שבודק טיפוסים יכול לבדוק לפני שבכלל נריץ את התוכנית. וככל שהקוד בייס גדל וגדל, כך קטנה היכולת שלנו לוודא שכל שורה ושורה שהתוכנית יכולה להגיע אליה לא מכילה טעות כזו או מוקש שמחכה להתפוצץ.
טיפוסיות חזקה
רבים מבלבלים בין המונחים "חזקה" ו"סטטית" בהקשר לטיפוסיות. בעוד שסטטי/דינמי מתייחס למתי אנחנו בודקים טיפוסים, טיפוסיות חזקה/חלשה מתייחס לאופן בו אנחנו בודקים טיפוסים, בלי קשר למתי תתבצע הבדיקה.
בשפות עם טיפוסיות חזקה, לכל ערך או ביטוי יש טיפוס ספציפי, ואם ננסה להתייחס לטיפוס הזה בדרך אחרת שלא מתאימה לו, נקבל שגיאה.
בשפות כאלו בדרך כלל לא נוכל לכפול מספר שלם למספר עשרוני, לא נוכל לבצע פעולת NOT
על מספר,
ובטח ובטח לא נוכל לבצע חיבור בין מחרוזת ומספר. עלינו לקרוא לפונקציות הנכונות עם פרמטרים מהטיפוסים הנכונים, ואם אנחנו בכל זאת
רוצים להשתמש ב-, לדוגמא, מספר שלם במקום של מספר עשרוני, עלינו לבצע את ההמרה בצורה מפורשת.
טיפוסיות חלשה
טיפוסיות חלשה מתייחסת למקרים בהם השפה מנסה להיות נחמדה ולהמיר טיפוסים מאחד לשני בצורה אוטומטית ובשבילנו. מאחר וזה כנראה יקרה עבור טיפוסים מסויימים בלבד, טיפוסיות חלשה/חזקה היא מעין ספקטרום. שפה בעלת טיפוסיות חלשה בדרך כלל תהיה חזקה עבור טיפוסים מסויימים וחלשה באחרים.
אם יצא לכם לעבוד עם שפת C,
יכול להיות שניסיתם לכתוב פונקציה שמחשבת ממוצעים במערך. גם אם הגדרתם את המשתנה שלכם כ float
,
אם בטעות טיפוס ההחזרה של הפונקציה היה int
,
שפת C
תנסה לעזור ולהמיר את המספר העשרוני למספר שלם אוטומטית, וכך תקבלו תוצאה לא נכונה.
מעצבן!
ואם יצא לכם לעבוד עם שפה כמו JavaScript,
אפילו ביטוי לכאורה תמים כמו x + y
יכול להעלות נורות אזהרה. אם שני המשתנים הם מספרים, נקבל מספר שהוא סכומם, אם אחד מהם הוא מספר והשני הוא מחרוזת שמכילה מספר,
נקבל חזרה מחרוזת ששירשרה את המחרוזת והמספר יחד!
אם אתם יכולים לחשוב על סיבה למה תרצו התנהגות כזו בשפת תכנות, שלחו לי מייל :)
מה עוד?
דיברנו על מה שבתכלס רציתי לדבר עליו בפוסט, אבל אם אנחנו כבר פה, בואו נדבר על עוד שני דברים שהם אולי קצת פחות נפוצים בשפות תכנות היום.
טיפוסיות הדרגתית
הרעיון של טיפוסיות הדרגתית יחסית חדש. בשפה עם טיפוסיות הדרגתית אפשר להגדיר מהו הטיפוס של חלק מהביטויים, והדבר הזה יבדק לפני ריצת התוכנית, או שאפשר לא להגדיר טיפוס עבור ביטוי מסויים, ואז בודק הטיפוסים לא ידע מה הטיפוס של הביטוי לפני ריצת התוכנית ולא יבדוק אותו. כך נוצר מצב שחלק מהתוכנה בעל טיפוסיות סטטית, וחלק בעל טיפוסיות דינמית.
ההבטחה של שפות עם טיפוסיות דינמית היא כזו של הגמישות של שפות עם טיפוסיות דינמית איפה שצריך, והבטחון של טיפוסיות סטטית איפה שצריך. טיפוסיות סטטית יכולה בנוסף גם להביא איתה ביצועים טובים יותר, שכן בשלב האנליזה והאופטימיזציה יש לנו יותר מידע לעבוד איתו, ואז דווקא בחלקים שחשוב שיהיו מהירים אפשר להשתמש בטיפוסיות הסטטית, ובאחרים בהם צריך יותר גמישות, בטיפוסיות דינמית.
אני מודה ומתוודה שלא יצא לי לעבוד עם שפות תכנות עם טיפוסיות הדרגתית, לדוגמא Typed Racket ו-TypeScript. אני מרגיש טיפה סקפטי לגבי הקונספט, אבל קשה לי להגיד יותר לגביו.
חסרי טיפוס
הרבה אנשים מתבלבלים בין הרעיון של טיפוסיות דינמית, שבה טיפוסיות נבדקת בזמן ריצה, לבין חוסר טיפוסיות, בה לא נבדקים טיפוסים באף שלב כי אין טיפוסים.
כמו שציינו בתחילת הפוסט, בסופו של דבר הזכרון במחשב מכיל דבר אחד: מאגר עצום של ערכים בינאריים - ביטים, שבדרך כלל מקובצים לקבוצות של 8 כבייטים. אלו מספרים. טיפוסים, נותנים משמעות למספרים האלה, ועוזרים לנו להבין איך להסתכל עליהם, מה הם מייצגים, ואיזה פעולות אפשר לבצע עליהם.
אבל זה לא דבר שבהכרח צריך ששפת התכנות תדאג לו. במיוחד אם כל הפעולות שאנחנו יכולים לבצע מורכבות ממניפולציה של מספרים.
השפה חסרת הטיפוסים הכי בולטת היא כמובן אסמבלי (לפחות הנפוצות ביותר, שכן יש גם שפות אסמבלי בעלות טיפוסים!). הפעולות שאנחנו יכולים לבצע בהן הן על מספרים בינאריים שנמצאים באוגרים (registers), או מובאים מהזכרון, וכו'. בשום שלב אין מכניזם שבודק את משמעות הבייטים שאנחנו עובדים איתם, ועלינו לשים לכך לב.
סיכום
עולם הטיפוסים בשפות תכנות הוא עולם ומלואו ונושא מחקר אקטיבי עשיר בו אנשים כל הזמן מנסים לאתגר את מה שאנחנו יכולים לעשות עם טיפוסים. בפוסט הזה ניסיתי לגעת בקצה הקרחון ולסדר קצת את הטרמינולוגיה והמשמעות של המונחים הנפוצים ביותר.
רוצים להגיב? בדקו מהי שאלת הסינון בעמוד הראשי.
0 תגובות