Type inference

בOCaml אחד הפיצרים שהקומפיילר נותן הוא type inference. הפיצ׳ר הזה מאפשר למתכנת לא להצהיר על המשתנה ישירות אלא לתת לקומפיילר באמצעות כללי היסק לפרש את הטיפוס בעצמו. Type inference נותן לנו את היכולת להמיר תחשיב למדא ללא טיפוסים לתחשיב למדא עם טיפוסים וככה לקבל את הפיצ׳ר של type safety.

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

  1. כל משתנה הוא ביטוי.
  2. אם x משתנה ו t ביטוי אז fun xt ביטוי.
  3. אם t1,t2 ביטויים אז t1t2 ביטויים.

הגדרה:
נניח קבוצה אינסופית של משתני טיפוס.
- אם α משתנה טיפוס אז הוא טיפוס.
- אם T,S טיפוסים אז כך גם TS .

הגדרה:
יחס ההטפסה של λOCaml Γt:T מוגדר כך -

O-VAR:x:TΓΓx:T O-APPΓt12:T1T2  Γt1:T1Γt12t1:T2 O-ABSΓx:T1  Γt:T2Γ(fun xt):T1T2

בעיית הסקת הטיפוסים

נגדיר את בעיה זו כך:
קלט- ביטוי t
פלט- הקשר הטפסה Γ ומיפוי m מביטויים שמופיעים ב t לטיפוסים כך שלכל תת-ביטוי t של t מתקיים Γt:m(t) (כולל t עצמו).

פתרון:

  1. יצירת מערכת משוואות בין ביטוי לטיפוס
  2. פתרון המערכת
  3. תרגום הפתרון ל Γ ו m המתאימים.

יצירת מערכת משוואות

נגדיר t ביטוי נורמלי אם לכל 2 ביטויים t1= fun x... ו t2= fun y... שמופיעים ב t , המשתנים x ו y שונים זה מזה.

נשים לב: לכל ביטוי יש ביטוי נורמלי השקול לו על ידי שימוש בכלל אלפה .

הגדרה עבור t ביטוי נגדיר At קבוצת המשוואות. לכל מופע של כל ביטוי שמופיע ב t ניצור משתנה טיפוס משלו ואז

  1. אם α,β מתאימים למופעים שונים של אותו הטיפוס אז נוסיף את α=β ל At .למשל עבור הפונקציה fun xx נסמן עבור ה x הראשון שהוא מטיפוס αx1 והשני הוא מטיפוס αx2 ונוסיף את αx1=αx2 ל At .
  2. אם משתני הטיפוס α,β,γ מתאימים ל t1,t2,t1t2 בהתאמה אז נוסיף את α=βγ ל At.
  3. לכל מופע fun xt אם α,β,γ מתאימים ל x,t,fun xt בהתאמה אז נוסיף את γ=αβ ל At.

דוגמה: עבור הביטוי t=( fun xx)y

yαyxyx1xαx2 fun x xαfun x xtαt

כעת נגדיר את At:

AT={αx1=αx2  ,  αfun x x=αx1αx2  ,  αfun x x=αyαt}

תרגום הפתרון

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

σ(T1T2)=σ(T1)σ(T2)

הגדרה: החלפה σ מאחדת שיוויון T1=T2 אם σ(T1)=σ(T2) .
σ מאחדת קבוצת שוויונות אם היא מאחדת את כל השיוויונות בקבוצה.

נסתכל על הדוגמה ממקודם-

σ(αx1)=σ(αx2)=σ(αy)=σ(αt)=α

וגם

σ(αfun xx)=αα=σ(αy)σ(αt)=σ(αyαt)=σ(αx1σx2)

הגדרה: יהי t ביטוי ו β פונקציה שמתאימה לכל תת ביטוי t של t את משתנה הטיפוס המתאים לאחד המופעים של t ותהי α החלפה שמאחדת את At אז

Γσβ={x:σ(β(x))  |  xVars(t)}

משפט: Γσβ ו σβ הם הפתרון לבעיית ההטפסה של t. בצורה פורמלית: יהי t ביטוי ו β,σ כמו לעיל אז Γσβt:σ(β(t))

אם אין σ כנ״ל אז t לא מטופס היטב.

פתרון מערכת משוואות

הגדרה:
בעיית היוניפיקציה-
קלט- קבוצה של שוויונות בין ביטויים
פלט- החלפה σ שמאחדת אותם

למשל אם x=y נרצה לבנות σ כך ש σ(x)=σ(y) נוכל להגדיר ש σ(x)=σ(y)=x .

ומה אם המשוואה אומרת ש x+y=xy ? כיוון שלא משנה מה יהיו כללי ההחלפה של המשתנים יתקיים σ(x+y)=σ(x)+σ(y) וכנ״ל עבור אז אין פתרון.

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

f(t1,,tn)=f(s1,,sn)

נוכל למצוא σ שתאחד כל ti,si בנפרד כלומר סיגמה שתקיים σ(ti)=σ(si).

והמקרה האחרון שיכול לעניין הוא x=f(y) נוכל להגדיר ש σ(x)=f(y) ו σ(y)=y .

כעת נסתכל על האלגוריתם בOCaml:

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;;

ובפסודו קוד זה האלגוריתם לקביעת הטיפוסים
Pasted image 20240322221846.png|400

