כיצד משתמשים בצורה נכונה בפונקציות?
כעת יש לנו את הכלים להבין מהן הסיבות לשימוש בפונקציות ומה היתרונות שלהן מעל תקיעת כל הקוד שלנו בבלוק ארוך אחד:
-
בידוד: פונקציה חיה בעולם משלה ובאופן כללי אינה מושפעת או משפיעה ממה שקורה מחוץ לה. ישנם יוצאי דופן למקרה זה, כפי שנראה בהמשך הקורס, אבל הבידוד מאפשר לנו לתת לפונקציה לפתור בעיה קטנה אחת בצורה טובה, ללא השפעה חיצונית.
-
פשטות: כשפונקציה מנסה לפתור בעיה אחת בבידוד מכל שאר התוכנית, הרבה יותר קל להבין מה צריך לעשות מאשר בקשר לכל דבר אחר שקורה בתוכנית.
-
פישוט ממשק: פונקציה יכולה להיות מסובכת ככל שנרצה, אבל השימוש בה תמיד נראה אותו דבר - קריאה לה עם הארגומנטים הנכונים. לדוגמא, אולי יפתיע אותכם לדעת ש-
printfהיא פונקציה מורכבת שעושה פעולות מסובכות רבות מאחורי הקלעים על מנת להדפיס למסך את הטקסט המפורמט שלנו. עם זאת, אין לנו שום צורך לדעת או להבין זאת - רק לקרוא לה לפי הממשק שהיא מגדירה. -
פירוק: הרבה מהמשימות שנפתור בקורס בצורה תכנותית יהיו מורכבות מאוד. פירוק משימה מורכבת להרבה תת משימות קטנות, שכל אחת מהן מיוצגת ע״י פונקציה, מאפשר לנו להתמקד כל פעם בשלב אחר בבעיה, ולהרכיב מאוחר יותר את התמונה הכללית.
-
מניעת חזרה: אם קטע קוד חוזר על עצמו, הוצאה שלו לפונקציה וקריאה שלו מונעת טעויות והופכת את הקוד לקריא וברור הרבה יותר.
-
קריאות: גם אם קטע קוד אינו חוזר על עצמו (ובמיוחד אם כן) הרבה יותר קל להבין מה השורה
if (is_even(x)) {...}עושה, על פני השורהif (x % 2 == 0). מתן שם לפעולות מאפשר לנו לכתוב קוד שמתנהג ונראה הרבה יותר כמו שפה טבעית מאשר פעולות מכונה שאינן ברורות לנו.
מהי פונקציה טובה?
קיימים אינספור חוקים לכתיבת פונקציות מוצלחות - נציין כמה מהם:
- פונקציה טובה עושה משימה אחת בלבד.
- שם של פונקציה טובה אומר בדיוק מה היא עושה. למשל:
- פונקציה שמחזירה
boolיכולה להיות מנוסחת כשאלה, כמוbool is_even(int x). - פונקציה שמחשבת ערך ומחזירה אותו יכולה להיות מנוסחת בתור פעולת הבאה, כמו
float get_discr(float a, float b, float c). - פונקציה שמבצעת פעולה יכולה להיות מנוסחת כפועל, כמו
void print_age(int age).
- פונקציה שמחזירה
- פונקציה טובה היא קצרה - כאשר פונקציה נעשת ארוכה, ככל הנראה קיימת פונקציה נוספת המסתתרת בתוכה.
- פונקציה טובה היא פונקציה עם מספר ארגומנטים קטן ככל הניתן.
Note
לפעמים קשה להגדיר מהי ״משימה אחת״ בהקשר של עקרון #1. אפשר לנסח אותו מעט אחרת - פונקציה נשארת באותה רמה של פעולה תמיד, ובמידה ולא יש לייצר פונקציה נוספת. נראה לכך דוגמא בהמשך.
דוגמאות
Clamp
נתחיל מפונקציה בשם clamp שמקבלת 3 מספרים - גבול תחתון lower, גבול עליון higher ומספר x.
אם x קטן מ-lower, נחזיר את lower. אם הוא גדול מ-higher, נחזיר את higher - אחרת, נחזיר את x עצמו.
מימוש אפשרי יהיה:
int clamp(int lower, int higher, int x) {
if (x <= lower) {
return lower;
}
if (x >= higher) {
return higher;
}
return x;
}
כעת נניח שמבקשים מאיתנו לכתוב פונקציה שמחזירה את המקסימום מבין ה-clamp של שני ערכים. מתמטית, אנחנו מחפשים את הערך הבא:
$$ \max\big(\text{clamp}(a), \text{clamp}(b)\big) $$
ואז המרת הפתרון לקוד נהית יחסית פשוטה - נכתוב פונקציה שמבצעת max ואז נבצע הרכבה לפונקציות:
int clamp(int lower, int higher, int x) {
if(x <= lower) {
return lower;
} else if(x >= higher) {
return higher;
}
return x;
}
int max2(int x, int y) {
return a > b ? a : b;
}
int max_of_clamp(int lower, int higher, int x, int y) {
return max2(clamp(lower, higher, x), clamp(lower, higher, y));
}
זו דוגמא קלאסית לפירוק בעיה מורכבת למספר שלבים ע“י כתיבת פונקציה לכל שלב.
משוואה ריבועית
נמשיך בדוגמא לפונקציה שפותרת משוואה ריבועית - חתימה של פונקציה כזו יכולה להיות כזו:
// Pass arguments to equation in the format ax^2 + bx + c = 0
float solve_quadratic(float a, float b, float c);
אנחנו מכירים את הפתרון למשוואה ריבועית:
$$ x_{1,2} = \frac{-b \pm \sqrt{b^2 - 4ac} }{2a} $$
כפי שאולי אתם מכירים (ובמידה ולא - זוהי עובדה שקשורה לדוגמא בלבד, אין צורך לדעת אותה בכללי) הביטוי שבתוך הסוגריים נקרא הדיסקרימיננטה ומסומן באות \(\Delta\). כאשר \(0\le\Delta\) קיים פתרון ממשי למשוואה הריבועית, ולכן נרצה לבדוק אותו בנפרד מחישוב שאר הפתרון.
כלומר, ניתן לכתוב:
$$ x_{1,2} = \frac{-b \pm \sqrt{\Delta} }{2a}, \Delta = b^2 -4ac $$
לכן, חלוקה של הפתרון לשלבים יכולה להיות כזו:
- חשב את הדיסקרימיננטה של הארגומנטים.
- אם היא שלילית - הדפס הודעת שגיאה וסיים את הפונקציה אם שגיאה.
- אחרת - חשב את הפתרון בשימוש בדיסקרימיננטה (אין צורך לחשב אותה פעם נוספת).
לצורך הדוגמא נחזיר רק את הפתרון שמתקבל ע“י ה-+ בנוסחא הריבועית. מימוש אפשרי יכול להיות כזה:
#include <math.h>
#include <stdio.h>
#define NO_REAL_SOLS 1
// discr - short for discriminant
float calculate_discr(float a, float b, float c) {
return pow(b, 2) - 4 * a * c;
}
float calculate_solution(float a, float b, float discr) {
return (-b + sqrt(discr)) / (2 * a);
}
float solve_quadratic(float a, float b, float c) {
float discr = calculate_discr(a, b, c);
if (discr < 0) {
printf("No real solutions!\n");
return NO_REAL_SOLS;
}
return calculate_solution(a, b, discr);
}
נשים לב להפרדה ברורה - הפונקציה solve_quadratic כתובה, כמעט באנגלית ברורה, כשרשור של פונקציות. פונקציות העזר calculate_solution ו-calculate_discr מבצעות כל אחת משימה אחת - מה ששם הפונקציה אומר.
Note
השתמשנו בפונקציה
sqrtהמוגדרת בקובץmath.hלצורך הדוגמא, בצורה דוגמא להבאת הפונקציהprintfמהקובץstdio.h.
המרת זמנים
כדוגמא נוספת, נכתוב פונקציה שמקבלת אורך זמן כלשהו בשניות seconds ותפקידה יהיה להדפיס המרה של אורך הזמן הזה לשעות, דקות ושניות:
#include <stdio.h>
#define SECONDS_IN_MINUTE 60
#define MINUTES_IN_HOUR 60
#define SECONDS_IN_HOUR (SECONDS_IN_MINUTE) * (MINUTES_IN_HOUR)
int extract_hours(int seconds) { return seconds / SECONDS_IN_HOUR; }
int extract_minutes(int seconds) { return seconds / SECONDS_IN_MINUTE; }
void convert_seconds_to_time(int seconds) {
int hours = extract_hours(seconds);
seconds %= SECONDS_IN_HOUR;
int minutes = extract_minutes(seconds);
seconds %= SECONDS_IN_MINUTE;
printf("%d : %d : %d\n", hours, minutes, seconds);
}