בOCaml אחד הפיצרים שהקומפיילר נותן הוא type inference. הפיצ׳ר הזה מאפשר למתכנת לא להצהיר על המשתנה ישירות אלא לתת לקומפיילר באמצעות כללי היסק לפרש את הטיפוס בעצמו. Type inference נותן לנו את היכולת להמיר תחשיב למדא ללא טיפוסים לתחשיב למדא עם טיפוסים וככה לקבל את הפיצ׳ר של type safety.
נבין את כיצד עובד המנגנון של הסקת טיפוסים על מקרה מצומצם של השפה. נגדיר אותו כ , שילוב של תחשיב למדא ואוקמל .בשפה זאת הביטויים מוגדרים כך
כל משתנה הוא ביטוי.
אם x משתנה ו t ביטוי אז ביטוי.
אם ביטויים אז ביטויים.
הגדרה:
נניח קבוצה אינסופית של משתני טיפוס.
- אם משתנה טיפוס אז הוא טיפוס.
- אם T,S טיפוסים אז כך גם .
הגדרה:
יחס ההטפסה של מוגדר כך -
בעיית הסקת הטיפוסים
נגדיר את בעיה זו כך: קלט- ביטוי t פלט- הקשר הטפסה ומיפוי m מביטויים שמופיעים ב t לטיפוסים כך שלכל תת-ביטוי של t מתקיים (כולל t עצמו).
פתרון:
יצירת מערכת משוואות בין ביטוי לטיפוס
פתרון המערכת
תרגום הפתרון ל ו m המתאימים.
יצירת מערכת משוואות
נגדיר t ביטוי נורמלי אם לכל 2 ביטויים ו שמופיעים ב t , המשתנים x ו y שונים זה מזה.
נשים לב: לכל ביטוי יש ביטוי נורמלי השקול לו על ידי שימוש בכלל אלפה .
הגדרה עבור t ביטוי נגדיר קבוצת המשוואות. לכל מופע של כל ביטוי שמופיע ב t ניצור משתנה טיפוס משלו ואז
אם מתאימים למופעים שונים של אותו הטיפוס אז נוסיף את ל .למשל עבור הפונקציה נסמן עבור ה x הראשון שהוא מטיפוס והשני הוא מטיפוס ונוסיף את ל .
אם משתני הטיפוס מתאימים ל בהתאמה אז נוסיף את ל .
לכל מופע אם מתאימים ל בהתאמה אז נוסיף את ל .
דוגמה: עבור הביטוי
כעת נגדיר את :
תרגום הפתרון
כיצד מתרגמים את הפתרון לכללי הטפסה ומיפוי?
באמצעות כללי substitution. החלפה היא פונקציה שמתאימה טיפוס לכל משתנה טיפוס.
החלפה מורחבת לטיפוסים באופן הבא
הגדרה: החלפה מאחדת שיוויון אם . מאחדת קבוצת שוויונות אם היא מאחדת את כל השיוויונות בקבוצה.
נסתכל על הדוגמה ממקודם-
וגם
הגדרה: יהי t ביטוי ו פונקציה שמתאימה לכל תת ביטוי של t את משתנה הטיפוס המתאים לאחד המופעים של ותהי החלפה שמאחדת את אז
משפט: ו הם הפתרון לבעיית ההטפסה של t. בצורה פורמלית: יהי ביטוי ו כמו לעיל אז
אם אין כנ״ל אז t לא מטופס היטב.
פתרון מערכת משוואות
הגדרה:
בעיית היוניפיקציה- קלט- קבוצה של שוויונות בין ביטויים פלט- החלפה שמאחדת אותם
למשל אם x=y נרצה לבנות כך ש נוכל להגדיר ש .
ומה אם המשוואה אומרת ש ? כיוון שלא משנה מה יהיו כללי ההחלפה של המשתנים יתקיים וכנ״ל עבור אז אין פתרון.
מקרה נוסף שיכול לעניין אותנו בניתוח האלגוריתם שנראה בהמשך הוא מהצורה
type id = string;;
type term = | Var of id | Term of id * term list
(*invariant for substitution*)
(*no id on a lhs occurs in any term earlier in the list*)
type substitution = (id * term) list
(*check if a variable occurs in a term*)
let rec occurs (x : id)(t : term) = match t with
| Var y -> x = y
| Term (_ , s)-> List.exists ( occurs x )s
(*substitute term s for all accurrences of variable x in term t*)
let rec subst (s : term)(x : id)(t : term) = match t with
| Var y -> if x = y then s else t
| Term (f,u) -> Term ( f, List.map (subst s x) u)
(*apply a substitution right to left *)
let apply (s: substitution)(t:term) =
List.fold_right (fun (x,u)->subst u x)s t
(*unidy one pair*)
let rec unify_one (s:term)(t:term) = match (s,t) with
| (Var x, Var y) -> if x = y then [] else [(x,t)]
| (Term(f,sc), Term(g,tc))-> if
f = g && List.length sc = List.length tc
then unify(List.combine sc tc)
else failwith "not unifiable: head symbol conflict"
| ((Var x, Term(_,_ as t)))
| (( Term(_,_ as t), Var x))->
if occurs x t
then failwith "not unifiable: circularity"
else [(x,t)]
(*unify a list of pairs*)
and unify (s: (term * term)list) = match s with
| []->[]
| (x,y) :: t ->
let t2 = unify t in
let t1 = unify_one (apply t2 x)(apply t2 y) in t1@t2;;
ובפסודו קוד זה האלגוריתם לקביעת הטיפוסים
נסתכל על דוגמה של ה ביטוי . לפי הקוד זה יהיה מהצורה Term("->", [Var ('a'), Var('a')].
substitution הוא פונקצית ההחלפה .
occurs היא פונקציה שבודקת אם id כלשהו מופיע בביטוי t מסויים.
subst - מחליפים את המופיעים החופשיים של x ב s בביטוי t
apply - מפעילה את subst על כל איבר ב s מהסוף.
unify_one and unify - נזכיר ש and זה keyword שמאפשר mutual recursion ב OCaml. זה נועד לפתור את המקרה של .
unify- מקבל רשימה של זוגות של term כאשר היחס בינהם הוא שיוויון. אם הרשימה ריקה אז המקרה ברור, אם יש איבר מהצורה (x,y) אז פותרים את יתר הרשימה t ואז מבצעים unify_one על התוצאה עם x ועל התוצאה עם y ומחזירים את שרשור התוצאה.
unify_one- מבצע unify על זוג של term יחיד שהיחס בינהם הוא שיוויון.
אם שניהם משתנים שונים אז מבצעים substitution לביטוי t.
אם המקרה יותר מורכב של פונקציות אז בודקים האם הם מכילים את אותו מספר ארגומנטים וגם שהם שווים ואז מבצעים unify לכל זוג בנפרד בצורה רקורסיבית
המקרה השלישי והאחרון בודק מצב שיש בצד אחד משתנה ובצד השני ביטוי ופשוט בודקים האם יש מצב שהמשתנה מופיע בביטוי ואם כן מחזירים שגיאה כי אחרת נקבל רקורסיה אין סופית של החלפות.
Note
האלגוריתם הזה הוא לא היחיד שפותר את בעיית היוניפיקציה, האלגוריתם הזה מוצא את מה שנקרא Most General Unifier (MGU) וכל פתרון אחר אפשר למצוא באמצעות MGU . נאמר ש היא יותר ״כללית״ מ אם קיימת כך ש
דוגמה
let f x = 2+x .
נשים טיפוסים גנריים לכל משתנה
משתנה שמופיע מספר פעמים בעל אותו טיפוס
יש להוסיף טיפוס עבור הפלט של התוכנית
פונקציה תמיד תהיה מטיפוס
מבחינה פורמלית כדי למצוא את כל המשתנים והפונקציות משתמשים בעץ הגזירה של הביטוי (ניתן להחשיב את יצירת עץ הגזירה כשלב נוסף באלגוריתם)
קובעים את האילוצים
נפתור את האילוצים, מהמערכת הנ״ל אפשר לקבוע ש . אבל גם מאיך ש מוגדר אפשר להסיק כי וגם . סך הכל נקבל .
קביעת הטיפוס של הפונקציה לפי מה שאמרנו למעלה .
נחזור לפסודו קוד של של Hindley-milner type inference ממקודם
ננסה להבין איך האלגוריתם עובד על הדוגמה ממקודם בתצורת למדה כלומר :
ניתן לראות שמה AST חילצו את כל האילוצים. כעת עם האלגוריתם unify ניתן לפתור את האילוצים באופן הבא-
כל על טיפוס ועל המשוואה שלו
חפש משוואה אחרת המכילה את הטיפוס
אם מצאת החלף את הטיפוס במשוואה שהתקבלה.
ניתן לראות בסדרת הפעולות הנ״ל שכל ריצה חיפשנו את המקומות שבהם נמצא הטיפוס הזה וממש החלפנו אותו בהשמה. למשל עבור מצאנו את ב וביצענו הצבה וכעת . ככה המשכנו להציב שוב ושוב עד שהגענו לביטוי האחרון שבו כבר היה כלל החלפה. כיוון ש אז יכלנו להחליף את ב nat בהתאמה.
בצורה פורמלית:
נסתכל על דוגמה נוספת .
נבצע השמה של טיפוסים גנריים לכל התוכנית
נסתכל על האילוצים
כעת אם ננסה לפתור אותם נקבל ש וגם שזה conflict בין הטיפוסים ולכן type error.
דוגמה .
השמת טיפוסים
אילוצים : וגם
פתרון לאילוצים:
סה״כ , מצב כזה נקרא type variable כלומר פונקציה גנרית.
רקורסיה
נסתכל על הקוד הבא:
let rec length xs =
match xs with
| [] -> 0
| (h:t) -> 1 + length t
כאשר עובדים עם pattern match מסתכלים על כל מקרה בנפרד, הטיפוסים גם של כל מקרה צריכים להיות שקולים באופן הבא
הטיפוס של כל אחד מהקלטים (צד שמאל של החץ) צריכים להיות שווים
הטיפוס של כל אחד מהפלטים (צד ימין של החץ צריך להיות שווה)
נשים טיפוסים לקטע הקוד הנ״ל
נשים לב שאת הרשימה שמנו מראש בטיפוס עטוף ב כי אנחנו יודעים שהטיפוס הוא רשימה מסוג של משהו.
נבנה את האילוצים
קל להבין למה הוא פונקציה שהקלט שלה והפלט הוא
הוא טיפוס של רשימה מסוג בגלל ה case הראשון וגם מטיפוס בגלל ה case השני.
בגלל ערך החזרה של המקרה הראשון - 0 , ניתן להגיד ש הוא שקול ל int.
בנוסף בגלל אותו הסבר
בגלל פעולת ה apply אנחנו יודעים שמצד ימין יש רשימה ומצד שמאל יש אלמנט שהוא מהטיפוס של איבר ברשימה.
length היא פונקציה בגלל המקרה השני שהפלט שלה הוא int.