נסתכל על דוגמה של ה ביטוי αα. לפי הקוד זה יהיה מהצורה Term("->", [Var ('a'), Var('a')].

Note

האלגוריתם הזה הוא לא היחיד שפותר את בעיית היוניפיקציה, האלגוריתם הזה מוצא את מה שנקרא Most General Unifier (MGU) וכל פתרון אחר אפשר למצוא באמצעות MGU . נאמר ש σ היא יותר ״כללית״ מ σ אם קיימת σ כך ש σ=σσ

דוגמה
let f x = 2+x .

  1. נשים טיפוסים גנריים לכל משתנה
 let f:t1  x:t2=(2:t3+:t4x):t5
  1. קובעים את האילוצים
t1=t2t5t4=t3t2t5t4=intintintt3=int
  1. נפתור את האילוצים, מהמערכת הנ״ל אפשר לקבוע ש t4=intt2t5 . אבל גם מאיך ש t4 מוגדר אפשר להסיק כי t2=int וגם t5=int . סך הכל נקבל f:intint .

  2. קביעת הטיפוס של הפונקציה לפי מה שאמרנו למעלה f:intint .

נחזור לפסודו קוד של של Hindley-milner type inference ממקודם

Screenshot 2024-03-24 at 19.05.55.png|400

ננסה להבין איך האלגוריתם עובד על הדוגמה ממקודם בתצורת למדה כלומר f=λx.x+2 :

Screenshot 2024-03-24 at 19.07.41.png|400

ניתן לראות שמה AST חילצו את כל האילוצים. כעת עם האלגוריתם unify ניתן לפתור את האילוצים באופן הבא-

  1. כל על טיפוס ועל המשוואה שלו
    1. חפש משוואה אחרת המכילה את הטיפוס
    2. אם מצאת החלף את הטיפוס במשוואה שהתקבלה.
      Pasted image 20240324191005.png|200
      Screenshot 2024-03-24 at 19.09.37.png|200
      Screenshot 2024-03-24 at 19.10.35.png|200
      Pasted image 20240324191107.png|200
      Screenshot 2024-03-24 at 19.11.46.png|200

ניתן לראות בסדרת הפעולות הנ״ל שכל ריצה חיפשנו את המקומות שבהם נמצא הטיפוס הזה וממש החלפנו אותו בהשמה. למשל עבור T3=T4T2 מצאנו את T3 ב T5 וביצענו הצבה וכעת T5=T1T3=T1T4T2 . ככה המשכנו להציב שוב ושוב עד שהגענו לביטוי האחרון שבו כבר היה כלל החלפה. כיוון ש T1natT2=natnatnat אז יכלנו להחליף את T1,T2 ב nat בהתאמה.

בצורה פורמלית:
Screenshot 2024-03-24 at 19.15.08.png|400

נסתכל על דוגמה נוספת let f x = 5.+x .
נבצע השמה של טיפוסים גנריים לכל התוכנית
Screenshot 2024-03-24 at 19.21.02.png|200

נסתכל על האילוצים

t1=t2t5t4=t3t2t5t4=intintintt3=float

כעת אם ננסה לפתור אותם נקבל ש t4=floatt2t5 וגם t4=intintint שזה conflict בין הטיפוסים ולכן type error.

דוגמה
let f x y = x y .

  1. השמת טיפוסים
    Screenshot 2024-03-24 at 19.29.01.png|300
  2. אילוצים : t1=t2t3t4 וגם t2=t3t4
  3. פתרון לאילוצים: t1=(t3t4)t3t4
  4. סה״כ f:(ab)ab , מצב כזה נקרא type variable כלומר פונקציה גנרית.

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

let rec length xs = 
	match xs with 
		| [] -> 0
		| (h:t) -> 1 + length t

כאשר עובדים עם pattern match מסתכלים על כל מקרה בנפרד, הטיפוסים גם של כל מקרה צריכים להיות שקולים באופן הבא

  1. הטיפוס של כל אחד מהקלטים (צד שמאל של החץ) צריכים להיות שווים
  2. הטיפוס של כל אחד מהפלטים (צד ימין של החץ צריך להיות שווה)

נשים טיפוסים לקטע הקוד הנ״ל

Screenshot 2024-03-24 at 20.12.19.png|300

נשים לב שאת הרשימה שמנו מראש בטיפוס עטוף ב [] כי אנחנו יודעים שהטיפוס הוא רשימה מסוג של משהו.

נבנה את האילוצים
Pasted image 20240324201744.png|350

  1. קל להבין למה t1 הוא פונקציה שהקלט שלה t2 והפלט הוא t3
  2. t2 הוא טיפוס של רשימה מסוג t4 בגלל ה case הראשון וגם מטיפוס t7 בגלל ה case השני.
  3. בגלל ערך החזרה של המקרה הראשון - 0 , ניתן להגיד ש t3 הוא שקול ל int.
  4. בנוסף t3=t8 בגלל אותו הסבר
  5. בגלל פעולת ה apply אנחנו יודעים שמצד ימין יש רשימה ומצד שמאל יש אלמנט שהוא מהטיפוס של איבר ברשימה.
  6. length היא פונקציה בגלל המקרה השני שהפלט שלה הוא int.

כך ניתן לפתור את האילוצים ולגלות ש length:[a]int .