טיפוס נתונים

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

טיפוסים פרימיטיביים

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

ערכי אמת

ישנם שני ערכי אמת: "אמת" ו-"שקר". כדי לייצג ערך אמת מספיקה סיבית אחת, אך כיוון שלסיבית בודדת אין כתובת, לעיתים ערך אמת נשמר בבית או במילה. בשפות C ו ++C, הערך "שקר" מותאם לערך השלם 0, והערך "אמת" מותאם לשאר הערכים השלמים, אך לרוב מיוצג על ידי 1. בשפות מעטפת בלינוקס, כגון C Shell, המוסכמה היא הפוכה, בשל העבודה הצמודה עם תוכניות בלינוקס - שם מקובל שערך שונה מ-0 מייצג הצלחה, לעומת ערכים אי-שליליים המייצגים מספר שגיאה. בשפות אחרות, למשל Java, לא ניתן להחליף בין ערכים מספריים לערכי אמת, ואלו טיפוסים נפרדים לחלוטין. מכיוון שפעמים רבות מסתפקים רק בשימוש בערכים 0 או 1, ערכי אמת יכולים להיות מוגדרים ומיוצגים כמשתנים בוליאנים, ומכונים גם "דגל".

יחידה

טיפוס היחידה (unit) הוא טיפוס המכיל ערך אחד בלבד. אין צורך להבדיל בין סימון הערך לסימון הטיפוס כי ישנה התאמה חד-חד ערכית ביניהם. לביטוי שערכו הוא מטיפוס יחידה אין שימוש כחלק מביטוי אחר, ולכן הוא משמש רק לצורך השגת תוצאת הלוואי של הפעולה. טיפוס זה משמש על פי רוב בקריאה לפונקציה שכוללת תוצאת לוואי כגון פעולת פלט. פונקציה שמחזירה טיפוס כזה היא למעשה פרוצדורה.
בשפת ML הטיפוס נקרא unit ומסומן "()". בשפות ממשפחת ה-C הטיפוס נקרא void; בשפת פייתון הטיפוס נקרא NoneType וערכו נקרא None.
יש להבחין בין טיפוס שמכיל רק איבר בודד כערך לבין טיפוס השקול לקבוצה הריקה. טיפוס שהוא קבוצה ריקה פירושו שאין לו כל ערך קריאה לפונקציה ה"מחזירה" טיפוס כזה היא למעשה קריאה לפונקציה שאיננה חוזרת כלל (לדוגמה, פונקציית terminate המסיימת את פעולת התוכנית).

טיפוסי נתונים מורכבים

רוב השפות המודרניות מאפשרות ליצור טיפוסי נתונים חדשים על ידי הרכבה של טיפוסים קיימים.
הדרך הפשוטה ביותר היא פשוט לתת שם חדש לטיפוס קיים (בשפת C, למשל, הפעולה הזאת מתבצעת על ידי ההצהרה typedef). פעולה כזאת תאפשר לשפר את קריאות התוכנית. לדוגמה, בשפת C (בתקן הראשון) אין טיפוס נתונים נפרד עבור ערכי אמת\שקר - מקביל לטיפוס Boolean בשפת פסקל. אם רוצים לדמות טיפוס כזה, אחת הדרכים היא להוסיף את ההצהרה
typedef int bool;
בהמשך התוכנית ניתן להציב ערכי אמת ושקר אל משתנים מטיפוס bool, וכך המטרה של משתנים אלה נעשית ברורה יותר למי שקורא את הקוד. ישנן שפות בהן הצהרה כזו היא פשוט הענקת שם חדש לטיפוס הקודם. בשפות אחרות הטיפוס החדש הוא טיפוס נפרד ולא ניתן להחליף בקלות פעולות על הטיפוס הישן עם פעולות על הטיפוס החדש.

רשומה

