آیین سعیدی، یه برنامه‌نویس و بلاگر

برگردید به صفحه‌ی اصلی

تایپ‌های وابسته و توابع دوسویه

January 29, 20215 دقیقه مطالعهبرای باتجربه‌ترهای علاقه‌مند به موضوعات وب و موبایل
  • #برنامه‌نویسی
  • #تایپ‌اسکریپت

قبل از اینکه وارد مفهوم پارامترها و توابع دوسویه بشم، باید اول چندتا مفهوم رو توضیح بدم. مفاهیمی که به جنریک‌ها و ماهیت ریاضیاتی توابع مربوط میشن.

اگر درمورد جنریک‌ها نمیدونین، پیشنهاد میکنم که این مطلب امیررضا رو حتما بخونید. لازم هست که قبل از اینکه وارد این موضوع بشید، حتما در مورد جنریک‌ها آگاهی داشته باشید.

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 {}
2
3class B extends A {}
4
5function f(p: A): B {
6 return new B()
7}
8
9let myA: A = f(new B())

به این معنی که اگه A>B پارامتر ورودی A میتونه B هم باشه چون هرچی که A داره رو اون هم داره. پس با همین تعریف میشه گفت که پارامترهای ورودی کنتراواریانت یا Contravariant هستن و پارامترهای خروجی، کوواریانت یا Covariant.

تایپ‌اسکریپت

حالا بریم یکم در مورد زبان تایپ‌اسکریپ و یکی از ویژگی‌های کمتر شناخته شده‌ی اون صحبت کنیم، strictFunctionTypes! این آرگومان که میشه به کامپایلر پاس داد، به شما کمک میکنه که نوع دیگه‌ای از باگ‌ها رو شناسایی کنید و جلوشون رو بیگیرید.

چه باگ‌هایی رو میتونه بگیره؟

بیاین اول با یه مثال بریم جلو، یک مثال که میشه جلوش رو با استفاده از این آرگومان گرفت.

1interface Post {
2 title: string
3}
4
5function fetchPost(onSuccess: (post: Post) => void) {}

تو مثال بالا، fetchPost یک فانکشنه که یه callback رو قبول میکنه تا بعد از دریافت پست، اجرا بشه.

جالب اینجاست که تایپ‌اسکریپت بدون اشکال (با تنظیمات دیفالت) کد زیر رو کامپایل میکنه:

1interface Post {
2 title: string
3}
4
5function fetchPost(onSuccess: (post: Post) => void) {}
6
7interface PostWithContent extends Post {
8 content: string
9}
10
11fetchPost((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) => void
2
3function fetchPost(onSuccess: CB<Post>) {}
4
5declare const cb: CB<PostWithContent>
6
7fetchPost(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: number
5 }
6 | {
7 type: "case_2"
8 field: string
9 }

تو یک ADT در تایپ‌اسکریپت، شما معمولا یک فیلد عمومی دارید که اسمش میتونه type باشه که مقادیر ثابتی رو میتونه دریافت کنه و تایپ‌اسکریپت میتونه فیلدهای دیگه رو بر مبنای مقدار این فیلد در نظر بگیره یا نگیره، اما ما همچنان میتونید از اسم مابقی فیلد‌ها استفاده کنیم!

مثلا تو مثال بالا، field تو هردو موارد وجود داره اما تایپش فرق میکنه. شما میتونید اینطور باهاش کار کنید:

1if (MyADT.type === "case_1") {
2 return MyADT.field * 3.1415
3} 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}
4
5const strings: Array<string> = ["foo", "bar"]
6messUpTheArray(strings)
7
8const s: string = strings[2]
9console.log(s.toLowerCase())

تایپ‌اسکریپت به اشتباه (و از روی عمد) میگه که Array<string> میتونه به Array<string | number> تخصیص داده بشه و شما میتونید یک s رو بسازید که تایپ‌اسکریپت فکر کنه مقدارش استرینگه ولی درواقع عدده.

چطور تو تایپ‌اسکریپت دوسویه بودن و وابستگی حل میشه؟

تایپ‌اسکریپت تو بعضی قسمت‌ها، یک زبان unsound به حساب میاد. دلیلش هم علاقه‌ی این زبان به جلوگیری از تایپ‌کستینگ‌هایه که تاثیری در زمان-اجرا نداشته باشن تا فقط بتونن تایپ‌چکر رو راضی کنن!

این کد، مستقیما از توضیحات سایت تایپ‌اسکریپت برداشته شده:

1enum EventType {
2 Mouse,
3 Keyboard,
4}
5
6interface Event {
7 timestamp: number
8}
9interface MyMouseEvent extends Event {
10 x: number
11 y: number
12}
13interface MyKeyEvent extends Event {
14 keyCode: number
15}
16
17function listenEvent(eventType: EventType, handler: (n: Event) => void) {
18 /* ... */
19}
20
21// Unsound, but useful and common
22listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y))
23
24// Undesirable alternatives in presence of soundness
25listenEvent(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)
30
31// Still disallowed (clear error). Type safety enforced for wholly incompatible types
32listenEvent(EventType.Mouse, (e: number) => console.log(e))

از اونجایی که تایپ‌اسکریپ از تایپ‌های وابسته هم پشتیبانی میکنه، تغییر مثال بالا به پایین مشکلی رو برای ما ایجاد نمیکنه:

1// Assuming EventType, Event, MyMouseEvent, and MyKeyEvent as above
2
3type GenericEvent<E extends EventType> = E extends EventType.Mouse
4 ? MyMouseEvent
5 : MyKeyEvent
6
7function listenEvent<E extends EventType>(
8 eventType: E,
9 handler: (n: GenericEvent<E>) => void
10): void
11function listenEvent(
12 eventType: EventType,
13 handler: (n: GenericEvent<typeof eventType>) => void
14): void {
15 /* ... */
16}
17
18// Valid
19listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y))
20// Valid
21listenEvent(EventType.Keyboard, (e: MyKeyEvent) => console.log(e.keyCode))
22// Invalid
23listenEvent(EventType.Mouse, (e: MyKeyEvent) => console.log(e.keyCode))
24// Invalid
25listenEvent(EventType.Mouse, (e: Event) => console.log(e.x + "," + e.y))

خب تا اینجا سعی کردم این مفاهیم رو تا حد امکان ساده بگم. اگر سوالی بود میتونید تو کامنت‌ها برای من بنویسید.

با آرزوی موفقیت.

میخوای همیشه بروز باشی؟

کافیه خیلی ساده ایمیلت رو اینجا بدی و من به محض انتشار یه پست جدید بهت خبر میدم. راستی، ایمیل رو با کسی به اشتراک نمیذارم و قرار نیست اسپم دریافت کنی.

کپی‌رایت 2021، حقوق معنوی محفوظ هست، ولی میتونید با ذکر منبع مطالب رو منتشر کنید.
Link to $https://twitter.com/aientechLink to $https://you.aien.me/joinLink to $https://www.instagram.com/aientech/Link to $https://github.com/AienTech