قبل از اینکه وارد مفهوم پارامترها و توابع دوسویه بشم، باید اول چندتا مفهوم رو توضیح بدم. مفاهیمی که به جنریکها و ماهیت ریاضیاتی توابع مربوط میشن.
اگر درمورد جنریکها نمیدونین، پیشنهاد میکنم که این مطلب امیررضا رو حتما بخونید. لازم هست که قبل از اینکه وارد این موضوع بشید، حتما در مورد جنریکها آگاهی داشته باشید.
Covariance و Contravariance
کوواریانس یا Covariance در واقع به وراثت برمیگرده. با یک مثال توضیح میدم، اگر تایپ B از A مشتق شده باشه، یعنی B وارث A هست یا به یه شکل دیگه، A کلاس پایهی B محسوب میشه. در نتیجه B تمام متدها و پراپرتیهای A رو توی خودش داره.
تو این حالت، میشه گفت که B از A بزرگتره. این ایده که B از A بزرگتره، یک ایدهی خیلی مهمه!
بذارید یکم ریاضیاتیتر توضیح بدم، اگر B از A مشتق میشه، پس میشه گفت A>B (من به جهت فلش خیلی دقت کردم، دقیقا درسته) و B هر آنچه که A داره رو توی خودش داره. حالا چون B همهی چیزهای A رو داره، ما میتونیم هرجا که از A استفاده شده، از B هم استفاده کنیم. گرچه این ارتباط اصطلاحا یکطرفه یا Asymmertical محسوب میشه. این ارتباط به عنوان اصل تعویض لیسکوف یا Liskov Substitution Principle هم شناخته میشه.
توابع
حالا بیاید ببینیم که این قائده چطور روی توابع هم استفاده میشه.
اگه ما یک فانکشن رو تعریف کنیم که شیئی از جنس A رو به عنوان ورودی دریافت کنه، و شیئی از جنس B رو خارج کنه، یا در واقع:
1f(A) -> B and A>B
برقرار باشه، طبق همین قانون ما میتونیم به عنوان ورودی تابع شیئی از جنس B رو بدیم، و شیئی از جنس B هم دریافت کنیم. در واقع میشه گفت که f(B)
جایزه. میشه نوشت:
1class A {}23class B extends A {}45function f(p: A): B {6 return new B()7}89let myA: A = f(new B())
به این معنی که اگه A>B پارامتر ورودی A میتونه B هم باشه چون هرچی که A داره رو اون هم داره. پس با همین تعریف میشه گفت که پارامترهای ورودی کنتراواریانت یا Contravariant هستن و پارامترهای خروجی، کوواریانت یا Covariant.
تایپاسکریپت
حالا بریم یکم در مورد زبان تایپاسکریپ و یکی از ویژگیهای کمتر شناخته شدهی اون صحبت کنیم، strictFunctionTypes
! این آرگومان که میشه به کامپایلر پاس داد، به شما کمک میکنه که نوع دیگهای از باگها رو شناسایی کنید و جلوشون رو بیگیرید.
چه باگهایی رو میتونه بگیره؟
بیاین اول با یه مثال بریم جلو، یک مثال که میشه جلوش رو با استفاده از این آرگومان گرفت.
1interface Post {2 title: string3}45function fetchPost(onSuccess: (post: Post) => void) {}
تو مثال بالا، fetchPost یک فانکشنه که یه callback رو قبول میکنه تا بعد از دریافت پست، اجرا بشه.
جالب اینجاست که تایپاسکریپت بدون اشکال (با تنظیمات دیفالت) کد زیر رو کامپایل میکنه:
1interface Post {2 title: string3}45function fetchPost(onSuccess: (post: Post) => void) {}67interface PostWithContent extends Post {8 content: string9}1011fetchPost((p: PostWithContent) => {12 console.log(p.content.toLowerCase())13})
متاسفانه، این کد میتونه باعث بروز اشکال تو زمان-اجرا یا Runtime بشه. فانکشنی که به عنوان کالبک به fetchPost
پاس داده شده، فقط بلده که با تایپهای خاصی که از Post مشتق شدن کار کنه. اونایی که خصوصا یک پراپرتی content
دارن. با این وجود، fetchPost میتونه همه نوع Post رو دریافت کنه، حتی اونایی که content
ندارن رو. که این باعث میشه p.content.toLowerCase()
ارور undefined
رو بده:
1TypeError: undefined is not an object (evaluating 'p.content.toLowerCase')
اینجاست که فعال کردن strictFunctionTypes
میتونه به ما کمک کنه! ارور زیر موقع کامپایل اتفاق میوفته:
1Argument of type '(p: PostWithContent) => void' is not assignable to parameter of type '(post: Post) => void'.
دوسویه یا Bivariance
اگه فقط میخواستید بدونید که strictFunctionTypes
چه کاری انجام میده، میتونید دیگه به خوندن ادامه ندید. با اینحال توصیه میکنم با من همراه باشید تا دلایل این ارورها رو به شما بگم.
اول بیاید یک جنریک بسازیم که نمایانگر یه کالبک از جنس خودش باشه:
1type CB<T> = (v: T) => void23function fetchPost(onSuccess: CB<Post>) {}45declare const cb: CB<PostWithContent>67fetchPost(cb)
دلیل اینکه fetchPost
نباید cb رو قبول کنه اینه که cb
خیلی ویژه به یک تایپ خاص اشاره داره. در نتیجه تایپ cb
نباید به تایپ onSuccess
تخصیص داده یا Assign بشه.
به بیان دیگه، این واقعیت که PostWithContent میتونه به Post تخصیص داده بشه، این رو توجیه نمیکنه که CB<PostWithContent>
هم به CB<Post>
بتونه تخصیص داده بشه. اگر میشد چنین چیزی رو توجیه کرد، پس باید میگفتیم که Callback کوواریانت محسوب میشه.
تو مثال ما دقیقا عکسش صادقه،CB<Post>
قابل تخصیص به CB<PostWothContent>
هست. دلیل اینه که یک کالبک که میتونه تمام Postها رو هندل کنه، همچنین میتونه PostWithContent رو هم هندل کنه، بنابراین میشه گفت که CB کنتراواریانت یا Contravariant هست.
اگر هردو این اظهارات درست باشن، میشه گفت که CB دو سویه یا Bivariant محسوب میشه.
حالا یه نگاه به تعریف strictFunctionTypes
کنیم:
1Disable bivariant parameter checking for function types.
خب به این معنی که اگر این آپشن رو فعال کنیم، پارامترها، به شکل تکسویه کنترل میشن و نه دوسویه!
تایپهای وابسته و توابع دوسویه
اگر مطلبی که بالاتر گذاشتم رو خوندید، میتونیم وارد یک بحث جدید بشیم. تایپهای وابسته.
تایپ وابسته یا Dependent Type چیه؟
یک تایپ در حالت عادی، صرفا یک نماده برای اینکه بگه یک مقدار یا Value چه ویژگیهایی داره. این تایپ یا یک تایپ کامله و یا اینکه به یک تایپ دیگه نیاز داره.
برای مثال، Int
یک تایپ مستقل یا Standalone
هست و Maybe
یکی که به یک تایپ دیگه نیازمند.
Maybe
برای اینکه کار کنه، نیاز داره تا یک تایپ دیگه بهش پاس داده بشه، که این باعث میشه، Maybe
یک تابع باشه که تایپ میگیره و تایپ هم برمیگردونه.
اما تایپهای وابسته کمی متفاتتر هستن. اونا به چیزهایی بیشتر از صرفا تایپ وابسته هستن. منظورم از چیزهای دیگه، مقادیر هستن، یعنی مثلا تایپ یک مقدار تو یک نقطه، میتونه تابع تایپ یک مقدار دیگه تو نقطهای دیگه از برنامه باشه.
چرا به تایپهای وابسته نیاز هست؟
برای این سوال، باید این رو بررسی کنیم که اساسا چرا به تایپچکینگ نیاز داریم. هدف از تایپچکینگ اینه که مطمئن باشیم تو زمان اجرا، مقادیر تایپ درستی دارن، و این درستی رو تو زمان کامپایل مشخص میکنیم.
یک مثال میتونه این باشه: یک فانکشن که عمل تقسیم رو انجام میده. برای اینکه مطمئن باشیم از تقسیم بر صفر جلوگیری میکنیم، میتونیم بگیم که تایپ خروجی، بستگی به مخرج تقسیم داره. اگر مثلا عددی غیر از صفر یا اصطلاحا nonezero
بود، بهمون double
و در غیر این صورت بهمون never
برگردونه. چون در این صورت یک exception رو پرت یا throw میکنه و اجرا هم نمیشه.
شما ۱۰۰٪ باهاشون کار کردین!
پیشنهاد میکنم باز هم که این مطلب امیررضا رو هم بخونید تا در مورد ADTها اطلاعات کسب کنید. اما باور کنید یا نه، شما تا حالا این مدل تایپها رو تو تایپاسکریپت دیدید. بهشون اصولا Discriminated Unions یا Algebraic Data Types (دیتا تایپهای جبری) هم میگن:
1type MyADT =2 | {3 type: "case_1"4 field: number5 }6 | {7 type: "case_2"8 field: string9 }
تو یک ADT در تایپاسکریپت، شما معمولا یک فیلد عمومی دارید که اسمش میتونه type
باشه که مقادیر ثابتی رو میتونه دریافت کنه و تایپاسکریپت میتونه فیلدهای دیگه رو بر مبنای مقدار این فیلد در نظر بگیره یا نگیره، اما ما همچنان میتونید از اسم مابقی فیلدها استفاده کنیم!
مثلا تو مثال بالا، field تو هردو موارد وجود داره اما تایپش فرق میکنه. شما میتونید اینطور باهاش کار کنید:
1if (MyADT.type === "case_1") {2 return MyADT.field * 3.14153} else {4 return `${MyADT.field} is an string`5}
Sound or Unsound
در مورد این اصطلاح توضیح زیادی نیست که بدم، اما درواقع میشه گفت یک تایپچکر اصطلاحا Sound هست، اگر، وقتی یک مقدار در اون زبان یک تایپ رو داشته باشه، همیشه و همیشه همون تایپ رو به طور قطع در زمان-اجرا داره. و یک تایپچکر اصطلاحا Unsound هست، اگر تو بعضی شرایط در زمان-اجرا، یک مقدار تایپی که بهش تو زمان کامپایل داده شده رو نداشته باشه.
1function messUpTheArray(arr: Array<string | number>): void {2 arr.push(3)3}45const strings: Array<string> = ["foo", "bar"]6messUpTheArray(strings)78const s: string = strings[2]9console.log(s.toLowerCase())
تایپاسکریپت به اشتباه (و از روی عمد) میگه که Array<string>
میتونه به Array<string | number>
تخصیص داده بشه و شما میتونید یک s
رو بسازید که تایپاسکریپت فکر کنه مقدارش استرینگه ولی درواقع عدده.
چطور تو تایپاسکریپت دوسویه بودن و وابستگی حل میشه؟
تایپاسکریپت تو بعضی قسمتها، یک زبان unsound به حساب میاد. دلیلش هم علاقهی این زبان به جلوگیری از تایپکستینگهایه که تاثیری در زمان-اجرا نداشته باشن تا فقط بتونن تایپچکر رو راضی کنن!
این کد، مستقیما از توضیحات سایت تایپاسکریپت برداشته شده:
1enum EventType {2 Mouse,3 Keyboard,4}56interface Event {7 timestamp: number8}9interface MyMouseEvent extends Event {10 x: number11 y: number12}13interface MyKeyEvent extends Event {14 keyCode: number15}1617function listenEvent(eventType: EventType, handler: (n: Event) => void) {18 /* ... */19}2021// Unsound, but useful and common22listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y))2324// Undesirable alternatives in presence of soundness25listenEvent(EventType.Mouse, (e: Event) =>26 console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)27)28listenEvent(EventType.Mouse, ((e: MyMouseEvent) =>29 console.log(e.x + "," + e.y)) as (e: Event) => void)3031// Still disallowed (clear error). Type safety enforced for wholly incompatible types32listenEvent(EventType.Mouse, (e: number) => console.log(e))
از اونجایی که تایپاسکریپ از تایپهای وابسته هم پشتیبانی میکنه، تغییر مثال بالا به پایین مشکلی رو برای ما ایجاد نمیکنه:
1// Assuming EventType, Event, MyMouseEvent, and MyKeyEvent as above23type GenericEvent<E extends EventType> = E extends EventType.Mouse4 ? MyMouseEvent5 : MyKeyEvent67function listenEvent<E extends EventType>(8 eventType: E,9 handler: (n: GenericEvent<E>) => void10): void11function listenEvent(12 eventType: EventType,13 handler: (n: GenericEvent<typeof eventType>) => void14): void {15 /* ... */16}1718// Valid19listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y))20// Valid21listenEvent(EventType.Keyboard, (e: MyKeyEvent) => console.log(e.keyCode))22// Invalid23listenEvent(EventType.Mouse, (e: MyKeyEvent) => console.log(e.keyCode))24// Invalid25listenEvent(EventType.Mouse, (e: Event) => console.log(e.x + "," + e.y))
خب تا اینجا سعی کردم این مفاهیم رو تا حد امکان ساده بگم. اگر سوالی بود میتونید تو کامنتها برای من بنویسید.
با آرزوی موفقیت.