רשומה (record) או מבנה (struct) זהו טיפוס מורכב בדומה למכפלה קרטזית, בהבדל אחד: לכל איבר בו ניתן שם. דוגמה למבנה בשפת C שמתאר זמן:
struct Time{
int seconds;
int minutes;
int hours;
}
בניה כזאת מאפשרת להתעסק במרוכז עם משתנים בעלי קשר הדדי ביניהם, להעביר אותם יחד לפונקציה, לאתחל אותם ביחד וכדומה. מבחינה מתמטית אין הבדל בין רשומה לTuple, אך מבחינת התחביר יש הבדל: ברשומה קל יותר להבין מה המשמעות של כל איבר, ואילו התחביר של מכפלה קרטזית קומפקטי וחסכוני יותר. גישה לאיברים במערך נעשית תוך שימוש באופרטור מתאים. לדוגמה, בשפות ++C/C, עבור משתנה t מטיפוס Time, גישה לשניות בו תתבצע בצורה t.seconds או (אם מדובר במצביע) t->seconds. בשפות אחרות ישנם אופרטורים אחרים המשמשים לאותה מטרה.
חשוב לשים לב שרשומה עלולה להחזיק אוסף ערכים שאין לו משמעות לוגית בתוכנית. האחריות להימנעות מכך היא על המתכנת, דרך הפעולות המתבצעות על משתנים מטיפוס זה. הרחבה של רשומה כך שגם פעולות על המשתנים השייכים אליה יתבצעו רק דרך פרוצדורות שמוגדרות ברשומה עצמה יוצרת למעשה מבנה נתונים מופשט, וקשורה לרעיונות הנכללים בתכנות מונחה עצמים.

טיפוס מאוחד (Union)

אם נתייחס לכל טיפוס כאל קבוצת הערכים הכלולים בו, עולה באופן טבעי האפשרות להגדיר איחוד של טיפוסים, בדומה לאיחוד קבוצות במתמטיקה. באופן זה, ניתן יהיה, למשל, להגדיר טיפוס שיכלול ביטויים מסוג מספר שלם וביטויים מסוג תו. הגדרה של טיפוס כזה בשפת C תהיה דומה להצהרה על מבנה, למעט המילה השמורה struct שתוחלף ב-union:
union {
 int integer;
 char character;
}
בכל נקודת זמן רק אחד מהשדות integer או character מחזיק מידע בעל משמעות. הדמיון בין התחביר של איחוד לשל רשומה נובע מכך שכל ההבדל הוא סוג הפעולה המתבצעת בין קבוצות - איחוד לעומת מכפלה קרטזית.
איחודים משמשים בעיקר למטרת חיסכון במקום - עבור כל משתנה יש צורך להקצות רק מקום עבור הטיפוס הגדול יותר. שימוש אחר של טיפוסים כאלה הוא למקרים בהם רוצים להתייחס לאותו רצף של סיביות, פעם אחת בתור משתנה מסוג א' ופעם שנייה בתור משתנה מסוג ב'. תכנות כזה הוא תכנות ברמה נמוכה, ומשמש כיום כמעט אך ורק במערכות זמן-אמת או מערכות משובצות מחשב. בשפות תכנות עיליות רבות טיפוסי נתונים מהסוג הזה אינם קיימים. אחת הבעיות היא שבמקרה של ניסיון לחסוך במקום, לא מצורף לטיפוס מידע על איזה מהשדות שלו בשימוש בכל רגע נתון. עובדה זו מקלה על הכנסת שגיאות לתוכנית.

טיפוסי נתונים רקורסיביים

ישנן שפות המאפשרות להגדיר טיפוסים חדשים תוך שימוש ברקורסיה.
לדוגמה, ניתן להגדיר רשימה מקושרת כTuple, מהצורה הבאה (הסינטקס הוא של שפת ML, ואין צורך להבין אותו במלואו):
datatype intlist = nil
| cons of int * intlist;
כאן הגדרנו רשימה intlist בתור טיפוס שכל ערך בו הוא איבר ריק (nil) או Tuple מהצורה (int, intlist). הגדרה זו מתייחסת לעצמה, ולכן נקראת רקורסיבית. מבנה נתונים רקורסיבי כזה הוא בפוטנציאל אינסופי, ולכן לא ניתן לבניה מלאה; היצירה שלו מתבצעת כחישוב עצל (Lazy),
ורק החלקים שהתוכנית מנסה לגשת אליהם בפועל בכל משתנה אכן מוגדרים.
באופן דומה, ניתן להגדיר בשפות המאפשרות הגדרה רקורסיבית גם עץ באותה שיטה:
datatype inttree = int
| cons of inttree * inttree;
בשפות אימפרטיביות, כדוגמת C ודומותיה, על פי רוב לא קיימים טיפוסים רקורסיביים. עם זאת, ניתן לדמות אותם בעזרת מצביע: במקום להצהיר על טיפוס שמכיל את עצמו, ניתן להצהיר על טיפוס שמכיל מצביע למשתנה מאותו טיפוס. הגדרה כזאת איננה רקורסיבית, אך בפועל נותנת את אותה גמישות בדיוק, במחיר של תחביר שעשוי להיות פחות מובן.